diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts index 8fc4a82088..e57faf3ff9 100644 --- a/src/android/app/build.gradle.kts +++ b/src/android/app/build.gradle.kts @@ -155,7 +155,6 @@ android { } } - externalNativeBuild { cmake { version = "3.22.1" diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml index 353c2f722d..d31deaa355 100644 --- a/src/android/app/src/main/AndroidManifest.xml +++ b/src/android/app/src/main/AndroidManifest.xml @@ -12,7 +12,7 @@ SPDX-FileCopyrightText: Eden Emulator Project SPDX-License-Identifier: GPL-3.0-or-later --> - + @@ -42,6 +42,7 @@ SPDX-License-Identifier: GPL-3.0-or-later android:banner="@drawable/tv_banner" android:fullBackupContent="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules_api_31" + tools:targetApi="33" android:enableOnBackInvokedCallback="true"> @@ -85,6 +86,10 @@ SPDX-License-Identifier: GPL-3.0-or-later + + + + @@ -100,4 +105,4 @@ SPDX-License-Identifier: GPL-3.0-or-later - \ No newline at end of file + 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 c0e5983fc6..ba50bcad34 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 @@ -4,7 +4,6 @@ // SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later - package org.yuzu.yuzu_emu import android.content.DialogInterface @@ -17,7 +16,6 @@ import android.widget.TextView import androidx.annotation.Keep import androidx.core.net.toUri import com.google.android.material.dialog.MaterialAlertDialogBuilder -import net.swiftzer.semver.SemVer import java.lang.ref.WeakReference import org.yuzu.yuzu_emu.activities.EmulationActivity import org.yuzu.yuzu_emu.fragments.CoreErrorDialogFragment @@ -28,7 +26,6 @@ import org.yuzu.yuzu_emu.model.InstallResult import org.yuzu.yuzu_emu.model.Patch import org.yuzu.yuzu_emu.model.GameVerificationResult import org.yuzu.yuzu_emu.network.NetPlayManager -import java.io.File /** * Class which contains methods that interact @@ -276,8 +273,7 @@ object NativeLibrary { val emulationActivity = sEmulationActivity.get() if (emulationActivity != null) { emulationActivity.addNetPlayMessages(type, message) - } - else { + } else { NetPlayManager.addNetPlayMessage(type, message) } } 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 40200931b7..1aa300f82d 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 @@ -4,7 +4,6 @@ // SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later - package org.yuzu.yuzu_emu.activities import android.annotation.SuppressLint @@ -58,7 +57,6 @@ import org.yuzu.yuzu_emu.utils.NativeConfig import org.yuzu.yuzu_emu.utils.NfcReader import org.yuzu.yuzu_emu.utils.ParamPackage import org.yuzu.yuzu_emu.utils.ThemeHelper -import org.yuzu.yuzu_emu.utils.PowerStateUtils import java.text.NumberFormat import kotlin.math.roundToInt @@ -421,7 +419,6 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { NetPlayManager.addNetPlayMessage(type, msg) } - private var pictureInPictureReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent) { if (intent.action == actionPlay) { 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 c4652f55e1..11b81a01a6 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 @@ -4,15 +4,10 @@ package org.yuzu.yuzu_emu.adapters import android.content.DialogInterface -import android.net.Uri import android.text.Html -import android.text.method.LinkMovementMethod import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.LinearLayout import android.widget.ImageView -import android.widget.TextView import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.core.content.pm.ShortcutInfoCompat @@ -37,7 +32,6 @@ import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.utils.GameIconUtils import org.yuzu.yuzu_emu.utils.ViewUtils.marquee import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder -import androidx.recyclerview.widget.RecyclerView import androidx.core.net.toUri import androidx.core.content.edit import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -94,7 +88,7 @@ class GameAdapter(private val activity: AppCompatActivity) : } VIEW_TYPE_CAROUSEL -> { val carouselBinding = holder.binding as CardGameCarouselBinding - //soothens transient flickering + // soothens transient flickering carouselBinding.cardGameCarousel.scaleY = 0f carouselBinding.cardGameCarousel.alpha = 0f } @@ -103,9 +97,21 @@ class GameAdapter(private val activity: AppCompatActivity) : override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder { val binding = when (viewType) { - VIEW_TYPE_LIST -> CardGameListBinding.inflate(LayoutInflater.from(parent.context), parent, false) - VIEW_TYPE_GRID -> CardGameGridBinding.inflate(LayoutInflater.from(parent.context), parent, false) - VIEW_TYPE_CAROUSEL -> CardGameCarouselBinding.inflate(LayoutInflater.from(parent.context), parent, false) + VIEW_TYPE_LIST -> CardGameListBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + VIEW_TYPE_GRID -> CardGameGridBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + VIEW_TYPE_CAROUSEL -> CardGameCarouselBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) else -> throw IllegalArgumentException("Invalid view type") } return GameViewHolder(binding, viewType) @@ -212,7 +218,10 @@ class GameAdapter(private val activity: AppCompatActivity) : .setIcon(GameIconUtils.getShortcutIcon(activity, game)) .setIntent(game.launchIntent) .build() - ShortcutManagerCompat.pushDynamicShortcut(YuzuApplication.appContext, shortcut) + ShortcutManagerCompat.pushDynamicShortcut( + YuzuApplication.appContext, + shortcut + ) } } @@ -232,7 +241,7 @@ class GameAdapter(private val activity: AppCompatActivity) : .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> launch() } - .setNegativeButton(android.R.string.cancel) { _,_ -> } + .setNegativeButton(android.R.string.cancel) { _, _ -> } .show() } else { launch() diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt index 5d72efa350..629b3a983c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt @@ -6,10 +6,8 @@ package org.yuzu.yuzu_emu.adapters import android.view.LayoutInflater import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import androidx.lifecycle.LifecycleOwner -import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.CardHomeOptionBinding import org.yuzu.yuzu_emu.fragments.MessageDialogFragment import org.yuzu.yuzu_emu.model.HomeSetting diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/ChatDialog.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/ChatDialog.kt index 2ae0377a95..5d6679bd28 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/ChatDialog.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/ChatDialog.kt @@ -28,8 +28,7 @@ class ChatMessage( val username: String, // Username is the community/forum username val message: String, val timestamp: String = SimpleDateFormat("HH:mm", Locale.getDefault()).format(Date()) -) { -} +) class ChatDialog(context: Context) : BottomSheetDialog(context) { private lateinit var binding: DialogChatBinding @@ -50,7 +49,8 @@ class ChatDialog(context: Context) : BottomSheetDialog(context) { behavior.state = BottomSheetBehavior.STATE_EXPANDED behavior.state = BottomSheetBehavior.STATE_EXPANDED - behavior.skipCollapsed = context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + behavior.skipCollapsed = + context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE handler.post { chatAdapter.notifyDataSetChanged() @@ -133,10 +133,12 @@ class ChatAdapter(private val messages: List) : fun bind(message: ChatMessage) { binding.usernameText.text = message.nickname binding.messageText.text = message.message - binding.userIcon.setImageResource(when (message.nickname) { - "System" -> R.drawable.ic_system - else -> R.drawable.ic_user - }) + binding.userIcon.setImageResource( + when (message.nickname) { + "System" -> R.drawable.ic_system + else -> R.drawable.ic_user + } + ) } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/LobbyBrowser.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/LobbyBrowser.kt index 39391d6e53..57fd551e02 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/LobbyBrowser.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/LobbyBrowser.kt @@ -220,7 +220,7 @@ class LobbyBrowser(context: Context) : BottomSheetDialog(context) { val baseList = NetPlayManager.getPublicRooms() val filteredList = baseList.filter { room -> (!binding.chipHideFull.isChecked || room.members.size < room.maxPlayers) && - (!binding.chipHideEmpty.isChecked || room.members.isNotEmpty()) + (!binding.chipHideEmpty.isChecked || room.members.isNotEmpty()) } if (binding.searchText.text.toString().isEmpty() && @@ -245,7 +245,6 @@ class LobbyBrowser(context: Context) : BottomSheetDialog(context) { it.score }.map { it.item } adapter.updateRooms(sortedList) - } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/NetPlayDialog.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/NetPlayDialog.kt index cd3e9a4474..99ab3ebab2 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/NetPlayDialog.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/NetPlayDialog.kt @@ -16,7 +16,6 @@ import android.view.View import android.view.ViewGroup import android.widget.ArrayAdapter import android.widget.Button -import android.widget.EditText import android.widget.PopupMenu import android.widget.Toast import androidx.recyclerview.widget.LinearLayoutManager @@ -38,7 +37,6 @@ import org.yuzu.yuzu_emu.network.NetDataValidators import org.yuzu.yuzu_emu.network.NetPlayManager import org.yuzu.yuzu_emu.utils.CompatUtils import org.yuzu.yuzu_emu.utils.GameHelper -import java.net.InetAddress class NetPlayDialog(context: Context) : BottomSheetDialog(context) { private lateinit var adapter: NetPlayAdapter @@ -55,7 +53,9 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) { context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE when { - NetPlayManager.netPlayIsJoined() -> DialogMultiplayerLobbyBinding.inflate(layoutInflater) + NetPlayManager.netPlayIsJoined() -> DialogMultiplayerLobbyBinding.inflate( + layoutInflater + ) .apply { setContentView(root) adapter = NetPlayAdapter() @@ -77,7 +77,6 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) { btnModeration.setOnClickListener { showModerationDialog() } - } else -> { @@ -140,7 +139,8 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) { inner class NetPlayAdapter : RecyclerView.Adapter() { val netPlayItems = mutableListOf() - abstract inner class NetPlayViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), + abstract inner class NetPlayViewHolder(itemView: View) : + RecyclerView.ViewHolder(itemView), View.OnClickListener { init { itemView.setOnClickListener(this) @@ -167,7 +167,9 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) { visibility = if (iconRes != 0) { setImageResource(iconRes) View.VISIBLE - } else View.GONE + } else { + View.GONE + } } } } @@ -186,14 +188,13 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) { override fun onClick(clicked: View) {} - private fun showPopupMenu(view: View) { PopupMenu(view.context, view).apply { menuInflater.inflate(R.menu.menu_netplay_member, menu) menu.findItem(R.id.action_kick).isEnabled = isModerator && - netPlayItems.name != StringSetting.WEB_USERNAME.getString() + netPlayItems.name != StringSetting.WEB_USERNAME.getString() menu.findItem(R.id.action_ban).isEnabled = isModerator && - netPlayItems.name != StringSetting.WEB_USERNAME.getString() + netPlayItems.name != StringSetting.WEB_USERNAME.getString() setOnMenuItemClickListener { item -> if (item.itemId == R.id.action_kick) { NetPlayManager.netPlayKickUser(netPlayItems.name) @@ -201,7 +202,9 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) { } else if (item.itemId == R.id.action_ban) { NetPlayManager.netPlayBanUser(netPlayItems.name) true - } else false + } else { + false + } } show() } @@ -360,12 +363,15 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) { val visibilityList: List = listOf( context.getString(R.string.multiplayer_public_visibility), - context.getString(R.string.multiplayer_unlisted_visibility), + context.getString(R.string.multiplayer_unlisted_visibility) ) binding.textTitle.text = activity.getString( - if (isCreateRoom) R.string.multiplayer_create_room - else R.string.multiplayer_join_room + if (isCreateRoom) { + R.string.multiplayer_create_room + } else { + R.string.multiplayer_join_room + } ) // setup listeners etc @@ -446,7 +452,9 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) { ) } - binding.dropdownLobbyVisibility.setText(context.getString(R.string.multiplayer_unlisted_visibility)) + binding.dropdownLobbyVisibility.setText( + context.getString(R.string.multiplayer_unlisted_visibility) + ) binding.dropdownLobbyVisibility.apply { setAdapter( @@ -501,8 +509,11 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) { binding.btnConfirm.isEnabled = false binding.btnConfirm.text = activity.getString( - if (isCreateRoom) R.string.multiplayer_creating - else R.string.multiplayer_joining + if (isCreateRoom) { + R.string.multiplayer_creating + } else { + R.string.multiplayer_joining + } ) // We don't need to worry about validation because it's already been done. @@ -546,8 +557,11 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) { Toast.makeText( YuzuApplication.appContext, - if (isCreateRoom) R.string.multiplayer_create_room_success - else R.string.multiplayer_join_room_success, + if (isCreateRoom) { + R.string.multiplayer_create_room_success + } else { + R.string.multiplayer_join_room_success + }, Toast.LENGTH_LONG ).show() @@ -619,7 +633,9 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val binding = ItemBanListBinding.inflate( - LayoutInflater.from(parent.context), parent, false + LayoutInflater.from(parent.context), + parent, + false ) return ViewHolder(binding) } @@ -654,6 +670,5 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) { notifyItemRemoved(position) } } - } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/DriverGroupAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/DriverGroupAdapter.kt index 14d69cb384..1607371cf5 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/DriverGroupAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/DriverGroupAdapter.kt @@ -13,7 +13,6 @@ import org.yuzu.yuzu_emu.databinding.ItemDriverGroupBinding import org.yuzu.yuzu_emu.fragments.DriverFetcherFragment.DriverGroup import androidx.core.view.isVisible import androidx.fragment.app.FragmentActivity -import androidx.transition.AutoTransition import androidx.transition.ChangeBounds import androidx.transition.Fade import androidx.transition.TransitionManager @@ -89,7 +88,9 @@ class DriverGroupAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DriverGroupViewHolder { val binding = ItemDriverGroupBinding.inflate( - LayoutInflater.from(parent.context), parent, false + LayoutInflater.from(parent.context), + parent, + false ) return DriverGroupViewHolder(binding) } @@ -105,4 +106,4 @@ class DriverGroupAdapter( driverGroups = newDriverGroups notifyDataSetChanged() } -} \ No newline at end of file +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/ReleaseAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/ReleaseAdapter.kt index 1dcec3c9f0..8efc5ecdea 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/ReleaseAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/ReleaseAdapter.kt @@ -71,7 +71,7 @@ class ReleaseAdapter( // truncates to 150 chars so it does not take up too much space. var bodyPreview = release.body.take(150) - bodyPreview = bodyPreview.replace("#", "").removeSurrounding(" "); + bodyPreview = bodyPreview.replace("#", "").removeSurrounding(" ") val body = bodyPreview.replace("\\r\\n", "\n").replace("\\n", "\n").replace("\n", "
") @@ -122,8 +122,11 @@ class ReleaseAdapter( binding.imageDownloadsArrow.rotation = if (isVisible) 0f else 180f binding.buttonToggleDownloads.text = - if (isVisible) activity.getString(R.string.show_downloads) - else activity.getString(R.string.hide_downloads) + if (isVisible) { + activity.getString(R.string.show_downloads) + } else { + activity.getString(R.string.hide_downloads) + } } binding.buttonToggleDownloads.setOnClickListener { @@ -139,9 +142,15 @@ class ReleaseAdapter( release.artifacts.forEach { artifact -> val button = MaterialButton(binding.root.context).apply { text = artifact.name - setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_LabelLarge) + setTextAppearance( + com.google.android.material.R.style.TextAppearance_Material3_LabelLarge + ) textAlignment = MaterialButton.TEXT_ALIGNMENT_VIEW_START - setBackgroundColor(context.getColor(com.google.android.material.R.color.m3_button_background_color_selector)) + setBackgroundColor( + context.getColor( + com.google.android.material.R.color.m3_button_background_color_selector + ) + ) setIconResource(R.drawable.ic_import) iconTint = ColorStateList.valueOf( MaterialColors.getColor( @@ -199,7 +208,9 @@ class ReleaseAdapter( input.copyTo(output) } } - ?: throw IOException(context.getString(R.string.empty_response_body)) + ?: throw IOException( + context.getString(R.string.empty_response_body) + ) } } @@ -211,7 +222,9 @@ class ReleaseAdapter( val driverData = GpuDriverHelper.getMetadataFromZip(file) val driverPath = - "${GpuDriverHelper.driverStoragePath}${FileUtil.getFilename(file.toUri())}" + "${GpuDriverHelper.driverStoragePath}${FileUtil.getFilename( + file.toUri() + )}" if (GpuDriverHelper.copyDriverToInternalStorage(file.toUri())) { driverViewModel.onDriverAdded(Pair(driverPath, driverData)) @@ -254,7 +267,9 @@ class ReleaseAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ReleaseViewHolder { val binding = ItemReleaseBinding.inflate( - LayoutInflater.from(parent.context), parent, false + LayoutInflater.from(parent.context), + parent, + false ) return ReleaseViewHolder(binding) } @@ -264,4 +279,4 @@ class ReleaseAdapter( } override fun getItemCount(): Int = releases.size -} \ No newline at end of file +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/SpacingItemDecoration.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/SpacingItemDecoration.kt index 8a8c025d8d..47afee1071 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/SpacingItemDecoration.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/SpacingItemDecoration.kt @@ -16,4 +16,4 @@ class SpacingItemDecoration(private val spacing: Int) : RecyclerView.ItemDecorat outRect.top = spacing } } -} \ No newline at end of file +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt index a558afab47..c31c534bfc 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt @@ -63,13 +63,12 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting { SHOW_SHADERS_BUILDING("show_shaders_building"), DEBUG_FLUSH_BY_LINE("flush_lines"), - USE_LRU_CACHE("use_lru_cache"),; + USE_LRU_CACHE("use_lru_cache"); external fun isRaiiEnabled(): Boolean // external fun isFrameSkippingEnabled(): Boolean external fun isFrameInterpolationEnabled(): Boolean - override fun getBoolean(needsGlobal: Boolean): Boolean = NativeConfig.getBoolean(key, needsGlobal) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt index a674857bc1..02950484ac 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt @@ -57,7 +57,7 @@ enum class IntSetting(override val key: String) : AbstractIntSetting { OFFLINE_WEB_APPLET("offline_web_applet_mode"), LOGIN_SHARE_APPLET("login_share_applet_mode"), WIFI_WEB_AUTH_APPLET("wifi_web_auth_applet_mode"), - MY_PAGE_APPLET("my_page_applet_mode"), + MY_PAGE_APPLET("my_page_applet_mode") ; override fun getInt(needsGlobal: Boolean): Int = NativeConfig.getInt(key, needsGlobal) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt index c7fb646e3a..55ddd5950c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt @@ -13,7 +13,7 @@ enum class StringSetting(override val key: String) : AbstractStringSetting { DEVICE_NAME("device_name"), WEB_TOKEN("eden_token"), - WEB_USERNAME("eden_username"), + WEB_USERNAME("eden_username") ; override fun getString(needsGlobal: Boolean): String = NativeConfig.getString(key, needsGlobal) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt index a269cab254..3124ddd480 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt @@ -21,7 +21,6 @@ import org.yuzu.yuzu_emu.features.settings.model.LongSetting import org.yuzu.yuzu_emu.features.settings.model.ShortSetting import org.yuzu.yuzu_emu.features.settings.model.StringSetting import org.yuzu.yuzu_emu.network.NetDataValidators -import org.yuzu.yuzu_emu.utils.GpuDriverHelper import org.yuzu.yuzu_emu.utils.NativeConfig /** @@ -79,7 +78,7 @@ abstract class SettingsItem( val needsRuntimeGlobal: Boolean get() = NativeLibrary.isRunning() && !setting.global && - !NativeConfig.isPerGameConfigLoaded() + !NativeConfig.isPerGameConfigLoaded() val clearable: Boolean get() = !setting.global && NativeConfig.isPerGameConfigLoaded() @@ -516,7 +515,6 @@ abstract class SettingsItem( ) ) - put( SingleChoiceSetting( IntSetting.RENDERER_VSYNC, @@ -724,7 +722,7 @@ abstract class SettingsItem( val fastmem = object : AbstractBooleanSetting { override fun getBoolean(needsGlobal: Boolean): Boolean = BooleanSetting.FASTMEM.getBoolean() && - BooleanSetting.FASTMEM_EXCLUSIVES.getBoolean() + BooleanSetting.FASTMEM_EXCLUSIVES.getBoolean() override fun setBoolean(value: Boolean) { BooleanSetting.FASTMEM.setBoolean(value) @@ -739,7 +737,7 @@ abstract class SettingsItem( override var global: Boolean get() { return BooleanSetting.FASTMEM.global && - BooleanSetting.FASTMEM_EXCLUSIVES.global + BooleanSetting.FASTMEM_EXCLUSIVES.global } set(value) { BooleanSetting.FASTMEM.global = value diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt index aea72946f7..41b307869d 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt @@ -18,7 +18,7 @@ class SingleChoiceSetting( @ArrayRes val choicesId: Int, @ArrayRes val valuesId: Int, val warnChoices: List = ArrayList(), - @StringRes val warningMessage: Int = 0, + @StringRes val warningMessage: Int = 0 ) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) { override val type = TYPE_SINGLE_CHOICE diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringInputSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringInputSetting.kt index 85076e680d..1d6de233b8 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringInputSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringInputSetting.kt @@ -6,7 +6,6 @@ package org.yuzu.yuzu_emu.features.settings.model.view -import android.text.Editable import androidx.annotation.StringRes import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt index aa17d05e34..dc9f561eca 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsDialogFragment.kt @@ -179,7 +179,13 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener override fun afterTextChanged(s: Editable?) { val isValid = validator(s.toString()) stringInputBinding.editTextLayout.isErrorEnabled = !isValid - stringInputBinding.editTextLayout.error = if (isValid) null else requireContext().getString(item.errorId) + stringInputBinding.editTextLayout.error = if (isValid) { + null + } else { + requireContext().getString( + item.errorId + ) + } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt index bac90eb4ef..3813a3e827 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt @@ -8,7 +8,6 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.content.edit import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding @@ -16,13 +15,11 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.findNavController import androidx.navigation.fragment.navArgs -import androidx.preference.PreferenceManager import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.transition.MaterialSharedAxis import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.FragmentSettingsBinding import org.yuzu.yuzu_emu.features.input.NativeInput -import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.fragments.MessageDialogFragment import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt index 31e2873b58..182dd0b9db 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -4,8 +4,6 @@ package org.yuzu.yuzu_emu.features.settings.ui import android.annotation.SuppressLint -import android.app.Activity -import android.app.AlertDialog import android.os.Build import android.widget.Toast import androidx.preference.PreferenceManager @@ -1056,7 +1054,9 @@ class SettingsFragmentPresenter( } val staticThemeColor: AbstractIntSetting = object : AbstractIntSetting { - val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) + val preferences = PreferenceManager.getDefaultSharedPreferences( + YuzuApplication.appContext + ) override fun getInt(needsGlobal: Boolean): Int = preferences.getInt(Settings.PREF_STATIC_THEME_COLOR, 0) override fun setInt(value: Int) { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt index 2879310007..0ec9984607 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later - package org.yuzu.yuzu_emu.fragments import android.content.ClipData diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddGameFolderDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddGameFolderDialogFragment.kt index cc95544d25..e2cb5f600d 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddGameFolderDialogFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddGameFolderDialogFragment.kt @@ -33,7 +33,10 @@ class AddGameFolderDialogFragment : DialogFragment() { .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> val newGameDir = GameDir(folderUriString!!, binding.deepScanSwitch.isChecked) homeViewModel.setGamesDirSelected(true) - val calledFromGameFragment = requireArguments().getBoolean("calledFromGameFragment", false) + val calledFromGameFragment = requireArguments().getBoolean( + "calledFromGameFragment", + false + ) gamesViewModel.addFolder(newGameDir, calledFromGameFragment) } .setNegativeButton(android.R.string.cancel, null) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt index 91670b207d..b8d0f2197e 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt @@ -62,14 +62,14 @@ class DriverFetcherFragment : Fragment() { val path: String = "", val sort: Int = 0, val useTagName: Boolean = false, - val sortMode: SortMode = SortMode.Default, + val sortMode: SortMode = SortMode.Default ) private val repoList: List = listOf( DriverRepo("Mr. Purple Turnip", "MrPurple666/purple-turnip", 0), DriverRepo("GameHub Adreno 8xx", "crueter/GameHub-8Elite-Drivers", 1), DriverRepo("KIMCHI Turnip", "K11MCH1/AdrenoToolsDrivers", 2, true, SortMode.PublishTime), - DriverRepo("Weab-Chan Freedreno", "Weab-chan/freedreno_turnip-CI", 3), + DriverRepo("Weab-Chan Freedreno", "Weab-chan/freedreno_turnip-CI", 3) ) private val driverMap = listOf( @@ -81,7 +81,7 @@ class DriverFetcherFragment : Fragment() { IntRange(700, 710) to "KIMCHI 25.2.0_r5", IntRange(711, 799) to "Mr. Purple T21", IntRange(800, 899) to "GameHub Adreno 8xx", - IntRange(900, Int.MAX_VALUE) to "Unsupported", + IntRange(900, Int.MAX_VALUE) to "Unsupported" ) private lateinit var driverGroupAdapter: DriverGroupAdapter @@ -124,7 +124,9 @@ class DriverFetcherFragment : Fragment() { } override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? ): View { _binding = FragmentDriverFetcherBinding.inflate(inflater) binding.badgeRecommendedDriver.text = recommendedDriver @@ -178,8 +180,12 @@ class DriverFetcherFragment : Fragment() { } } catch (e: Exception) { withContext(Dispatchers.Main) { - MaterialAlertDialogBuilder(requireActivity()).setTitle(getString(R.string.error_during_fetch)) - .setMessage("${getString(R.string.failed_to_fetch)} ${name}:\n${e.message}") + MaterialAlertDialogBuilder(requireActivity()).setTitle( + getString(R.string.error_during_fetch) + ) + .setMessage( + "${getString(R.string.failed_to_fetch)} $name:\n${e.message}" + ) .setPositiveButton(getString(R.string.ok)) { dialog, _ -> dialog.cancel() } .show() @@ -188,7 +194,9 @@ class DriverFetcherFragment : Fragment() { } val group = DriverGroup( - name, releases, sort + name, + releases, + sort ) synchronized(driverGroups) { @@ -223,7 +231,9 @@ class DriverFetcherFragment : Fragment() { binding.listDrivers.updateMargins(left = leftInsets, right = rightInsets) binding.listDrivers.updatePadding( - bottom = barInsets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab) + bottom = barInsets.bottom + resources.getDimensionPixelSize( + R.dimen.spacing_bottom_list_fab + ) ) windowInsets @@ -239,11 +249,13 @@ class DriverFetcherFragment : Fragment() { var artifacts: List = ArrayList(), var prerelease: Boolean = false, var latest: Boolean = false, - var publishTime: LocalDateTime = LocalDateTime.now(), + var publishTime: LocalDateTime = LocalDateTime.now() ) { companion object { fun fromJsonArray( - jsonString: String, useTagName: Boolean, sortMode: SortMode + jsonString: String, + useTagName: Boolean, + sortMode: SortMode ): ArrayList { val mapper = jacksonObjectMapper() @@ -310,7 +322,16 @@ class DriverFetcherFragment : Fragment() { } } - return Release(tagName, titleName, title, body, artifacts, prerelease, false, localTime) + return Release( + tagName, + titleName, + title, + body, + artifacts, + prerelease, + false, + localTime + ) } catch (e: Exception) { // TODO: handle malformed input. e.printStackTrace() @@ -324,6 +345,6 @@ class DriverFetcherFragment : Fragment() { data class DriverGroup( val name: String, val releases: ArrayList, - val sort: Int, + val sort: Int ) -} \ No newline at end of file +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt index 47b84a403a..f521343272 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt @@ -25,7 +25,6 @@ import kotlinx.coroutines.withContext import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.adapters.DriverAdapter import org.yuzu.yuzu_emu.databinding.FragmentDriverManagerBinding -import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.StringSetting import org.yuzu.yuzu_emu.model.Driver.Companion.toDriver @@ -108,7 +107,9 @@ class DriverManagerFragment : Fragment() { } binding.buttonFetch.setOnClickListener { - binding.root.findNavController().navigate(R.id.action_driverManagerFragment_to_driverFetcherFragment) + binding.root.findNavController().navigate( + R.id.action_driverManagerFragment_to_driverFetcherFragment + ) } binding.listDrivers.apply { 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 6cce31a4eb..a7e564c179 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 @@ -45,6 +45,7 @@ import androidx.drawerlayout.widget.DrawerLayout import androidx.drawerlayout.widget.DrawerLayout.DrawerListener import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope import androidx.navigation.findNavController import androidx.navigation.fragment.navArgs import androidx.window.layout.FoldingFeature @@ -52,7 +53,6 @@ import androidx.window.layout.WindowInfoTracker import androidx.window.layout.WindowLayoutInfo import com.google.android.material.color.MaterialColors import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.slider.Slider import com.google.android.material.textview.MaterialTextView import org.yuzu.yuzu_emu.HomeNavigationDirections import org.yuzu.yuzu_emu.NativeLibrary @@ -81,6 +81,13 @@ import org.yuzu.yuzu_emu.utils.NativeConfig import org.yuzu.yuzu_emu.utils.ViewUtils import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible import org.yuzu.yuzu_emu.utils.collect +import org.yuzu.yuzu_emu.utils.CustomSettingsHandler +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine import java.io.File class EmulationFragment : Fragment(), SurfaceHolder.Callback { @@ -90,24 +97,29 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { private var perfStatsUpdater: (() -> Unit)? = null private var socUpdater: (() -> Unit)? = null - private lateinit var cpuBackend: String - private lateinit var gpuDriver: String - private var _binding: FragmentEmulationBinding? = null + private val binding get() = _binding!! private val args by navArgs() - private lateinit var game: Game + private var game: Game? = null private val emulationViewModel: EmulationViewModel by activityViewModels() private val driverViewModel: DriverViewModel by activityViewModels() private var isInFoldableLayout = false + private var emulationStarted = false private lateinit var gpuModel: String private lateinit var fwVersion: String + private var intentGame: Game? = null + private var isCustomSettingsIntent = false + + private var perfStatsRunnable: Runnable? = null + private var socRunnable: Runnable? = null + override fun onAttach(context: Context) { super.onAttach(context) if (context is EmulationActivity) { @@ -125,9 +137,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { super.onCreate(savedInstanceState) updateOrientation() - val intentUri: Uri? = requireActivity().intent.data - var intentGame: Game? = null - if (intentUri != null) { + val intent = requireActivity().intent + val intentUri: Uri? = intent.data + intentGame = null + isCustomSettingsIntent = false + + if (intent.action == CustomSettingsHandler.CUSTOM_CONFIG_ACTION) { + handleEmuReadyIntent(intent) + return + } else if (intentUri != null) { intentGame = if (Game.extensions.contains(FileUtil.getExtension(intentUri))) { GameHelper.getGame(requireActivity().intent.data!!, false) } else { @@ -135,38 +153,308 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } } + finishGameSetup() + } + + /** + * Complete the game setup process (extracted for async custom settings handling) + */ + private fun finishGameSetup() { try { - game = if (args.game != null) { - args.game!! - } else { - intentGame!! + val gameToUse = args.game ?: intentGame + + if (gameToUse == null) { + Log.error("[EmulationFragment] No game found in arguments or intent") + Toast.makeText( + requireContext(), + R.string.no_game_present, + Toast.LENGTH_SHORT + ).show() + requireActivity().finish() + return } - } catch (e: NullPointerException) { + + game = gameToUse + + } catch (e: Exception) { + Log.error("[EmulationFragment] Error during game setup: ${e.message}") Toast.makeText( requireContext(), - R.string.no_game_present, + "Setup error: ${e.message?.take(30) ?: "Unknown"}", Toast.LENGTH_SHORT ).show() requireActivity().finish() return } - // Always load custom settings when launching a game from an intent - if (args.custom || intentGame != null) { - SettingsFile.loadCustomConfig(game) - NativeConfig.unloadPerGameConfig() - } else { - NativeConfig.reloadGlobalConfig() + try { + if (isCustomSettingsIntent) { + Log.info("[EmulationFragment] Using custom settings from intent") + } else if (intentGame != null && game != null) { + val customConfigFile = SettingsFile.getCustomSettingsFile(game!!) + if (customConfigFile.exists()) { + Log.info( + "[EmulationFragment] Found existing custom settings for ${game!!.title}, loading them" + ) + SettingsFile.loadCustomConfig(game!!) + } else { + Log.info( + "[EmulationFragment] No custom settings found for ${game!!.title}, using global settings" + ) + NativeConfig.reloadGlobalConfig() + } + } else { + val isCustomFromArgs = if (game != null && game == args.game) { + try { + args.custom + } catch (e: Exception) { + false + } + } else { + false + } + + if (isCustomFromArgs && game != null) { + SettingsFile.loadCustomConfig(game!!) + Log.info("[EmulationFragment] Loading custom settings for ${game!!.title}") + } else { + Log.info("[EmulationFragment] Using global settings") + NativeConfig.reloadGlobalConfig() + } + } + } catch (e: Exception) { + Log.error("[EmulationFragment] Error loading configuration: ${e.message}") + Log.info("[EmulationFragment] Falling back to global settings") + try { + NativeConfig.reloadGlobalConfig() + } catch (fallbackException: Exception) { + Log.error( + "[EmulationFragment] Critical error: could not load global config: ${fallbackException.message}" + ) + throw fallbackException + } } - // Install the selected driver asynchronously as the game starts - driverViewModel.onLaunchGame() - - // 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 } + + } + + /** + * Handle EmuReady intent for launching games with or without custom settings + */ + private fun handleEmuReadyIntent(intent: Intent) { + val titleId = intent.getStringExtra(CustomSettingsHandler.EXTRA_TITLE_ID) + val customSettings = intent.getStringExtra(CustomSettingsHandler.EXTRA_CUSTOM_SETTINGS) + + if (titleId != null) { + Log.info("[EmulationFragment] Received EmuReady intent for title: $titleId") + + lifecycleScope.launch { + try { + Toast.makeText( + requireContext(), + getString(R.string.searching_for_game), + Toast.LENGTH_SHORT + ).show() + val foundGame = CustomSettingsHandler.findGameByTitleId( + titleId, + requireContext() + ) + if (foundGame == null) { + Log.error("[EmulationFragment] Game not found for title ID: $titleId") + Toast.makeText( + requireContext(), + getString(R.string.game_not_found_for_title_id, titleId), + Toast.LENGTH_LONG + ).show() + requireActivity().finish() + return@launch + } + + val shouldLaunch = showLaunchConfirmationDialog( + foundGame.title, + customSettings != null + ) + if (!shouldLaunch) { + Log.info("[EmulationFragment] User cancelled EmuReady launch") + requireActivity().finish() + return@launch + } + + if (customSettings != null) { + intentGame = CustomSettingsHandler.applyCustomSettingsWithDriverCheck( + titleId, + customSettings, + requireContext(), + requireActivity(), + driverViewModel + ) + + if (intentGame == null) { + Log.error( + "[EmulationFragment] Custom settings processing failed for title ID: $titleId" + ) + Toast.makeText( + requireContext(), + getString(R.string.custom_settings_failed_title), + Toast.LENGTH_SHORT + ).show() + + val launchWithDefault = askUserToLaunchWithDefaultSettings( + foundGame.title, + "This could be due to:\n• User cancelled configuration overwrite\n• Driver installation failed\n• Missing required drivers" + ) + + if (launchWithDefault) { + Log.info( + "[EmulationFragment] User chose to launch with default settings" + ) + Toast.makeText( + requireContext(), + getString(R.string.launch_with_default_settings), + Toast.LENGTH_SHORT + ).show() + intentGame = foundGame + isCustomSettingsIntent = false + } else { + Log.info( + "[EmulationFragment] User cancelled launch after custom settings failure" + ) + Toast.makeText( + requireContext(), + getString(R.string.launch_cancelled), + Toast.LENGTH_SHORT + ).show() + requireActivity().finish() + return@launch + } + } else { + isCustomSettingsIntent = true + } + } else { + Log.info("[EmulationFragment] Launching game with default settings") + + val customConfigFile = SettingsFile.getCustomSettingsFile(foundGame) + if (customConfigFile.exists()) { + Log.info("[EmulationFragment] Found existing custom settings for ${foundGame.title}, loading them") + SettingsFile.loadCustomConfig(foundGame) + } else { + Log.info("[EmulationFragment] No custom settings found for ${foundGame.title}, using global settings") + } + + Toast.makeText( + requireContext(), + getString(R.string.launching_game, foundGame.title), + Toast.LENGTH_SHORT + ).show() + intentGame = foundGame + isCustomSettingsIntent = false + } + + if (intentGame != null) { + withContext(Dispatchers.Main) { + try { + finishGameSetup() + Log.info("[EmulationFragment] Game setup complete for intent launch") + + if (_binding != null) { + completeViewSetup() + + val driverReady = driverViewModel.isInteractionAllowed.value + if (driverReady && !NativeLibrary.isRunning() && !NativeLibrary.isPaused()) { + Log.info("[EmulationFragment] Starting emulation after async intent setup - driver ready") + startEmulation() + } + } + } catch (e: Exception) { + Log.error("[EmulationFragment] Error in finishGameSetup: ${e.message}") + requireActivity().finish() + return@withContext + } + } + } else { + Log.error("[EmulationFragment] No valid game found after processing intent") + Toast.makeText( + requireContext(), + getString(R.string.failed_to_initialize_game), + Toast.LENGTH_SHORT + ).show() + requireActivity().finish() + } + } catch (e: Exception) { + Log.error("[EmulationFragment] Error processing EmuReady intent: ${e.message}") + Toast.makeText( + requireContext(), + "Error: ${e.message?.take(50) ?: "Unknown error"}", + Toast.LENGTH_LONG + ).show() + requireActivity().finish() + } + } + } else { + Log.error("[EmulationFragment] EmuReady intent missing title_id") + Toast.makeText( + requireContext(), + "Invalid request: missing title ID", + Toast.LENGTH_SHORT + ).show() + requireActivity().finish() + } + } + + /** + * Show confirmation dialog for EmuReady game launches + */ + private suspend fun showLaunchConfirmationDialog(gameTitle: String, hasCustomSettings: Boolean): Boolean { + return suspendCoroutine { continuation -> + requireActivity().runOnUiThread { + val message = if (hasCustomSettings) { + getString( + R.string.custom_intent_launch_message_with_settings, + gameTitle + ) + } else { + getString(R.string.custom_intent_launch_message, gameTitle) + } + + MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.custom_intent_launch_title)) + .setMessage(message) + .setPositiveButton(getString(R.string.launch)) { _, _ -> + continuation.resume(true) + } + .setNegativeButton(getString(R.string.cancel)) { _, _ -> + continuation.resume(false) + } + .setCancelable(false) + .show() + } + } + } + + /** + * Ask user if they want to launch with default settings when custom settings fail + */ + private suspend fun askUserToLaunchWithDefaultSettings(gameTitle: String, errorMessage: String): Boolean { + return suspendCoroutine { continuation -> + requireActivity().runOnUiThread { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.custom_settings_failed_title)) + .setMessage( + getString(R.string.custom_settings_failed_message, gameTitle, errorMessage) + ) + .setPositiveButton(getString(R.string.launch_with_default_settings)) { _, _ -> + continuation.resume(true) + } + .setNegativeButton(getString(R.string.cancel)) { _, _ -> + continuation.resume(false) + } + .setCancelable(false) + .show() + } + } } /** @@ -187,6 +475,20 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { return } + if (game == null) { + Log.warning("[EmulationFragment] Game not yet initialized in onViewCreated - will be set up by async intent handler") + return + } + + completeViewSetup() + } + + private fun completeViewSetup() { + if (_binding == null || game == null) { + return + } + Log.info("[EmulationFragment] Starting view setup for game: ${game?.title}") + gpuModel = GpuDriverHelper.getGpuModel().toString() fwVersion = NativeLibrary.firmwareVersion() @@ -223,10 +525,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } }) binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) - binding.inGameMenu.getHeaderView(0).apply { - val titleView = findViewById(R.id.text_game_title) - titleView.text = game.title - } + + updateGameTitle() binding.inGameMenu.menu.findItem(R.id.menu_lock_drawer).apply { val lockMode = IntSetting.LOCK_DRAWER.getInt() @@ -293,13 +593,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { true } - R.id.menu_multiplayer -> { emulationActivity?.displayMultiplayerDialog() true } - R.id.menu_controls -> { val action = HomeNavigationDirections.actionGlobalSettingsActivity( null, @@ -368,8 +666,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } ) - GameIconUtils.loadGameIcon(game, binding.loadingImage) - binding.loadingTitle.text = game.title + GameIconUtils.loadGameIcon(game!!, binding.loadingImage) + binding.loadingTitle.text = game!!.title binding.loadingTitle.isSelected = true binding.loadingText.isSelected = true @@ -408,7 +706,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { emulationState.updateSurface() - // Setup overlays updateShowStatsOverlay() updateSocOverlay() @@ -418,7 +715,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { val cpuBackendLabel = findViewById(R.id.cpu_backend) val vendorLabel = findViewById(R.id.gpu_vendor) - titleView.text = game.title + titleView.text = game?.title ?: "" cpuBackendLabel.text = NativeLibrary.getCpuBackend() vendorLabel.text = NativeLibrary.getGpuDriver() } @@ -456,16 +753,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { ViewUtils.showView(binding.loadingIndicator) } } - emulationViewModel.emulationStopped.collect(viewLifecycleOwner) { - if (it && emulationViewModel.programChanged.value != -1) { - if (perfStatsUpdater != null) { - perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!) - } - if (socUpdater != null) { - socUpdateHandler.removeCallbacks(socUpdater!!) + emulationViewModel.emulationStopped.collect(viewLifecycleOwner) { stopped -> + if (stopped && emulationViewModel.programChanged.value != -1) { + perfStatsRunnable?.let { runnable -> + perfStatsUpdateHandler.removeCallbacks( + runnable + ) } - + socRunnable?.let { runnable -> socUpdateHandler.removeCallbacks(runnable) } emulationState.changeProgram(emulationViewModel.programChanged.value) emulationViewModel.setProgramChanged(-1) emulationViewModel.setEmulationStopped(false) @@ -473,7 +769,19 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } driverViewModel.isInteractionAllowed.collect(viewLifecycleOwner) { - if (it) startEmulation() + Log.info("[EmulationFragment] Driver interaction allowed: $it") + if (it && !NativeLibrary.isRunning() && !NativeLibrary.isPaused()) { + startEmulation() + } + } + + driverViewModel.onLaunchGame() + + val currentDriverState = driverViewModel.isInteractionAllowed.value + Log.info("[EmulationFragment] Checking initial driver state after onLaunchGame: $currentDriverState") + if (currentDriverState && !NativeLibrary.isRunning() && !NativeLibrary.isPaused()) { + Log.info("[EmulationFragment] Starting emulation immediately - driver already ready") + startEmulation() } } @@ -485,6 +793,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { updateScreenLayout() + Log.info("[EmulationFragment] Calling emulationState.run() - surface will start emulation when available") emulationState.run(emulationActivity!!.isActivityRecreated, programIndex) } } @@ -518,6 +827,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } } + private fun updateGameTitle() { + game?.let { + binding.inGameMenu.getHeaderView(0).apply { + val titleView = findViewById(R.id.text_game_title) + titleView.text = it.title + } + } + } + override fun onPause() { if (emulationState.isRunning && emulationActivity?.isInPictureInPictureMode != true) { emulationState.pause() @@ -634,7 +952,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { val batteryTemp = getBatteryTemperature() when (IntSetting.BAT_TEMPERATURE_UNIT.getInt(needsGlobal)) { 0 -> sb.append(String.format("%.1f°C", batteryTemp)) - 1 -> sb.append(String.format("%.1f°F", celsiusToFahrenheit(batteryTemp))) + 1 -> sb.append( + String.format( + "%.1f°F", + celsiusToFahrenheit(batteryTemp) + ) + ) } } @@ -643,8 +966,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { val battery: BatteryManager = requireContext().getSystemService(Context.BATTERY_SERVICE) as BatteryManager - val batteryIntent = requireContext().registerReceiver(null, - IntentFilter(Intent.ACTION_BATTERY_CHANGED)) + val batteryIntent = requireContext().registerReceiver( + null, + IntentFilter(Intent.ACTION_BATTERY_CHANGED) + ) val capacity = battery.getIntProperty(BATTERY_PROPERTY_CAPACITY) val nowUAmps = battery.getIntProperty(BATTERY_PROPERTY_CURRENT_NOW) @@ -653,7 +978,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { val status = batteryIntent?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || - status == BatteryManager.BATTERY_STATUS_FULL + status == BatteryManager.BATTERY_STATUS_FULL if (isCharging) { sb.append(" ${getString(R.string.charging)}") @@ -671,20 +996,21 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } if (BooleanSetting.PERF_OVERLAY_BACKGROUND.getBoolean(needsGlobal)) { - binding.showStatsOverlayText.setBackgroundResource(R.color.yuzu_transparent_black) + binding.showStatsOverlayText.setBackgroundResource( + R.color.yuzu_transparent_black + ) } else { binding.showStatsOverlayText.setBackgroundResource(0) } binding.showStatsOverlayText.text = sb.toString() } - perfStatsUpdateHandler.postDelayed(perfStatsUpdater!!, 800) + perfStatsUpdateHandler.postDelayed(perfStatsRunnable!!, 800) } - perfStatsUpdateHandler.post(perfStatsUpdater!!) + perfStatsRunnable = Runnable { perfStatsUpdater?.invoke() } + perfStatsUpdateHandler.post(perfStatsRunnable!!) } else { - if (perfStatsUpdater != null) { - perfStatsUpdateHandler.removeCallbacks(perfStatsUpdater!!) - } + perfStatsRunnable?.let { perfStatsUpdateHandler.removeCallbacks(it) } } } @@ -767,47 +1093,62 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { ) { sb.setLength(0) - if (BooleanSetting.SHOW_DEVICE_MODEL.getBoolean(NativeConfig.isPerGameConfigLoaded())) { + if (BooleanSetting.SHOW_DEVICE_MODEL.getBoolean( + NativeConfig.isPerGameConfigLoaded() + ) + ) { sb.append(Build.MODEL) } - if (BooleanSetting.SHOW_GPU_MODEL.getBoolean(NativeConfig.isPerGameConfigLoaded())) { + if (BooleanSetting.SHOW_GPU_MODEL.getBoolean( + NativeConfig.isPerGameConfigLoaded() + ) + ) { if (sb.isNotEmpty()) sb.append(" | ") sb.append(gpuModel) } if (Build.VERSION.SDK_INT >= 31) { - if (BooleanSetting.SHOW_SOC_MODEL.getBoolean(NativeConfig.isPerGameConfigLoaded())) { + if (BooleanSetting.SHOW_SOC_MODEL.getBoolean( + NativeConfig.isPerGameConfigLoaded() + ) + ) { if (sb.isNotEmpty()) sb.append(" | ") sb.append(Build.SOC_MODEL) } } - if (BooleanSetting.SHOW_FW_VERSION.getBoolean(NativeConfig.isPerGameConfigLoaded())) { + if (BooleanSetting.SHOW_FW_VERSION.getBoolean( + NativeConfig.isPerGameConfigLoaded() + ) + ) { if (sb.isNotEmpty()) sb.append(" | ") sb.append(fwVersion) } binding.showSocOverlayText.text = sb.toString() - if (BooleanSetting.SOC_OVERLAY_BACKGROUND.getBoolean(NativeConfig.isPerGameConfigLoaded())) { - binding.showSocOverlayText.setBackgroundResource(R.color.yuzu_transparent_black) + if (BooleanSetting.SOC_OVERLAY_BACKGROUND.getBoolean( + NativeConfig.isPerGameConfigLoaded() + ) + ) { + binding.showSocOverlayText.setBackgroundResource( + R.color.yuzu_transparent_black + ) } else { binding.showSocOverlayText.setBackgroundResource(0) } } - socUpdateHandler.postDelayed(socUpdater!!, 1000) + socUpdateHandler.postDelayed(socRunnable!!, 1000) } - socUpdateHandler.post(socUpdater!!) + socRunnable = Runnable { socUpdater?.invoke() } + socUpdateHandler.post(socRunnable!!) } else { - if (socUpdater != null) { - socUpdateHandler.removeCallbacks(socUpdater!!) - } + socRunnable?.let { socUpdateHandler.removeCallbacks(it) } } } - @SuppressLint("SourceLockedOrientationActivity") private fun updateOrientation() { emulationActivity?.let { @@ -919,11 +1260,19 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { Log.debug("[EmulationFragment] Surface changed. Resolution: " + width + "x" + height) - emulationState.newSurface(holder.surface) + if (!emulationStarted) { + Log.info("[EmulationFragment] Starting emulation") + emulationStarted = true + emulationState.newSurface(holder.surface) + } else { + Log.debug("[EmulationFragment] Emulation already started, updating surface") + emulationState.newSurface(holder.surface) + } } override fun surfaceDestroyed(holder: SurfaceHolder) { emulationState.clearSurface() + emulationStarted = false } private fun showOverlayOptions() { @@ -1096,22 +1445,18 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { inputScaleSlider.apply { valueTo = 150F value = IntSetting.OVERLAY_SCALE.getInt().toFloat() - addOnChangeListener( - Slider.OnChangeListener { _, value, _ -> - inputScaleValue.text = "${value.toInt()}%" - setControlScale(value.toInt()) - } - ) + addOnChangeListener { _, value, _ -> + inputScaleValue.text = "${value.toInt()}%" + setControlScale(value.toInt()) + } } inputOpacitySlider.apply { valueTo = 100F value = IntSetting.OVERLAY_OPACITY.getInt().toFloat() - addOnChangeListener( - Slider.OnChangeListener { _, value, _ -> - inputOpacityValue.text = "${value.toInt()}%" - setControlOpacity(value.toInt()) - } - ) + addOnChangeListener { _, value, _ -> + inputOpacityValue.text = "${value.toInt()}%" + setControlOpacity(value.toInt()) + } } inputScaleValue.text = "${inputScaleSlider.value.toInt()}%" inputOpacityValue.text = "${inputOpacitySlider.value.toInt()}%" @@ -1147,7 +1492,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { val cutInsets: Insets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) var left = 0 var right = 0 - if (ViewCompat.getLayoutDirection(v) == ViewCompat.LAYOUT_DIRECTION_LTR) { + if (v.layoutDirection == View.LAYOUT_DIRECTION_LTR) { left = cutInsets.left } else { right = cutInsets.right @@ -1168,7 +1513,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { lateinit var emulationThread: Thread init { - // Starting state is stopped. state = State.STOPPED } @@ -1176,7 +1520,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { val isStopped: Boolean get() = state == State.STOPPED - // Getters for the current state @get:Synchronized val isPaused: Boolean get() = state == State.PAUSED @@ -1196,7 +1539,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } } - // State changing methods @Synchronized fun pause() { if (state != State.PAUSED) { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt index 5fed99e0b0..5763f3120f 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt @@ -27,7 +27,6 @@ import androidx.navigation.findNavController import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.GridLayoutManager import com.google.android.material.transition.MaterialSharedAxis -import org.yuzu.yuzu_emu.BuildConfig import org.yuzu.yuzu_emu.HomeNavigationDirections import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R @@ -35,15 +34,14 @@ import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.adapters.HomeSettingAdapter import org.yuzu.yuzu_emu.databinding.FragmentHomeSettingsBinding import org.yuzu.yuzu_emu.features.DocumentProvider +import org.yuzu.yuzu_emu.features.fetcher.SpacingItemDecoration import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.model.DriverViewModel import org.yuzu.yuzu_emu.model.HomeSetting import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.ui.main.MainActivity import org.yuzu.yuzu_emu.utils.FileUtil -import org.yuzu.yuzu_emu.utils.GpuDriverHelper import org.yuzu.yuzu_emu.utils.Log -import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins class HomeSettingsFragment : Fragment() { private var _binding: FragmentHomeSettingsBinding? = null @@ -112,7 +110,7 @@ class HomeSettingsFragment : Fragment() { .actionHomeSettingsFragmentToDriverManagerFragment(null) binding.root.findNavController().navigate(action) }, - {true}, + { true }, R.string.custom_driver_not_supported, R.string.custom_driver_not_supported_description, driverViewModel.selectedDriverTitle @@ -125,7 +123,7 @@ class HomeSettingsFragment : Fragment() { R.drawable.ic_two_users, { mainActivity.displayMultiplayerDialog() - }, + } ) ) add( @@ -254,6 +252,8 @@ class HomeSettingsFragment : Fragment() { viewLifecycleOwner, optionsList ) + val spacing = resources.getDimensionPixelSize(R.dimen.spacing_small) + addItemDecoration(SpacingItemDecoration(spacing)) } setInsets() @@ -403,7 +403,7 @@ class HomeSettingsFragment : Fragment() { val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) binding.scrollViewSettings.updatePadding( - top = barInsets.top, + top = barInsets.top ) binding.homeSettingsList.updatePadding( diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt index 33146c82ca..c02411d1bb 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt @@ -134,10 +134,10 @@ class InstallableFragment : Fragment() { install = { mainActivity.getFirmware.launch(arrayOf("application/zip")) } ), Installable( - R.string.uninstall_firmware, - R.string.uninstall_firmware_description, - install = { mainActivity.uninstallFirmware() } - ), + R.string.uninstall_firmware, + R.string.uninstall_firmware_description, + install = { mainActivity.uninstallFirmware() } + ), Installable( R.string.install_prod_keys, R.string.install_prod_keys_description, diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LicensesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LicensesFragment.kt index 52d9ef43d4..aa18aa2482 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LicensesFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LicensesFragment.kt @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later - package org.yuzu.yuzu_emu.fragments import android.os.Bundle 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 61797f75f5..b7c75c127f 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 @@ -78,7 +78,6 @@ class SetupFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { mainActivity = requireActivity() as MainActivity - requireActivity().onBackPressedDispatcher.addCallback( viewLifecycleOwner, object : OnBackPressedCallback(true) { @@ -129,7 +128,7 @@ class SetupFragment : Fragment() { 0, { if (NotificationManagerCompat.from(requireContext()) - .areNotificationsEnabled() + .areNotificationsEnabled() ) { StepState.COMPLETE } else { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/layout/MidScreenSwipeRefreshLayout.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/layout/MidScreenSwipeRefreshLayout.kt index 649bea9d54..35d027567e 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/layout/MidScreenSwipeRefreshLayout.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/layout/MidScreenSwipeRefreshLayout.kt @@ -22,7 +22,11 @@ class MidScreenSwipeRefreshLayout @JvmOverloads constructor( MotionEvent.ACTION_DOWN -> { startX = ev.x val width = width - val center_fraction = resources.getFraction(R.fraction.carousel_midscreenswipe_width_fraction, 1, 1).coerceIn(0f, 1f) + val center_fraction = resources.getFraction( + R.fraction.carousel_midscreenswipe_width_fraction, + 1, + 1 + ).coerceIn(0f, 1f) val leftBound = ((1 - center_fraction) / 2) * width val rightBound = leftBound + (width * center_fraction) allowRefresh = startX >= leftBound && startX <= rightBound @@ -30,4 +34,4 @@ class MidScreenSwipeRefreshLayout @JvmOverloads constructor( } return if (allowRefresh) super.onInterceptTouchEvent(ev) else false } -} \ No newline at end of file +} 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 66f012d1af..72ce006a7e 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 @@ -141,7 +141,7 @@ class GamesViewModel : ViewModel() { } } - fun addFolder(gameDir: GameDir, savedFromGameFragment: Boolean) = + fun addFolder(gameDir: GameDir, savedFromGameFragment: Boolean) = viewModelScope.launch { withContext(Dispatchers.IO) { NativeConfig.addGameDir(gameDir) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/network/NetDataValidators.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/network/NetDataValidators.kt index b3edf35d8e..c2ad475c95 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/network/NetDataValidators.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/network/NetDataValidators.kt @@ -27,7 +27,7 @@ object NetDataValidators { fun roomVisibility(s: String, context: Context): Boolean { if (s != context.getString(R.string.multiplayer_public_visibility)) { - return true; + return true } return token() @@ -53,4 +53,4 @@ object NetDataValidators { fun port(s: String): Boolean { return s.toIntOrNull() in 1..65535 } -} \ No newline at end of file +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/network/NetPlayManager.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/network/NetPlayManager.kt index 478dea6bdd..1e8e9f97d0 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/network/NetPlayManager.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/network/NetPlayManager.kt @@ -70,7 +70,6 @@ object NetPlayManager { val gameName: String ) - private var messageListener: ((Int, String) -> Unit)? = null private var adapterRefreshListener: ((Int, String) -> Unit)? = null @@ -199,7 +198,6 @@ object NetPlayManager { } } - Handler(Looper.getMainLooper()).post { if (!isChatOpen) { // TODO(alekpop, crueter): Improve this, potentially a drawer at the top? @@ -207,7 +205,6 @@ object NetPlayManager { } } - messageListener?.invoke(type, msg) adapterRefreshListener?.invoke(type, msg) } @@ -218,19 +215,29 @@ object NetPlayManager { NetPlayStatus.LOST_CONNECTION -> context.getString(R.string.multiplayer_lost_connection) NetPlayStatus.NAME_COLLISION -> context.getString(R.string.multiplayer_name_collision) NetPlayStatus.MAC_COLLISION -> context.getString(R.string.multiplayer_mac_collision) - NetPlayStatus.CONSOLE_ID_COLLISION -> context.getString(R.string.multiplayer_console_id_collision) + NetPlayStatus.CONSOLE_ID_COLLISION -> context.getString( + R.string.multiplayer_console_id_collision + ) NetPlayStatus.WRONG_VERSION -> context.getString(R.string.multiplayer_wrong_version) NetPlayStatus.WRONG_PASSWORD -> context.getString(R.string.multiplayer_wrong_password) - NetPlayStatus.COULD_NOT_CONNECT -> context.getString(R.string.multiplayer_could_not_connect) + NetPlayStatus.COULD_NOT_CONNECT -> context.getString( + R.string.multiplayer_could_not_connect + ) NetPlayStatus.ROOM_IS_FULL -> context.getString(R.string.multiplayer_room_is_full) NetPlayStatus.HOST_BANNED -> context.getString(R.string.multiplayer_host_banned) - NetPlayStatus.PERMISSION_DENIED -> context.getString(R.string.multiplayer_permission_denied) + NetPlayStatus.PERMISSION_DENIED -> context.getString( + R.string.multiplayer_permission_denied + ) NetPlayStatus.NO_SUCH_USER -> context.getString(R.string.multiplayer_no_such_user) NetPlayStatus.ALREADY_IN_ROOM -> context.getString(R.string.multiplayer_already_in_room) - NetPlayStatus.CREATE_ROOM_ERROR -> context.getString(R.string.multiplayer_create_room_error) + NetPlayStatus.CREATE_ROOM_ERROR -> context.getString( + R.string.multiplayer_create_room_error + ) NetPlayStatus.HOST_KICKED -> context.getString(R.string.multiplayer_host_kicked) NetPlayStatus.UNKNOWN_ERROR -> context.getString(R.string.multiplayer_unknown_error) - NetPlayStatus.ROOM_UNINITIALIZED -> context.getString(R.string.multiplayer_room_uninitialized) + NetPlayStatus.ROOM_UNINITIALIZED -> context.getString( + R.string.multiplayer_room_uninitialized + ) NetPlayStatus.ROOM_IDLE -> context.getString(R.string.multiplayer_room_idle) NetPlayStatus.ROOM_JOINING -> context.getString(R.string.multiplayer_room_joining) NetPlayStatus.ROOM_JOINED -> context.getString(R.string.multiplayer_room_joined) @@ -247,7 +254,9 @@ object NetPlayManager { msg ) - NetPlayStatus.ADDRESS_UNBANNED -> context.getString(R.string.multiplayer_address_unbanned) + NetPlayStatus.ADDRESS_UNBANNED -> context.getString( + R.string.multiplayer_address_unbanned + ) NetPlayStatus.CHAT_MESSAGE -> msg else -> "" } 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 43b9085f50..db1f808017 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 @@ -7,20 +7,13 @@ import android.content.Context import android.content.Intent import android.content.res.Configuration import android.os.Bundle -import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.view.ViewTreeObserver import android.view.inputmethod.InputMethodManager -import android.widget.ImageButton import android.widget.PopupMenu -import android.widget.TextView import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb -import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding @@ -111,7 +104,7 @@ class GamesFragment : Fragment() { } gameAdapter = GameAdapter( - requireActivity() as AppCompatActivity, + requireActivity() as AppCompatActivity ) applyGridGamesBinding() @@ -238,7 +231,9 @@ class GamesFragment : Fragment() { override fun onResume() { super.onResume() if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) { - (binding.gridGames as? CarouselRecyclerView)?.restoreScrollState(gamesViewModel.lastScrollPosition) + (binding.gridGames as? CarouselRecyclerView)?.restoreScrollState( + gamesViewModel.lastScrollPosition + ) } } @@ -389,7 +384,9 @@ class GamesFragment : Fragment() { val searchTerm = binding.searchText.text.toString().lowercase(Locale.getDefault()) if (searchTerm.isEmpty()) { - ((binding.gridGames as? RecyclerView)?.adapter as? GameAdapter)?.submitList(filteredList) + ((binding.gridGames as? RecyclerView)?.adapter as? GameAdapter)?.submitList( + filteredList + ) gamesViewModel.setFilteredGames(filteredList) return } @@ -464,7 +461,9 @@ class GamesFragment : Fragment() { // Always set margin as original + insets mlpHeader.leftMargin = (originalHeaderLeftMargin ?: 0) + leftInset mlpHeader.rightMargin = (originalHeaderRightMargin ?: 0) + rightInset - mlpHeader.topMargin = (originalHeaderTopMargin ?: 0) + topInset + resources.getDimensionPixelSize(R.dimen.spacing_med) + mlpHeader.topMargin = (originalHeaderTopMargin ?: 0) + topInset + resources.getDimensionPixelSize( + R.dimen.spacing_med + ) binding.header.layoutParams = mlpHeader binding.noticeText.updatePadding(bottom = spacingNavigation) 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 6d9a2002f5..fffaa1e3ba 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 @@ -6,8 +6,6 @@ package org.yuzu.yuzu_emu.ui.main import android.content.Intent import android.net.Uri import android.os.Bundle -import android.os.ParcelFileDescriptor -import android.provider.OpenableColumns import android.view.View import android.view.ViewGroup.MarginLayoutParams import android.view.WindowManager @@ -49,7 +47,6 @@ import java.io.BufferedOutputStream import java.util.zip.ZipEntry import java.util.zip.ZipInputStream import androidx.core.content.edit -import androidx.core.net.toFile class MainActivity : AppCompatActivity(), ThemeProvider { private lateinit var binding: ActivityMainBinding @@ -69,7 +66,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider { private var checkedFirmware = false private val requestBluetoothPermissionsLauncher = - registerForActivityResult(androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions()) { permissions -> + registerForActivityResult( + androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions() + ) { permissions -> val granted = permissions.entries.all { it.value } if (granted) { // Permissions were granted. @@ -111,10 +110,8 @@ class MainActivity : AppCompatActivity(), ThemeProvider { binding = ActivityMainBinding.inflate(layoutInflater) - setContentView(binding.root) - checkAndRequestBluetoothPermissions() if (savedInstanceState != null) { @@ -151,16 +148,20 @@ class MainActivity : AppCompatActivity(), ThemeProvider { binding.statusBarShade.setBackgroundColor( ThemeHelper.getColorWithOpacity( MaterialColors.getColor( - binding.root, com.google.android.material.R.attr.colorSurface - ), ThemeHelper.SYSTEM_BAR_ALPHA + binding.root, + com.google.android.material.R.attr.colorSurface + ), + ThemeHelper.SYSTEM_BAR_ALPHA ) ) if (InsetsHelper.getSystemGestureType(applicationContext) != InsetsHelper.GESTURE_NAVIGATION) { binding.navigationBarShade.setBackgroundColor( ThemeHelper.getColorWithOpacity( MaterialColors.getColor( - binding.root, com.google.android.material.R.attr.colorSurface - ), ThemeHelper.SYSTEM_BAR_ALPHA + binding.root, + com.google.android.material.R.attr.colorSurface + ), + ThemeHelper.SYSTEM_BAR_ALPHA ) ) } @@ -171,7 +172,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider { homeViewModel.statusBarShadeVisible.collect(this) { showStatusBarShade(it) } homeViewModel.contentToInstall.collect( - this, resetState = { homeViewModel.setContentToInstall(null) }) { + this, + resetState = { homeViewModel.setContentToInstall(null) } + ) { if (it != null) { installContent(it) } @@ -181,7 +184,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider { } homeViewModel.checkFirmware.collect( - this, resetState = { homeViewModel.setCheckFirmware(false) }) { + this, + resetState = { homeViewModel.setCheckFirmware(false) } + ) { if (it) checkFirmware() } @@ -204,7 +209,8 @@ class MainActivity : AppCompatActivity(), ThemeProvider { PreferenceManager.getDefaultSharedPreferences(applicationContext).edit() { putBoolean(Settings.PREF_SHOULD_SHOW_PRE_ALPHA_WARNING, false) } - }).show(supportFragmentManager, MessageDialogFragment.TAG) + } + ).show(supportFragmentManager, MessageDialogFragment.TAG) } } @@ -225,7 +231,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { private fun checkFirmware() { val resultCode: Int = NativeLibrary.verifyFirmware() - if (resultCode == 0) return; + if (resultCode == 0) return val resultString: String = resources.getStringArray(R.array.verifyFirmwareResults)[resultCode] @@ -313,14 +319,17 @@ class MainActivity : AppCompatActivity(), ThemeProvider { fun processGamesDir(result: Uri, calledFromGameFragment: Boolean = false) { contentResolver.takePersistableUriPermission( - result, Intent.FLAG_GRANT_READ_URI_PERMISSION + result, + Intent.FLAG_GRANT_READ_URI_PERMISSION ) val uriString = result.toString() val folder = gamesViewModel.folders.value.firstOrNull { it.uriString == uriString } if (folder != null) { Toast.makeText( - applicationContext, R.string.folder_already_added, Toast.LENGTH_SHORT + applicationContext, + R.string.folder_already_added, + Toast.LENGTH_SHORT ).show() return } @@ -343,16 +352,19 @@ class MainActivity : AppCompatActivity(), ThemeProvider { fun processKey(result: Uri, extension: String = "keys") { contentResolver.takePersistableUriPermission( - result, Intent.FLAG_GRANT_READ_URI_PERMISSION + result, + Intent.FLAG_GRANT_READ_URI_PERMISSION ) - val resultCode: Int = NativeLibrary.installKeys(result.toString(), extension); + val resultCode: Int = NativeLibrary.installKeys(result.toString(), extension) if (resultCode == 0) { // TODO(crueter): It may be worth it to switch some of these Toasts to snackbars, // since most of it is foreground-only anyways. Toast.makeText( - applicationContext, R.string.keys_install_success, Toast.LENGTH_SHORT + applicationContext, + R.string.keys_install_success, + Toast.LENGTH_SHORT ).show() gamesViewModel.reloadGames(true) @@ -384,12 +396,15 @@ class MainActivity : AppCompatActivity(), ThemeProvider { val cacheFirmwareDir = File("${cacheDir.path}/registered/") ProgressDialogFragment.newInstance( - this, R.string.firmware_installing + this, + R.string.firmware_installing ) { progressCallback, _ -> var messageToShow: Any try { FileUtil.unzipToInternalStorage( - result.toString(), cacheFirmwareDir, progressCallback + result.toString(), + cacheFirmwareDir, + progressCallback ) val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1 val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2 @@ -423,7 +438,8 @@ class MainActivity : AppCompatActivity(), ThemeProvider { val firmwarePath = File(DirectoryInitialization.userDirectory + "/nand/system/Contents/registered/") ProgressDialogFragment.newInstance( - this, R.string.firmware_uninstalling + this, + R.string.firmware_uninstalling ) { progressCallback, _ -> var messageToShow: Any try { @@ -459,12 +475,15 @@ class MainActivity : AppCompatActivity(), ThemeProvider { } ProgressDialogFragment.newInstance( - this@MainActivity, R.string.verifying_content, false + this@MainActivity, + R.string.verifying_content, + false ) { _, _ -> var updatesMatchProgram = true for (document in documents) { val valid = NativeLibrary.doesUpdateMatchProgram( - addonViewModel.game!!.programId, document.toString() + addonViewModel.game!!.programId, + document.toString() ) if (!valid) { updatesMatchProgram = false @@ -480,14 +499,16 @@ class MainActivity : AppCompatActivity(), ThemeProvider { titleId = R.string.content_install_notice, descriptionId = R.string.content_install_notice_description, positiveAction = { homeViewModel.setContentToInstall(documents) }, - negativeAction = {}) + negativeAction = {} + ) } }.show(supportFragmentManager, ProgressDialogFragment.TAG) } private fun installContent(documents: List) { ProgressDialogFragment.newInstance( - this@MainActivity, R.string.installing_game_content + this@MainActivity, + R.string.installing_game_content ) { progressCallback, messageCallback -> var installSuccess = 0 var installOverwrite = 0 @@ -495,11 +516,14 @@ class MainActivity : AppCompatActivity(), ThemeProvider { var error = 0 documents.forEach { messageCallback.invoke(FileUtil.getFilename(it)) - when (InstallResult.from( - NativeLibrary.installFileToNand( - it.toString(), progressCallback + when ( + InstallResult.from( + NativeLibrary.installFileToNand( + it.toString(), + progressCallback + ) ) - )) { + ) { InstallResult.Success -> { installSuccess += 1 } @@ -525,7 +549,8 @@ class MainActivity : AppCompatActivity(), ThemeProvider { if (installSuccess > 0) { installResult.append( getString( - R.string.install_game_content_success_install, installSuccess + R.string.install_game_content_success_install, + installSuccess ) ) installResult.append(separator) @@ -533,7 +558,8 @@ class MainActivity : AppCompatActivity(), ThemeProvider { if (installOverwrite > 0) { installResult.append( getString( - R.string.install_game_content_success_overwrite, installOverwrite + R.string.install_game_content_success_overwrite, + installOverwrite ) ) installResult.append(separator) @@ -543,7 +569,8 @@ class MainActivity : AppCompatActivity(), ThemeProvider { installResult.append(separator) installResult.append( getString( - R.string.install_game_content_failed_count, errorTotal + R.string.install_game_content_failed_count, + errorTotal ) ) installResult.append(separator) @@ -584,7 +611,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider { } ProgressDialogFragment.newInstance( - this, R.string.exporting_user_data, true + this, + R.string.exporting_user_data, + true ) { progressCallback, _ -> val zipResult = FileUtil.zipFromInternalStorage( File(DirectoryInitialization.userDirectory!!), @@ -608,7 +637,8 @@ class MainActivity : AppCompatActivity(), ThemeProvider { } ProgressDialogFragment.newInstance( - this, R.string.importing_user_data + this, + R.string.importing_user_data ) { progressCallback, _ -> val checkStream = ZipInputStream(BufferedInputStream(contentResolver.openInputStream(result))) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/CustomSettingsHandler.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/CustomSettingsHandler.kt new file mode 100644 index 0000000000..508f9de463 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/CustomSettingsHandler.kt @@ -0,0 +1,314 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.utils + +import android.content.Context +import android.widget.Toast +import androidx.fragment.app.FragmentActivity +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.model.DriverViewModel +import org.yuzu.yuzu_emu.model.Game +import java.io.File +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine +import android.net.Uri +import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile + +object CustomSettingsHandler { + const val CUSTOM_CONFIG_ACTION = "dev.eden.eden_emulator.LAUNCH_WITH_CUSTOM_CONFIG" + const val EXTRA_TITLE_ID = "title_id" + const val EXTRA_CUSTOM_SETTINGS = "custom_settings" + + /** + * Apply custom settings from a string instead of loading from file + * @param titleId The game title ID (16-digit hex string) + * @param customSettings The complete INI file content as string + * @param context Application context + * @return Game object created from title ID, or null if not found + */ + fun applyCustomSettings(titleId: String, customSettings: String, context: Context): Game? { + // For synchronous calls without driver checking + Log.info("[CustomSettingsHandler] Applying custom settings for title ID: $titleId") + // Find the game by title ID + val game = findGameByTitleId(titleId, context) + if (game == null) { + Log.error("[CustomSettingsHandler] Game not found for title ID: $titleId") + return null + } + + // Check if config already exists - this should be handled by the caller + val configFile = getConfigFile(game) + if (configFile.exists()) { + Log.warning("[CustomSettingsHandler] Config file already exists for game: ${game.title}") + } + + // Write the config file + if (!writeConfigFile(game, customSettings)) { + Log.error("[CustomSettingsHandler] Failed to write config file") + return null + } + + // Initialize per-game config + try { + val fileName = FileUtil.getFilename(Uri.parse(game.path)) + NativeConfig.initializePerGameConfig(game.programId, fileName) + Log.info("[CustomSettingsHandler] Successfully applied custom settings") + return game + } catch (e: Exception) { + Log.error("[CustomSettingsHandler] Failed to apply custom settings: ${e.message}") + return null + } + } + + /** + * Apply custom settings with automatic driver checking and installation + * @param titleId The game title ID (16-digit hex string) + * @param customSettings The complete INI file content as string + * @param context Application context + * @param activity Fragment activity for driver installation dialogs (optional) + * @param driverViewModel DriverViewModel for driver management (optional) + * @return Game object created from title ID, or null if not found + */ + suspend fun applyCustomSettingsWithDriverCheck( + titleId: String, + customSettings: String, + context: Context, + activity: FragmentActivity?, + driverViewModel: DriverViewModel? + ): Game? { + Log.info("[CustomSettingsHandler] Applying custom settings for title ID: $titleId") + // Find the game by title ID + val game = findGameByTitleId(titleId, context) + if (game == null) { + Log.error("[CustomSettingsHandler] Game not found for title ID: $titleId") + // This will be handled by the caller to show appropriate error message + return null + } + + // Check if config already exists + val configFile = getConfigFile(game) + if (configFile.exists() && activity != null) { + Log.info( + "[CustomSettingsHandler] Config file already exists, asking user for confirmation" + ) + Toast.makeText( + activity, + activity.getString(R.string.config_exists_prompt), + Toast.LENGTH_SHORT + ).show() + val shouldOverwrite = askUserToOverwriteConfig(activity, game.title) + if (!shouldOverwrite) { + Log.info("[CustomSettingsHandler] User chose not to overwrite existing config") + Toast.makeText( + activity, + activity.getString(R.string.overwrite_cancelled), + Toast.LENGTH_SHORT + ).show() + return null + } + } + + // Check for driver requirements if activity and driverViewModel are provided + if (activity != null && driverViewModel != null) { + val driverPath = extractDriverPath(customSettings) + if (driverPath != null) { + Log.info("[CustomSettingsHandler] Custom settings specify driver: $driverPath") + // Check if driver exists in the driver storage + val driverFile = File(driverPath) + if (!driverFile.exists()) { + Log.error("[CustomSettingsHandler] Required driver not found: $driverPath") + Toast.makeText( + activity, + activity.getString( + R.string.custom_settings_failed_message, + game.title, + activity.getString(R.string.driver_not_found, driverFile.name) + ), + Toast.LENGTH_LONG + ).show() + // Don't write config if driver is missing + return null + } + + // Verify it's a valid driver + val metadata = GpuDriverHelper.getMetadataFromZip(driverFile) + if (metadata.name == null) { + Log.error("[CustomSettingsHandler] Invalid driver file: $driverPath") + Toast.makeText( + activity, + activity.getString( + R.string.custom_settings_failed_message, + game.title, + activity.getString(R.string.invalid_driver_file, driverFile.name) + ), + Toast.LENGTH_LONG + ).show() + return null + } + + Log.info("[CustomSettingsHandler] Driver verified: ${metadata.name}") + } + } + + // Only write the config file after all checks pass + if (!writeConfigFile(game, customSettings)) { + Log.error("[CustomSettingsHandler] Failed to write config file") + activity?.let { + Toast.makeText( + it, + it.getString(R.string.config_write_failed), + Toast.LENGTH_SHORT + ).show() + } + return null + } + + // Initialize per-game config + try { + SettingsFile.loadCustomConfig(game) + Log.info("[CustomSettingsHandler] Successfully applied custom settings") + activity?.let { + Toast.makeText( + it, + it.getString(R.string.custom_settings_applied), + Toast.LENGTH_SHORT + ).show() + } + return game + } catch (e: Exception) { + Log.error("[CustomSettingsHandler] Failed to apply custom settings: ${e.message}") + activity?.let { + Toast.makeText( + it, + it.getString(R.string.config_apply_failed), + Toast.LENGTH_SHORT + ).show() + } + return null + } + } + + /** + * Find a game by its title ID in the user's game library + */ + fun findGameByTitleId(titleId: String, context: Context): Game? { + Log.info("[CustomSettingsHandler] Searching for game with title ID: $titleId") + // Convert hex title ID to decimal for comparison with programId + val programIdDecimal = try { + titleId.toLong(16).toString() + } catch (e: NumberFormatException) { + Log.error("[CustomSettingsHandler] Invalid title ID format: $titleId") + return null + } + + // Expected hex format with "0" prefix + val expectedHex = "0${titleId.uppercase()}" + // First check cached games for fast lookup + GameHelper.cachedGameList.find { game -> + game.programId == programIdDecimal || + game.programIdHex.equals(expectedHex, ignoreCase = true) + }?.let { foundGame -> + Log.info("[CustomSettingsHandler] Found game in cache: ${foundGame.title}") + return foundGame + } + // If not in cache, perform full game library scan + Log.info("[CustomSettingsHandler] Game not in cache, scanning full library...") + val allGames = GameHelper.getGames() + val foundGame = allGames.find { game -> + game.programId == programIdDecimal || + game.programIdHex.equals(expectedHex, ignoreCase = true) + } + if (foundGame != null) { + Log.info("[CustomSettingsHandler] Found game: ${foundGame.title} at ${foundGame.path}") + } else { + Log.warning("[CustomSettingsHandler] No game found for title ID: $titleId") + } + return foundGame + } + + /** + * Get the config file path for a game + */ + private fun getConfigFile(game: Game): File { + return SettingsFile.getCustomSettingsFile(game) + } + + /** + * Get the title ID config file path + */ + private fun getTitleIdConfigFile(titleId: String): File { + val configDir = File(DirectoryInitialization.userDirectory, "config/custom") + return File(configDir, "$titleId.ini") + } + + /** + * Write the config file with the custom settings + */ + private fun writeConfigFile(game: Game, customSettings: String): Boolean { + return try { + val configFile = getConfigFile(game) + val configDir = configFile.parentFile + if (configDir != null && !configDir.exists()) { + configDir.mkdirs() + } + + configFile.writeText(customSettings) + + Log.info("[CustomSettingsHandler] Wrote config file: ${configFile.absolutePath}") + true + } catch (e: Exception) { + Log.error("[CustomSettingsHandler] Failed to write config file: ${e.message}") + false + } + } + + /** + * Ask user if they want to overwrite existing configuration + */ + private suspend fun askUserToOverwriteConfig(activity: FragmentActivity, gameTitle: String): Boolean { + return suspendCoroutine { continuation -> + activity.runOnUiThread { + MaterialAlertDialogBuilder(activity) + .setTitle(activity.getString(R.string.config_already_exists_title)) + .setMessage( + activity.getString(R.string.config_already_exists_message, gameTitle) + ) + .setPositiveButton(activity.getString(R.string.overwrite)) { _, _ -> + continuation.resume(true) + } + .setNegativeButton(activity.getString(R.string.cancel)) { _, _ -> + continuation.resume(false) + } + .setCancelable(false) + .show() + } + } + } + + /** + * Extract driver path from custom settings INI content + */ + private fun extractDriverPath(customSettings: String): String? { + val lines = customSettings.lines() + var inGpuDriverSection = false + + for (line in lines) { + val trimmed = line.trim() + if (trimmed.startsWith("[") && trimmed.endsWith("]")) { + inGpuDriverSection = trimmed == "[GpuDriver]" + continue + } + + if (inGpuDriverSection && trimmed.startsWith("driver_path=")) { + return trimmed.substringAfter("driver_path=") + } + } + + return null + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt index feed8b3cf8..a30eaf5890 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt @@ -197,7 +197,9 @@ object FileUtil { */ fun getFilename(uri: Uri): String { if (uri.scheme == "file") { - return uri.lastPathSegment?.takeIf { it.isNotEmpty() } ?: throw IOException("Invalid file URI: $uri") + return uri.lastPathSegment?.takeIf { it.isNotEmpty() } ?: throw IOException( + "Invalid file URI: $uri" + ) } val resolver = YuzuApplication.appContext.contentResolver diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PowerStateUpdater.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PowerStateUpdater.kt index 58d2d1e7ed..ec492569ce 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PowerStateUpdater.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PowerStateUpdater.kt @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later - package org.yuzu.yuzu_emu.utils import android.os.Handler @@ -17,7 +16,6 @@ object PowerStateUpdater { private var isStarted = false fun start() { - if (isStarted) { return } @@ -43,4 +41,4 @@ object PowerStateUpdater { } isStarted = false } -} \ No newline at end of file +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PowerStateUtils.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PowerStateUtils.kt index 86e3d9c785..48382fad5b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PowerStateUtils.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PowerStateUtils.kt @@ -13,7 +13,6 @@ object PowerStateUtils { @JvmStatic fun getBatteryInfo(context: Context?): IntArray { - if (context == null) { return intArrayOf(0, 0, 0) // Percentage, IsCharging, HasBattery } @@ -42,4 +41,4 @@ object PowerStateUtils { return results } -} \ No newline at end of file +} 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 a035628588..3e138c0244 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 @@ -23,10 +23,12 @@ import org.yuzu.yuzu_emu.features.settings.model.Settings object ThemeHelper { const val SYSTEM_BAR_ALPHA = 0.9f + // Listener that detects if the theme keys are being changed from the setting menu and recreates the activity private var listener: SharedPreferences.OnSharedPreferenceChangeListener? = null - private val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) - + private val preferences = PreferenceManager.getDefaultSharedPreferences( + YuzuApplication.appContext + ) fun setTheme(activity: AppCompatActivity) { setThemeMode(activity) @@ -52,6 +54,7 @@ object ThemeHelper { private fun getSelectedStaticThemeColor(): Int { val themeIndex = preferences.getInt(Settings.PREF_STATIC_THEME_COLOR, 0) val themes = arrayOf( + R.style.Theme_Eden_Main, R.style.Theme_Yuzu_Main_Violet, R.style.Theme_Yuzu_Main_Blue, R.style.Theme_Yuzu_Main_Cyan, @@ -120,7 +123,11 @@ object ThemeHelper { fun ThemeChangeListener(activity: AppCompatActivity) { listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> - val relevantKeys = listOf(Settings.PREF_STATIC_THEME_COLOR, Settings.PREF_THEME_MODE, Settings.PREF_BLACK_BACKGROUNDS) + val relevantKeys = listOf( + Settings.PREF_STATIC_THEME_COLOR, + Settings.PREF_THEME_MODE, + Settings.PREF_BLACK_BACKGROUNDS + ) if (key in relevantKeys) { activity.recreate() } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/views/CarouselRecyclerView.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/views/CarouselRecyclerView.kt index ae40fcb498..8a66ebf11f 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/views/CarouselRecyclerView.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/views/CarouselRecyclerView.kt @@ -1,6 +1,9 @@ // SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + package org.yuzu.yuzu_emu.ui import android.content.Context @@ -53,7 +56,7 @@ class CarouselRecyclerView @JvmOverloads constructor( var flingMultiplier: Float = 1f - public var pendingScrollAfterReload: Boolean = false + var pendingScrollAfterReload: Boolean = false var useCustomDrawingOrder: Boolean = false set(value) { @@ -76,7 +79,11 @@ class CarouselRecyclerView @JvmOverloads constructor( private fun getLayoutManagerCenter(layoutManager: RecyclerView.LayoutManager): Int { return if (layoutManager is LinearLayoutManager) { - calculateCenter(layoutManager.width, layoutManager.paddingStart, layoutManager.paddingEnd) + calculateCenter( + layoutManager.width, + layoutManager.paddingStart, + layoutManager.paddingEnd + ) } else { width / 2 } @@ -121,13 +128,13 @@ class CarouselRecyclerView @JvmOverloads constructor( fun shapingFunction(x: Float, option: Int = 0): Float { return when (option) { - 0 -> 1f //Off - 1 -> 1f - x //linear descending - 2 -> (1f - x) * (1f - x) //Ease out - 3 -> if (x < 0.05f) 1f else (1f-x) * 0.8f - 4 -> kotlin.math.cos(x * Math.PI).toFloat() //Cosine - 5 -> kotlin.math.cos( (1.5f * x).coerceIn(0f, 1f) * Math.PI).toFloat() //Cosine 1.5x trimmed - else -> 1f //Default to Off + 0 -> 1f // Off + 1 -> 1f - x // linear descending + 2 -> (1f - x) * (1f - x) // Ease out + 3 -> if (x < 0.05f) 1f else (1f - x) * 0.8f + 4 -> kotlin.math.cos(x * Math.PI).toFloat() // Cosine + 5 -> kotlin.math.cos((1.5f * x).coerceIn(0f, 1f) * Math.PI).toFloat() // Cosine 1.5x trimmed + else -> 1f // Default to Off } } @@ -143,20 +150,36 @@ class CarouselRecyclerView @JvmOverloads constructor( val center = getRecyclerViewCenter() val distance = abs(getChildDistanceToCenter(child)) val internalBorderScale = resources.getFraction(R.fraction.carousel_bordercards_scale, 1, 1) - val borderScale = preferences.getFloat(CAROUSEL_BORDERCARDS_SCALE, internalBorderScale).coerceIn(0f, 1f) + val borderScale = preferences.getFloat(CAROUSEL_BORDERCARDS_SCALE, internalBorderScale).coerceIn( + 0f, + 1f + ) val shapeInput = (distance / center).coerceIn(0f, 1f) val internalShapeSetting = resources.getInteger(R.integer.carousel_cards_scaling_shape) - val scalingShapeSetting = preferences.getInt(CAROUSEL_CARDS_SCALING_SHAPE, internalShapeSetting) + val scalingShapeSetting = preferences.getInt( + CAROUSEL_CARDS_SCALING_SHAPE, + internalShapeSetting + ) val shapedScaling = shapingFunction(shapeInput, scalingShapeSetting) val scale = (borderScale + (1f - borderScale) * shapedScaling).coerceIn(0f, 1f) val maxDistance = width / 2f val alphaInput = (distance / maxDistance).coerceIn(0f, 1f) - val internalBordersAlpha = resources.getFraction(R.fraction.carousel_bordercards_alpha, 1, 1) - val borderAlpha = preferences.getFloat(CAROUSEL_BORDERCARDS_ALPHA, internalBordersAlpha).coerceIn(0f, 1f) + val internalBordersAlpha = resources.getFraction( + R.fraction.carousel_bordercards_alpha, + 1, + 1 + ) + val borderAlpha = preferences.getFloat(CAROUSEL_BORDERCARDS_ALPHA, internalBordersAlpha).coerceIn( + 0f, + 1f + ) val internalAlphaShapeSetting = resources.getInteger(R.integer.carousel_cards_alpha_shape) - val alphaShapeSetting = preferences.getInt(CAROUSEL_CARDS_ALPHA_SHAPE, internalAlphaShapeSetting) + val alphaShapeSetting = preferences.getInt( + CAROUSEL_CARDS_ALPHA_SHAPE, + internalAlphaShapeSetting + ) val shapedAlpha = shapingFunction(alphaInput, alphaShapeSetting) val alpha = (borderAlpha + (1f - borderAlpha) * shapedAlpha).coerceIn(0f, 1f) @@ -185,16 +208,33 @@ class CarouselRecyclerView @JvmOverloads constructor( val insets = rootWindowInsets val bottomInset = insets?.getInsets(android.view.WindowInsets.Type.systemBars())?.bottom ?: 0 val internalFactor = resources.getFraction(R.fraction.carousel_card_size_factor, 1, 1) - val userFactor = preferences.getFloat(CAROUSEL_CARD_SIZE_FACTOR, internalFactor).coerceIn(0f, 1f) + val userFactor = preferences.getFloat(CAROUSEL_CARD_SIZE_FACTOR, internalFactor).coerceIn( + 0f, + 1f + ) val cardSize = (userFactor * (height - bottomInset)).toInt() gameAdapter?.setCardSize(cardSize) - val internalOverlapFactor = resources.getFraction(R.fraction.carousel_overlap_factor, 1, 1) - overlapFactor = preferences.getFloat(CAROUSEL_OVERLAP_FACTOR, internalOverlapFactor).coerceIn(0f, 1f) + val internalOverlapFactor = resources.getFraction( + R.fraction.carousel_overlap_factor, + 1, + 1 + ) + overlapFactor = preferences.getFloat(CAROUSEL_OVERLAP_FACTOR, internalOverlapFactor).coerceIn( + 0f, + 1f + ) overlapPx = (cardSize * overlapFactor).toInt() - val internalFlingMultiplier = resources.getFraction(R.fraction.carousel_fling_multiplier, 1, 1) - flingMultiplier = preferences.getFloat(CAROUSEL_FLING_MULTIPLIER, internalFlingMultiplier).coerceIn(1f, 5f) + val internalFlingMultiplier = resources.getFraction( + R.fraction.carousel_fling_multiplier, + 1, + 1 + ) + flingMultiplier = preferences.getFloat( + CAROUSEL_FLING_MULTIPLIER, + internalFlingMultiplier + ).coerceIn(1f, 5f) gameAdapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { override fun onChanged() { @@ -290,20 +330,28 @@ class CarouselRecyclerView @JvmOverloads constructor( View.FOCUS_LEFT -> { if (position > 0) { val now = System.currentTimeMillis() - val repeatDetected = (now - lastFocusSearchTime) < resources.getInteger(R.integer.carousel_focus_search_repeat_threshold_ms) + val repeatDetected = (now - lastFocusSearchTime) < resources.getInteger( + R.integer.carousel_focus_search_repeat_threshold_ms + ) lastFocusSearchTime = now - if (!repeatDetected) { //ensures the first run + if (!repeatDetected) { // ensures the first run val offset = focused.width - overlapPx smoothScrollBy(-offset, 0) } - findViewHolderForAdapterPosition(position - 1)?.itemView ?: super.focusSearch(focused, direction) + findViewHolderForAdapterPosition(position - 1)?.itemView ?: super.focusSearch( + focused, + direction + ) } else { focused } } View.FOCUS_RIGHT -> { if (position < itemCount - 1) { - findViewHolderForAdapterPosition(position + 1)?.itemView ?: super.focusSearch(focused, direction) + findViewHolderForAdapterPosition(position + 1)?.itemView ?: super.focusSearch( + focused, + direction + ) } else { focused } @@ -341,7 +389,10 @@ class CarouselRecyclerView @JvmOverloads constructor( inner class OverlappingDecoration(private val overlap: Int) : ItemDecoration() { override fun getItemOffsets( - outRect: Rect, view: View, parent: RecyclerView, state: State + outRect: Rect, + view: View, + parent: RecyclerView, + state: State ) { val position = parent.getChildAdapterPosition(view) if (position > 0) { @@ -378,12 +429,17 @@ class CarouselRecyclerView @JvmOverloads constructor( return layoutManager.findViewByPosition(getClosestChildPosition()) } - //NEEDED: fixes ghost movement when snapping, but breaks inertial scrolling + // NEEDED: fixes ghost movement when snapping, but breaks inertial scrolling override fun calculateDistanceToFinalSnap( layoutManager: RecyclerView.LayoutManager, targetView: View ): IntArray? { - if (layoutManager !is LinearLayoutManager) return super.calculateDistanceToFinalSnap(layoutManager, targetView) + if (layoutManager !is LinearLayoutManager) { + return super.calculateDistanceToFinalSnap( + layoutManager, + targetView + ) + } val out = IntArray(2) out[0] = getChildDistanceToCenter(targetView).toInt() out[1] = 0 @@ -399,11 +455,14 @@ class CarouselRecyclerView @JvmOverloads constructor( if (layoutManager !is LinearLayoutManager) return RecyclerView.NO_POSITION val closestPosition = this@CarouselRecyclerView.getClosestChildPosition() val internalMaxFling = resources.getInteger(R.integer.carousel_max_fling_count) - val maxFling = preferences.getInt(CAROUSEL_MAX_FLING_COUNT, internalMaxFling).coerceIn(1, 10) + val maxFling = preferences.getInt(CAROUSEL_MAX_FLING_COUNT, internalMaxFling).coerceIn( + 1, + 10 + ) val rawFlingCount = if (velocityX == 0) 0 else velocityX / 2000 val flingCount = rawFlingCount.coerceIn(-maxFling, maxFling) - var targetPos = (closestPosition + flingCount).coerceIn(0, layoutManager.itemCount - 1) + val targetPos = (closestPosition + flingCount).coerceIn(0, layoutManager.itemCount - 1) return targetPos } } -} \ No newline at end of file +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/views/GradientBorderCardView.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/views/GradientBorderCardView.kt new file mode 100644 index 0000000000..7badadd119 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/views/GradientBorderCardView.kt @@ -0,0 +1,120 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.views + +import android.content.Context +import android.graphics.* +import android.util.AttributeSet +import com.google.android.material.card.MaterialCardView +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.features.settings.model.Settings +import androidx.preference.PreferenceManager + +class GradientBorderCardView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : MaterialCardView(context, attrs, defStyleAttr) { + + private val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.STROKE + strokeWidth = 6f + } + + private val borderPath = Path() + private val borderRect = RectF() + private var showGradientBorder = false + private var isEdenTheme = false + + init { + setWillNotDraw(false) + updateThemeState() + } + + private fun updateThemeState() { + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + val themeIndex = try { + prefs.getInt(Settings.PREF_STATIC_THEME_COLOR, 0) + } catch (e: Exception) { + 0 // Default to Eden theme if error + } + isEdenTheme = themeIndex == 0 + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + + // Update border style based on theme + if (isEdenTheme) { + // Gradient for Eden theme + borderPaint.shader = LinearGradient( + 0f, 0f, + w.toFloat(), h.toFloat(), + context.getColor(R.color.eden_border_gradient_start), + context.getColor(R.color.eden_border_gradient_end), + Shader.TileMode.CLAMP + ) + } else { + // Solid color for other themes + borderPaint.shader = null + val typedValue = android.util.TypedValue() + context.theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue, true) + borderPaint.color = typedValue.data + } + + // Update border rect with padding for stroke + val halfStroke = borderPaint.strokeWidth / 2 + borderRect.set( + halfStroke, + halfStroke, + w - halfStroke, + h - halfStroke + ) + + // Update path with rounded corners + borderPath.reset() + borderPath.addRoundRect( + borderRect, + radius, + radius, + Path.Direction.CW + ) + } + + override fun onFocusChanged(gainFocus: Boolean, direction: Int, previouslyFocusedRect: Rect?) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect) + showGradientBorder = gainFocus + invalidate() + } + + override fun setSelected(selected: Boolean) { + super.setSelected(selected) + showGradientBorder = selected + invalidate() + } + + override fun setPressed(pressed: Boolean) { + super.setPressed(pressed) + if (pressed) { + showGradientBorder = true + invalidate() + } + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + if (showGradientBorder && !isPressed) { + canvas.drawPath(borderPath, borderPaint) + } + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + updateThemeState() + requestLayout() + } +} diff --git a/src/android/app/src/main/res/drawable/eden_background_gradient.xml b/src/android/app/src/main/res/drawable/eden_background_gradient.xml new file mode 100644 index 0000000000..476e88bb32 --- /dev/null +++ b/src/android/app/src/main/res/drawable/eden_background_gradient.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/drawable/eden_button_primary_background.xml b/src/android/app/src/main/res/drawable/eden_button_primary_background.xml new file mode 100644 index 0000000000..ec22911d66 --- /dev/null +++ b/src/android/app/src/main/res/drawable/eden_button_primary_background.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/drawable/eden_card_background.xml b/src/android/app/src/main/res/drawable/eden_card_background.xml new file mode 100644 index 0000000000..a944c19fed --- /dev/null +++ b/src/android/app/src/main/res/drawable/eden_card_background.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/src/android/app/src/main/res/drawable/eden_card_elevated_background.xml b/src/android/app/src/main/res/drawable/eden_card_elevated_background.xml new file mode 100644 index 0000000000..04ccf3172c --- /dev/null +++ b/src/android/app/src/main/res/drawable/eden_card_elevated_background.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/drawable/eden_card_elevated_selector.xml b/src/android/app/src/main/res/drawable/eden_card_elevated_selector.xml new file mode 100644 index 0000000000..9c7144c100 --- /dev/null +++ b/src/android/app/src/main/res/drawable/eden_card_elevated_selector.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/drawable/eden_dialog_background.xml b/src/android/app/src/main/res/drawable/eden_dialog_background.xml new file mode 100644 index 0000000000..d06b85d406 --- /dev/null +++ b/src/android/app/src/main/res/drawable/eden_dialog_background.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/drawable/eden_gradient_border.xml b/src/android/app/src/main/res/drawable/eden_gradient_border.xml new file mode 100644 index 0000000000..d17b57d7ae --- /dev/null +++ b/src/android/app/src/main/res/drawable/eden_gradient_border.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/drawable/eden_list_item_selector.xml b/src/android/app/src/main/res/drawable/eden_list_item_selector.xml new file mode 100644 index 0000000000..2689c6d41e --- /dev/null +++ b/src/android/app/src/main/res/drawable/eden_list_item_selector.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/drawable/popup_menu_background.xml b/src/android/app/src/main/res/drawable/popup_menu_background.xml new file mode 100644 index 0000000000..f792c97a23 --- /dev/null +++ b/src/android/app/src/main/res/drawable/popup_menu_background.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/drawable/theme_card_background.xml b/src/android/app/src/main/res/drawable/theme_card_background.xml new file mode 100644 index 0000000000..1d369c6519 --- /dev/null +++ b/src/android/app/src/main/res/drawable/theme_card_background.xml @@ -0,0 +1,13 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/drawable/theme_dialog_background.xml b/src/android/app/src/main/res/drawable/theme_dialog_background.xml new file mode 100644 index 0000000000..9d456afb60 --- /dev/null +++ b/src/android/app/src/main/res/drawable/theme_dialog_background.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/drawable/theme_list_item_selector.xml b/src/android/app/src/main/res/drawable/theme_list_item_selector.xml new file mode 100644 index 0000000000..ab6fb2f213 --- /dev/null +++ b/src/android/app/src/main/res/drawable/theme_list_item_selector.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/android/app/src/main/res/layout-land/card_game_carousel.xml b/src/android/app/src/main/res/layout-land/card_game_carousel.xml index fcc9397a13..4d8e5fd148 100644 --- a/src/android/app/src/main/res/layout-land/card_game_carousel.xml +++ b/src/android/app/src/main/res/layout-land/card_game_carousel.xml @@ -1,27 +1,30 @@ - + app:cardBackgroundColor="@color/eden_card_background" + app:strokeWidth="1dp" + app:strokeColor="@color/eden_border"> - - + diff --git a/src/android/app/src/main/res/layout-land/fragment_games.xml b/src/android/app/src/main/res/layout-land/fragment_games.xml index 14e9d5358c..9bd60c6f89 100644 --- a/src/android/app/src/main/res/layout-land/fragment_games.xml +++ b/src/android/app/src/main/res/layout-land/fragment_games.xml @@ -4,7 +4,6 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="?attr/colorSurface" android:clipChildren="false" > @@ -44,7 +43,10 @@ style="?attr/materialCardViewFilledStyle" android:layout_width="match_parent" android:layout_height="48dp" - app:cardCornerRadius="21dp" + app:cardCornerRadius="24dp" + app:cardBackgroundColor="?attr/colorSurfaceVariant" + app:strokeColor="?attr/colorOutline" + app:strokeWidth="1dp" > \ No newline at end of file diff --git a/src/android/app/src/main/res/layout-w600dp/activity_main.xml b/src/android/app/src/main/res/layout-w600dp/activity_main.xml index d4188e7c9d..99d24f3e65 100644 --- a/src/android/app/src/main/res/layout-w600dp/activity_main.xml +++ b/src/android/app/src/main/res/layout-w600dp/activity_main.xml @@ -6,7 +6,7 @@ android:id="@+id/coordinator_main" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="?attr/colorSurface"> + android:background="@drawable/eden_background_gradient"> + > + android:touchscreenBlocksFocus="false" + android:background="@android:color/transparent" + app:elevation="0dp"> + android:orientation="horizontal" + android:padding="24dp"> - - - + + android:orientation="vertical" + android:paddingHorizontal="24dp" + android:paddingVertical="20dp"> - + - + - + + - - - + + android:orientation="vertical" + android:paddingHorizontal="24dp" + android:paddingVertical="20dp"> - + - + - + + - - - + + android:orientation="vertical" + android:paddingHorizontal="24dp" + android:paddingVertical="20dp"> - + - + - + + - - - + + android:orientation="vertical" + android:paddingHorizontal="24dp" + android:paddingVertical="20dp"> - + - + - + + -