diff options
Diffstat (limited to 'src/android')
12 files changed, 160 insertions, 41 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt index c408485c6..55abba093 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt @@ -303,6 +303,11 @@ object NativeLibrary { */ external fun getCpuBackend(): String + /** + * Returns the current GPU Driver. + */ + external fun getGpuDriver(): String + external fun applySettings() external fun logSettings() @@ -615,6 +620,11 @@ object NativeLibrary { external fun clearFilesystemProvider() /** + * Checks if all necessary keys are present for decryption + */ + external fun areKeysPresent(): Boolean + + /** * Button type for use in onTouchEvent */ object ButtonType { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AbstractDiffAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AbstractDiffAdapter.kt index f006f9e3d..0ab1b46c3 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AbstractDiffAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AbstractDiffAdapter.kt @@ -14,15 +14,20 @@ import androidx.recyclerview.widget.RecyclerView * Generic adapter that implements an [AsyncDifferConfig] and covers some of the basic boilerplate * code used in every [RecyclerView]. * Type assigned to [Model] must inherit from [Object] in order to be compared properly. + * @param exact Decides whether each item will be compared by reference or by their contents */ -abstract class AbstractDiffAdapter<Model : Any, Holder : AbstractViewHolder<Model>> : - ListAdapter<Model, Holder>(AsyncDifferConfig.Builder(DiffCallback<Model>()).build()) { +abstract class AbstractDiffAdapter<Model : Any, Holder : AbstractViewHolder<Model>>( + exact: Boolean = true +) : ListAdapter<Model, Holder>(AsyncDifferConfig.Builder(DiffCallback<Model>(exact)).build()) { override fun onBindViewHolder(holder: Holder, position: Int) = holder.bind(currentList[position]) - private class DiffCallback<Model> : DiffUtil.ItemCallback<Model>() { + private class DiffCallback<Model>(val exact: Boolean) : DiffUtil.ItemCallback<Model>() { override fun areItemsTheSame(oldItem: Model & Any, newItem: Model & Any): Boolean { - return oldItem === newItem + if (exact) { + return oldItem === newItem + } + return oldItem == newItem } @SuppressLint("DiffUtilEquals") 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 b4f4d950f..85c8249e6 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 @@ -30,7 +30,7 @@ import org.yuzu.yuzu_emu.utils.GameIconUtils import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder class GameAdapter(private val activity: AppCompatActivity) : - AbstractDiffAdapter<Game, GameAdapter.GameViewHolder>() { + AbstractDiffAdapter<Game, GameAdapter.GameViewHolder>(exact = false) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder { CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false) .also { return GameViewHolder(it) } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt index 2a97ae14d..d17e087fe 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt @@ -38,7 +38,6 @@ import androidx.window.layout.WindowLayoutInfo import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.slider.Slider import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import org.yuzu.yuzu_emu.HomeNavigationDirections @@ -141,7 +140,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { // So this fragment doesn't restart on configuration changes; i.e. rotation. retainInstance = true - emulationState = EmulationState(game.path) + emulationState = EmulationState(game.path) { + return@EmulationState driverViewModel.isInteractionAllowed.value + } } /** @@ -371,6 +372,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } } launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + driverViewModel.isInteractionAllowed.collect { + if (it) { + startEmulation() + } + } + } + } + launch { repeatOnLifecycle(Lifecycle.State.CREATED) { emulationViewModel.emulationStarted.collectLatest { if (it) { @@ -398,19 +408,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } } } - launch { - repeatOnLifecycle(Lifecycle.State.RESUMED) { - driverViewModel.isInteractionAllowed.collect { - if (it) { - onEmulationStart() - } - } - } - } } } - private fun onEmulationStart() { + private fun startEmulation() { if (!NativeLibrary.isRunning() && !NativeLibrary.isPaused()) { if (!DirectoryInitialization.areDirectoriesReady) { DirectoryInitialization.start() @@ -485,12 +486,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { val FRAMETIME = 2 val SPEED = 3 perfStatsUpdater = { - if (emulationViewModel.emulationStarted.value) { + if (emulationViewModel.emulationStarted.value && + !emulationViewModel.isEmulationStopping.value + ) { val perfStats = NativeLibrary.getPerfStats() val cpuBackend = NativeLibrary.getCpuBackend() + val gpuDriver = NativeLibrary.getGpuDriver() if (_binding != null) { binding.showFpsText.text = - String.format("FPS: %.1f\n%s", perfStats[FPS], cpuBackend) + String.format("FPS: %.1f\n%s/%s", perfStats[FPS], cpuBackend, gpuDriver) } perfStatsUpdateHandler.postDelayed(perfStatsUpdater!!, 800) } @@ -807,7 +811,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } } - private class EmulationState(private val gamePath: String) { + private class EmulationState( + private val gamePath: String, + private val emulationCanStart: () -> Boolean + ) { private var state: State private var surface: Surface? = null @@ -901,6 +908,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { State.PAUSED -> Log.warning( "[EmulationFragment] Surface cleared while emulation paused." ) + else -> Log.warning( "[EmulationFragment] Surface cleared while emulation stopped." ) @@ -910,6 +918,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { private fun runWithValidSurface() { NativeLibrary.surfaceChanged(surface) + if (!emulationCanStart.invoke()) { + return + } + when (state) { State.STOPPED -> { val emulationThread = Thread({ diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt index 620d8db7c..22b084b9a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt @@ -26,9 +26,15 @@ class MessageDialogFragment : DialogFragment() { val descriptionId = requireArguments().getInt(DESCRIPTION_ID) val descriptionString = requireArguments().getString(DESCRIPTION_STRING)!! val helpLinkId = requireArguments().getInt(HELP_LINK) + val dismissible = requireArguments().getBoolean(DISMISSIBLE) + val clearPositiveAction = requireArguments().getBoolean(CLEAR_POSITIVE_ACTION) val builder = MaterialAlertDialogBuilder(requireContext()) + if (clearPositiveAction) { + messageDialogViewModel.positiveAction = null + } + if (messageDialogViewModel.positiveAction == null) { builder.setPositiveButton(R.string.close, null) } else { @@ -51,6 +57,8 @@ class MessageDialogFragment : DialogFragment() { } } + isCancelable = dismissible + return builder.show() } @@ -67,6 +75,8 @@ class MessageDialogFragment : DialogFragment() { private const val DESCRIPTION_ID = "DescriptionId" private const val DESCRIPTION_STRING = "DescriptionString" private const val HELP_LINK = "Link" + private const val DISMISSIBLE = "Dismissible" + private const val CLEAR_POSITIVE_ACTION = "ClearPositiveAction" fun newInstance( activity: FragmentActivity? = null, @@ -75,22 +85,28 @@ class MessageDialogFragment : DialogFragment() { descriptionId: Int = 0, descriptionString: String = "", helpLinkId: Int = 0, + dismissible: Boolean = true, positiveAction: (() -> Unit)? = null ): MessageDialogFragment { + var clearPositiveAction = false + if (activity != null) { + ViewModelProvider(activity)[MessageDialogViewModel::class.java].apply { + clear() + this.positiveAction = positiveAction + } + } else { + clearPositiveAction = true + } + val dialog = MessageDialogFragment() - val bundle = Bundle() - bundle.apply { + val bundle = Bundle().apply { putInt(TITLE_ID, titleId) putString(TITLE_STRING, titleString) putInt(DESCRIPTION_ID, descriptionId) putString(DESCRIPTION_STRING, descriptionString) putInt(HELP_LINK, helpLinkId) - } - if (activity != null) { - ViewModelProvider(activity)[MessageDialogViewModel::class.java].apply { - clear() - this.positiveAction = positiveAction - } + putBoolean(DISMISSIBLE, dismissible) + putBoolean(CLEAR_POSITIVE_ACTION, clearPositiveAction) } dialog.arguments = bundle return dialog 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 064342cdd..ebf41a639 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 @@ -31,6 +31,7 @@ import androidx.preference.PreferenceManager import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback import com.google.android.material.transition.MaterialFadeThrough import kotlinx.coroutines.launch +import org.yuzu.yuzu_emu.NativeLibrary import java.io.File import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication @@ -162,7 +163,7 @@ class SetupFragment : Fragment() { R.string.install_prod_keys_warning_help, { val file = File(DirectoryInitialization.userDirectory + "/keys/prod.keys") - if (file.exists()) { + if (file.exists() && NativeLibrary.areKeysPresent()) { StepState.COMPLETE } else { StepState.INCOMPLETE @@ -347,7 +348,8 @@ class SetupFragment : Fragment() { val getProdKey = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> if (result != null) { - if (mainActivity.processKey(result)) { + mainActivity.processKey(result) + if (NativeLibrary.areKeysPresent()) { keyCallback.onStepCompleted() } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt index 15ae3a42b..5ed754c96 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt @@ -144,6 +144,7 @@ class DriverViewModel : ViewModel() { val selectedDriverFile = File(StringSetting.DRIVER_PATH.getString()) val selectedDriverMetadata = GpuDriverHelper.customDriverSettingData if (GpuDriverHelper.installedCustomDriverData == selectedDriverMetadata) { + setDriverReady() return } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt index c8a4a2d17..6859b7780 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt @@ -70,11 +70,19 @@ class Game( } override fun equals(other: Any?): Boolean { - if (other !is Game) { - return false - } + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Game + + if (title != other.title) return false + if (path != other.path) return false + if (programId != other.programId) return false + if (developer != other.developer) return false + if (version != other.version) return false + if (isHomebrew != other.isHomebrew) return false - return hashCode() == other.hashCode() + return true } override fun hashCode(): Int { 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 513ac2fc5..cfc777b81 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 @@ -31,6 +31,9 @@ class HomeViewModel : ViewModel() { private val _reloadPropertiesList = MutableStateFlow(false) val reloadPropertiesList get() = _reloadPropertiesList.asStateFlow() + private val _checkKeys = MutableStateFlow(false) + val checkKeys = _checkKeys.asStateFlow() + var navigatedToSetup = false fun setNavigationVisibility(visible: Boolean, animated: Boolean) { @@ -66,4 +69,8 @@ class HomeViewModel : ViewModel() { fun reloadPropertiesList(reload: Boolean) { _reloadPropertiesList.value = reload } + + fun setCheckKeys(value: Boolean) { + _checkKeys.value = value + } } 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 c2cc29961..b3967d294 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 @@ -64,6 +64,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider { override var themeId: Int = 0 + private val CHECKED_DECRYPTION = "CheckedDecryption" + private var checkedDecryption = false + override fun onCreate(savedInstanceState: Bundle?) { val splashScreen = installSplashScreen() splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady } @@ -75,6 +78,18 @@ class MainActivity : AppCompatActivity(), ThemeProvider { binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) + if (savedInstanceState != null) { + checkedDecryption = savedInstanceState.getBoolean(CHECKED_DECRYPTION) + } + if (!checkedDecryption) { + val firstTimeSetup = PreferenceManager.getDefaultSharedPreferences(applicationContext) + .getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true) + if (!firstTimeSetup) { + checkKeys() + } + checkedDecryption = true + } + WindowCompat.setDecorFitsSystemWindows(window, false) window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING) @@ -150,6 +165,16 @@ class MainActivity : AppCompatActivity(), ThemeProvider { } } } + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + homeViewModel.checkKeys.collect { + if (it) { + checkKeys() + homeViewModel.setCheckKeys(false) + } + } + } + } } // Dismiss previous notifications (should not happen unless a crash occurred) @@ -158,6 +183,21 @@ class MainActivity : AppCompatActivity(), ThemeProvider { setInsets() } + private fun checkKeys() { + if (!NativeLibrary.areKeysPresent()) { + MessageDialogFragment.newInstance( + titleId = R.string.keys_missing, + descriptionId = R.string.keys_missing_description, + helpLinkId = R.string.keys_missing_help + ).show(supportFragmentManager, MessageDialogFragment.TAG) + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putBoolean(CHECKED_DECRYPTION, checkedDecryption) + } + fun finishSetup(navController: NavController) { navController.navigate(R.id.action_firstTimeSetupFragment_to_gamesFragment) (binding.navigationView as NavigationBarView).setupWithNavController(navController) @@ -349,6 +389,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { R.string.install_keys_success, Toast.LENGTH_SHORT ).show() + homeViewModel.setCheckKeys(true) gamesViewModel.reloadGames(true) return true } else { @@ -399,6 +440,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { firmwarePath.deleteRecursively() cacheFirmwareDir.copyRecursively(firmwarePath, true) NativeLibrary.initializeSystem(true) + homeViewModel.setCheckKeys(true) getString(R.string.save_file_imported_success) } } catch (e: Exception) { diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 963f57380..247f2c2b3 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -247,6 +247,7 @@ Core::SystemResultStatus EmulationSession::InitializeEmulation(const std::string m_system.GetCpuManager().OnGpuReady(); m_system.RegisterExitCallback([&] { HaltEmulation(); }); + OnEmulationStarted(); return Core::SystemResultStatus::Success; } @@ -463,8 +464,8 @@ int Java_org_yuzu_yuzu_1emu_NativeLibrary_installFileToNand(JNIEnv* env, jobject }; return static_cast<int>( - ContentManager::InstallNSP(&EmulationSession::GetInstance().System(), - EmulationSession::GetInstance().System().GetFilesystem().get(), + ContentManager::InstallNSP(EmulationSession::GetInstance().System(), + *EmulationSession::GetInstance().System().GetFilesystem(), GetJString(env, j_file), callback)); } @@ -674,6 +675,11 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCpuBackend(JNIEnv* env, jclass return ToJString(env, "JIT"); } +jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getGpuDriver(JNIEnv* env, jobject jobj) { + return ToJString(env, + EmulationSession::GetInstance().System().GPU().Renderer().GetDeviceVendor()); +} + void Java_org_yuzu_yuzu_1emu_NativeLibrary_applySettings(JNIEnv* env, jobject jobj) { EmulationSession::GetInstance().System().ApplySettings(); } @@ -819,7 +825,7 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_removeUpdate(JNIEnv* env, jobject job void Java_org_yuzu_yuzu_1emu_NativeLibrary_removeDLC(JNIEnv* env, jobject jobj, jstring jprogramId) { auto program_id = EmulationSession::GetProgramId(env, jprogramId); - ContentManager::RemoveAllDLC(&EmulationSession::GetInstance().System(), program_id); + ContentManager::RemoveAllDLC(EmulationSession::GetInstance().System(), program_id); } void Java_org_yuzu_yuzu_1emu_NativeLibrary_removeMod(JNIEnv* env, jobject jobj, jstring jprogramId, @@ -829,8 +835,9 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_removeMod(JNIEnv* env, jobject jobj, program_id, GetJString(env, jname)); } -jobject Java_org_yuzu_yuzu_1emu_NativeLibrary_verifyInstalledContents(JNIEnv* env, jobject jobj, - jobject jcallback) { +jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_verifyInstalledContents(JNIEnv* env, + jobject jobj, + jobject jcallback) { auto jlambdaClass = env->GetObjectClass(jcallback); auto jlambdaInvokeMethod = env->GetMethodID( jlambdaClass, "invoke", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"); @@ -842,7 +849,7 @@ jobject Java_org_yuzu_yuzu_1emu_NativeLibrary_verifyInstalledContents(JNIEnv* en auto& session = EmulationSession::GetInstance(); std::vector<std::string> result = ContentManager::VerifyInstalledContents( - &session.System(), session.GetContentProvider(), callback); + session.System(), *session.GetContentProvider(), callback); jobjectArray jresult = env->NewObjectArray(result.size(), IDCache::GetStringClass(), ToJString(env, "")); for (size_t i = 0; i < result.size(); ++i) { @@ -863,7 +870,7 @@ jint Java_org_yuzu_yuzu_1emu_NativeLibrary_verifyGameContents(JNIEnv* env, jobje }; auto& session = EmulationSession::GetInstance(); return static_cast<jint>( - ContentManager::VerifyGameContents(&session.System(), GetJString(env, jpath), callback)); + ContentManager::VerifyGameContents(session.System(), GetJString(env, jpath), callback)); } jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject jobj, @@ -912,4 +919,10 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_clearFilesystemProvider(JNIEnv* env, EmulationSession::GetInstance().GetContentProvider()->ClearAllEntries(); } +jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_areKeysPresent(JNIEnv* env, jobject jobj) { + auto& system = EmulationSession::GetInstance().System(); + system.GetFileSystemController().CreateFactories(*system.GetFilesystem()); + return ContentManager::AreKeysPresent(); +} + } // extern "C" diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 779eb36a8..3cd1586fd 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -144,6 +144,9 @@ <string name="no_save_data_found">No save data found</string> <string name="verify_installed_content">Verify installed content</string> <string name="verify_installed_content_description">Checks all installed content for corruption</string> + <string name="keys_missing">Encryption keys are missing</string> + <string name="keys_missing_description">Firmware and retail games cannot be decrypted</string> + <string name="keys_missing_help">https://yuzu-emu.org/help/quickstart/#dumping-decryption-keys</string> <!-- Applet launcher strings --> <string name="applets">Applet launcher</string> |