Browse Source

fix pls lol

pull/2852/head
godpow 3 months ago
parent
commit
acb4c0a473
  1. 68
      src/android/android/.gitignore
  2. 114
      src/android/android/app/src/main/AndroidManifest.xml
  3. 151
      src/android/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt
  4. 476
      src/android/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
  5. 140
      src/android/android/app/src/main/java/org/yuzu/yuzu_emu/utils/LogFilter.kt
  6. 1584
      src/android/android/app/src/main/res/values/strings.xml
  7. 23
      src/android/android/build.gradle.kts
  8. 19
      src/android/android/gradle.properties
  9. BIN
      src/android/android/gradle/wrapper/gradle-wrapper.jar
  10. 6
      src/android/android/gradle/wrapper/gradle-wrapper.properties
  11. 175
      src/android/android/gradlew
  12. 87
      src/android/android/gradlew.bat
  13. 21
      src/android/android/settings.gradle.kts
  14. 10
      src/android/app/src/main/AndroidManifest.xml
  15. 30
      src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/LogFilter.kt
  16. 9
      src/android/app/src/main/res/xml/file_paths.xml

68
src/android/android/.gitignore

@ -0,0 +1,68 @@
# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
# Built application files
*.apk
*.ap_
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/
# Keystore files
# Uncomment the following line if you do not want to check your keystore files in.
#*.jks
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
# CXX compile cache
app/.cxx
# Google Services (e.g. APIs or Firebase)
google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Autogenerated library for vulkan validation layers
libVkLayer_khronos_validation.so

114
src/android/android/app/src/main/AndroidManifest.xml

@ -0,0 +1,114 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
SPDX-FileCopyrightText: 2023 yuzu Emulator Project
SPDX-License-Identifier: GPL-3.0-or-later
SPDX-FileCopyrightText: 2025 Eden Emulator Project
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!--
SPDX-FileCopyrightText: Eden Emulator Project
SPDX-License-Identifier: GPL-3.0-or-later
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<uses-feature android:name="android.hardware.gamepad" android:required="false" />
<uses-feature android:name="android.software.leanback" android:required="false" />
<uses-feature android:name="android.hardware.vulkan.version" android:version="0x401000" android:required="true" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" android:required="false" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<application
android:name="org.yuzu.yuzu_emu.YuzuApplication"
android:label="@string/app_name_suffixed"
android:icon="@drawable/ic_launcher"
android:allowBackup="true"
android:hasFragileUserData="false"
android:supportsRtl="true"
android:isGame="true"
android:appCategory="game"
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">
<meta-data android:name="com.samsung.android.gamehub" android:value="true" />
<meta-data android:name="com.xiaomi.gamecenter.sdk.service.enabled" android:value="true" />
<meta-data android:name="com.asus.gamecenter.gamebooster" android:value="true" />
<meta-data android:name="com.oneplus.gamespace.gamebooster" android:value="true" />
<meta-data android:name="android.game_mode_config" android:resource="@xml/game_mode_config" />
<activity
android:name="org.yuzu.yuzu_emu.ui.main.MainActivity"
android:exported="true"
android:theme="@style/Theme.Yuzu.Splash.Main">
<intent-filter>
<action android:name="com.miui.gamecenter.GAME_BOOSTER_LAUNCH"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.GAME" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity"
android:theme="@style/Theme.Yuzu.Main"
android:label="@string/preferences_settings"/>
<activity
android:name="org.yuzu.yuzu_emu.activities.EmulationActivity"
android:theme="@style/Theme.Yuzu.Main"
android:launchMode="singleTop"
android:supportsPictureInPicture="true"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
android:exported="true">
<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/octet-stream" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/octet-stream" android:scheme="content"/>
</intent-filter>
<intent-filter>
<action android:name="dev.eden.eden_emulator.LAUNCH_WITH_CUSTOM_CONFIG" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data android:name="android.nfc.action.TECH_DISCOVERED" android:resource="@xml/nfc_tech_filter" />
</activity>
<service android:name="org.yuzu.yuzu_emu.utils.ForegroundService" android:foregroundServiceType="specialUse">
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" android:value="Keep emulation running in background"/>
</service>
<provider
android:name=".features.DocumentProvider"
android:authorities="${applicationId}.user"
android:grantUriPermissions="true"
android:exported="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>
</application>
</manifest>

151
src/android/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt

@ -0,0 +1,151 @@
// 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.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
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.databinding.CardHomeOptionWithButtonBinding
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
import org.yuzu.yuzu_emu.model.HomeSetting
import org.yuzu.yuzu_emu.utils.ViewUtils.marquee
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import org.yuzu.yuzu_emu.utils.collect
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
class HomeSettingAdapter(
private val activity: AppCompatActivity,
private val viewLifecycle: LifecycleOwner,
options: List<HomeSetting>,
private val onFilterClick: (() -> Unit)? = null
) : AbstractListAdapter<HomeSetting, AbstractViewHolder<HomeSetting>>(options) {
companion object {
private const val VIEW_TYPE_NORMAL = 0
private const val VIEW_TYPE_WITH_BUTTON = 1
}
override fun getItemViewType(position: Int): Int {
val item = getItem(position)
return if (item.titleId == R.string.share_log) {
VIEW_TYPE_WITH_BUTTON
} else {
VIEW_TYPE_NORMAL
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractViewHolder<HomeSetting> {
return when (viewType) {
VIEW_TYPE_WITH_BUTTON -> {
val binding = CardHomeOptionWithButtonBinding.inflate(LayoutInflater.from(parent.context), parent, false)
HomeOptionWithButtonViewHolder(binding)
}
else -> {
val binding = CardHomeOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
HomeOptionViewHolder(binding)
}
}
}
inner class HomeOptionViewHolder(val binding: CardHomeOptionBinding) :
AbstractViewHolder<HomeSetting>(binding) {
override fun bind(model: HomeSetting) {
binding.optionTitle.text = activity.resources.getString(model.titleId)
binding.optionDescription.text = activity.resources.getString(model.descriptionId)
binding.optionIcon.setImageDrawable(
ResourcesCompat.getDrawable(
activity.resources,
model.iconId,
activity.theme
)
)
if (!model.isEnabled.invoke()) {
binding.optionTitle.alpha = 0.5f
binding.optionDescription.alpha = 0.5f
binding.optionIcon.alpha = 0.5f
}
model.details.collect(viewLifecycle) { updateOptionDetails(it) }
binding.optionDetail.marquee()
binding.root.setOnClickListener { onClick(model) }
}
private fun onClick(model: HomeSetting) {
if (model.isEnabled.invoke()) {
model.onClick.invoke()
} else {
MessageDialogFragment.newInstance(
activity,
titleId = model.disabledTitleId,
descriptionId = model.disabledMessageId
).show(activity.supportFragmentManager, MessageDialogFragment.TAG)
}
}
private fun updateOptionDetails(detailString: String) {
if (detailString.isNotEmpty()) {
binding.optionDetail.text = detailString
binding.optionDetail.setVisible(true)
}
}
}
inner class HomeOptionWithButtonViewHolder(val binding: CardHomeOptionWithButtonBinding) :
AbstractViewHolder<HomeSetting>(binding) {
override fun bind(model: HomeSetting) {
binding.optionTitle.text = activity.resources.getString(model.titleId)
binding.optionDescription.text = activity.resources.getString(model.descriptionId)
binding.optionIcon.setImageDrawable(
ResourcesCompat.getDrawable(
activity.resources,
model.iconId,
activity.theme
)
)
if (!model.isEnabled.invoke()) {
binding.optionTitle.alpha = 0.5f
binding.optionDescription.alpha = 0.5f
binding.optionIcon.alpha = 0.5f
binding.filterButton.alpha = 0.5f
}
model.details.collect(viewLifecycle) { updateOptionDetails(it) }
binding.optionDetail.marquee()
binding.root.setOnClickListener { onClick(model) }
binding.filterButton.setOnClickListener {
onFilterClick?.invoke()
}
}
private fun onClick(model: HomeSetting) {
if (model.isEnabled.invoke()) {
model.onClick.invoke()
} else {
MessageDialogFragment.newInstance(
activity,
titleId = model.disabledTitleId,
descriptionId = model.disabledMessageId
).show(activity.supportFragmentManager, MessageDialogFragment.TAG)
}
}
private fun updateOptionDetails(detailString: String) {
if (detailString.isNotEmpty()) {
binding.optionDetail.text = detailString
binding.optionDetail.setVisible(true)
}
}
}
}

476
src/android/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt

@ -0,0 +1,476 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments
import android.Manifest
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.provider.DocumentsContract
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
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.HomeNavigationDirections
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
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.LogFilter
class HomeSettingsFragment : Fragment() {
private var _binding: FragmentHomeSettingsBinding? = null
private val binding get() = _binding!!
private lateinit var mainActivity: MainActivity
private val homeViewModel: HomeViewModel by activityViewModels()
private val driverViewModel: DriverViewModel by activityViewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentHomeSettingsBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
homeViewModel.setStatusBarShadeVisibility(visible = true)
mainActivity = requireActivity() as MainActivity
val optionsList: MutableList<HomeSetting> = mutableListOf<HomeSetting>().apply {
add(
HomeSetting(
R.string.advanced_settings,
R.string.settings_description,
R.drawable.ic_settings,
{
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
null,
Settings.MenuTag.SECTION_ROOT
)
binding.root.findNavController().navigate(action)
}
)
)
add(
HomeSetting(
R.string.app_settings,
R.string.app_settings_description,
R.drawable.ic_palette,
{
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
null,
Settings.MenuTag.SECTION_APP_SETTINGS
)
binding.root.findNavController().navigate(action)
}
)
)
add(
HomeSetting(
R.string.preferences_controls,
R.string.preferences_controls_description,
R.drawable.ic_controller,
{
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
null,
Settings.MenuTag.SECTION_INPUT
)
binding.root.findNavController().navigate(action)
}
)
)
add(
HomeSetting(
R.string.gpu_driver_manager,
R.string.install_gpu_driver_description,
R.drawable.ic_build,
{
val action = HomeSettingsFragmentDirections
.actionHomeSettingsFragmentToDriverManagerFragment(null)
binding.root.findNavController().navigate(action)
},
{ true },
R.string.custom_driver_not_supported,
R.string.custom_driver_not_supported_description,
driverViewModel.selectedDriverTitle
)
)
add(
HomeSetting(
R.string.multiplayer,
R.string.multiplayer_description,
R.drawable.ic_two_users,
{
mainActivity.displayMultiplayerDialog()
}
)
)
add(
HomeSetting(
R.string.applets,
R.string.applets_description,
R.drawable.ic_applet,
{
binding.root.findNavController()
.navigate(R.id.action_homeSettingsFragment_to_appletLauncherFragment)
},
{ NativeLibrary.isFirmwareAvailable() },
R.string.applets_error_firmware,
R.string.applets_error_description
)
)
add(
HomeSetting(
R.string.manage_yuzu_data,
R.string.manage_yuzu_data_description,
R.drawable.ic_install,
{
binding.root.findNavController()
.navigate(R.id.action_homeSettingsFragment_to_installableFragment)
}
)
)
add(
HomeSetting(
R.string.manage_game_folders,
R.string.select_games_folder_description,
R.drawable.ic_add,
{
binding.root.findNavController()
.navigate(R.id.action_homeSettingsFragment_to_gameFoldersFragment)
}
)
)
add(
HomeSetting(
R.string.verify_installed_content,
R.string.verify_installed_content_description,
R.drawable.ic_check_circle,
{
ProgressDialogFragment.newInstance(
requireActivity(),
titleId = R.string.verifying,
cancellable = true
) { progressCallback, _ ->
val result = NativeLibrary.verifyInstalledContents(progressCallback)
return@newInstance if (progressCallback.invoke(100, 100)) {
// Invoke the progress callback to check if the process was cancelled
MessageDialogFragment.newInstance(
titleId = R.string.verify_no_result,
descriptionId = R.string.verify_no_result_description
)
} else if (result.isEmpty()) {
MessageDialogFragment.newInstance(
titleId = R.string.verify_success,
descriptionId = R.string.operation_completed_successfully
)
} else {
val failedNames = result.joinToString("\n")
val errorMessage = YuzuApplication.appContext.getString(
R.string.verification_failed_for,
failedNames
)
MessageDialogFragment.newInstance(
titleId = R.string.verify_failure,
descriptionString = errorMessage
)
}
}.show(parentFragmentManager, ProgressDialogFragment.TAG)
}
)
)
add(
HomeSetting(
R.string.share_log,
R.string.share_log_description,
R.drawable.ic_log,
{ shareLog() }
)
)
add(
HomeSetting(
R.string.open_user_folder,
R.string.open_user_folder_description,
R.drawable.ic_folder_open,
{ openFileManager() }
)
)
add(
HomeSetting(
R.string.about,
R.string.about_description,
R.drawable.ic_info_outline,
{
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
parentFragmentManager.primaryNavigationFragment?.findNavController()
?.navigate(R.id.action_homeSettingsFragment_to_aboutFragment)
}
)
)
}
binding.homeSettingsList.apply {
layoutManager =
GridLayoutManager(requireContext(), resources.getInteger(R.integer.grid_columns))
adapter = HomeSettingAdapter(
requireActivity() as AppCompatActivity,
viewLifecycleOwner,
optionsList
) {
// Filter button click callback
shareLogWithFilter(true)
}
val spacing = resources.getDimensionPixelSize(R.dimen.spacing_small)
addItemDecoration(SpacingItemDecoration(spacing))
}
setInsets()
}
override fun onStart() {
super.onStart()
exitTransition = null
}
override fun onResume() {
super.onResume()
driverViewModel.updateDriverNameForGame(null)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun openFileManager() {
// First, try to open the user data folder directly
try {
startActivity(getFileManagerIntentOnDocumentProvider(Intent.ACTION_VIEW))
return
} catch (_: ActivityNotFoundException) {
}
try {
startActivity(getFileManagerIntentOnDocumentProvider("android.provider.action.BROWSE"))
return
} catch (_: ActivityNotFoundException) {
}
// Just try to open the file manager, try the package name used on "normal" phones
try {
startActivity(getFileManagerIntent("com.google.android.documentsui"))
showNoLinkNotification()
return
} catch (_: ActivityNotFoundException) {
}
try {
// Next, try the AOSP package name
startActivity(getFileManagerIntent("com.android.documentsui"))
showNoLinkNotification()
return
} catch (_: ActivityNotFoundException) {
}
Toast.makeText(
requireContext(),
resources.getString(R.string.no_file_manager),
Toast.LENGTH_LONG
).show()
}
private fun getFileManagerIntent(packageName: String): Intent {
// Fragile, but some phones don't expose the system file manager in any better way
val intent = Intent(Intent.ACTION_MAIN)
intent.setClassName(packageName, "com.android.documentsui.files.FilesActivity")
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
return intent
}
private fun getFileManagerIntentOnDocumentProvider(action: String): Intent {
val authority = "${requireContext().packageName}.user"
val intent = Intent(action)
intent.addCategory(Intent.CATEGORY_DEFAULT)
intent.data = DocumentsContract.buildRootUri(authority, DocumentProvider.ROOT_ID)
intent.addFlags(
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
return intent
}
private fun showNoLinkNotification() {
val builder = NotificationCompat.Builder(
requireContext(),
getString(R.string.notice_notification_channel_id)
)
.setSmallIcon(R.drawable.ic_stat_notification_logo)
.setContentTitle(getString(R.string.notification_no_directory_link))
.setContentText(getString(R.string.notification_no_directory_link_description))
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
// TODO: Make the click action for this notification lead to a help article
with(NotificationManagerCompat.from(requireContext())) {
if (ActivityCompat.checkSelfPermission(
requireContext(),
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
Toast.makeText(
requireContext(),
resources.getString(R.string.notification_permission_not_granted),
Toast.LENGTH_LONG
).show()
return
}
notify(0, builder.build())
}
}
// Share the current log if we just returned from a game but share the old log
// if we just started the app and the old log exists.
private fun shareLog() {
shareLogWithFilter(false)
}
private fun shareLogWithFilter(filter: Boolean) {
val currentLog = DocumentFile.fromSingleUri(
mainActivity,
DocumentsContract.buildDocumentUri(
DocumentProvider.AUTHORITY,
"${DocumentProvider.ROOT_ID}/log/eden_log.txt"
)
)!!
val oldLog = DocumentFile.fromSingleUri(
mainActivity,
DocumentsContract.buildDocumentUri(
DocumentProvider.AUTHORITY,
"${DocumentProvider.ROOT_ID}/log/eden_log.txt.old.txt"
)
)!!
// Determine which log to use
val logToShare = if (!Log.gameLaunched && oldLog.exists()) {
oldLog.uri
} else if (currentLog.exists()) {
currentLog.uri
} else {
null
}
if (logToShare == null) {
Toast.makeText(
requireContext(),
getText(R.string.share_log_missing),
Toast.LENGTH_SHORT
).show()
return
}
// Process the log and share it
processAndShareLog(logToShare, filter)
}
private fun processAndShareLog(logUri: Uri, filter: Boolean) {
val shareUri = if (filter) {
// Create filtered log file first
val result = LogFilter.createFilteredLogFile(requireContext(), logUri)
if (result != null) {
val (filteredFile, filteredUri) = result
val filterSuccess = LogFilter.filterLogs(requireContext(), logUri, filteredFile)
if (filterSuccess) {
Toast.makeText(
requireContext(),
"Filtered log created successfully",
Toast.LENGTH_SHORT
).show()
filteredUri
} else {
Toast.makeText(
requireContext(),
"Failed to filter log, sharing original",
Toast.LENGTH_SHORT
).show()
Log.error("[HomeSettingsFragment] Failed to filter log file")
logUri
}
} else {
Toast.makeText(
requireContext(),
"Failed to create filtered file, sharing original",
Toast.LENGTH_SHORT
).show()
Log.error("[HomeSettingsFragment] Failed to create filtered log file")
logUri
}
} else {
// Use original log without filtering
logUri
}
// Share the log
val intent = Intent(Intent.ACTION_SEND)
.setDataAndType(shareUri, FileUtil.TEXT_PLAIN)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.putExtra(Intent.EXTRA_STREAM, shareUri)
startActivity(Intent.createChooser(intent, getText(R.string.share_log)))
}
private fun setInsets() =
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
binding.scrollViewSettings.updatePadding(
top = barInsets.top
)
binding.homeSettingsList.updatePadding(
left = barInsets.left + cutoutInsets.left,
top = cutoutInsets.top,
right = barInsets.right + cutoutInsets.right,
bottom = barInsets.bottom
)
windowInsets
}
}

140
src/android/android/app/src/main/java/org/yuzu/yuzu_emu/utils/LogFilter.kt

@ -0,0 +1,140 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.utils
import android.content.Context
import android.net.Uri
import androidx.core.content.FileProvider
import androidx.documentfile.provider.DocumentFile
import java.io.BufferedReader
import java.io.File
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.util.regex.Pattern
object LogFilter {
/**
* Filters log content to show only warnings, errors, and critical messages
* @param context Android context
* @param inputUri URI of the input log file
* @param outputUri URI of the output filtered log file
* @return true if filtering was successful, false otherwise
*/
fun filterLogs(context: Context, inputUri: Uri, outputFile: File): Boolean {
return try {
val inputDocFile = DocumentFile.fromSingleUri(context, inputUri)
if (inputDocFile == null || !inputDocFile.exists()) {
Log.error("[LogFilter] Input file does not exist: $inputUri")
return false
}
Log.info("[LogFilter] Starting filtering from $inputUri to ${outputFile.absolutePath}")
// Define the regex pattern for warnings, errors, and criticals
val pattern = Pattern.compile(".*<(?:Warning|Error|Critical)>.*", Pattern.CASE_INSENSITIVE)
var filteredLines = 0
var totalLines = 0
context.contentResolver.openInputStream(inputUri)?.use { inputStream ->
outputFile.outputStream().use { outputStream ->
val reader = BufferedReader(InputStreamReader(inputStream))
val writer = OutputStreamWriter(outputStream)
var line: String?
while (reader.readLine().also { line = it } != null) {
totalLines++
if (pattern.matcher(line!!).matches()) {
writer.write(line)
writer.write("\n")
filteredLines++
}
}
writer.flush()
}
}
Log.info("[LogFilter] Filtered $filteredLines lines out of $totalLines total lines")
Log.info("[LogFilter] Output file size: ${outputFile.length()} bytes")
true
} catch (e: Exception) {
Log.error("[LogFilter] Error filtering logs: ${e.message}")
e.printStackTrace()
false
}
}
/**
* Gets the filtered log URI for a given log URI
* @param logUri Original log URI
* @return URI for the filtered log file
*/
fun getFilteredLogUri(logUri: Uri): Uri {
val uriString = logUri.toString()
val filteredUriString = if (uriString.endsWith(".txt")) {
uriString.replace(".txt", "_filtered.txt")
} else {
"${uriString}_filtered"
}
return Uri.parse(filteredUriString)
}
/**
* Creates a filtered log file in the app's cache directory
* @param context Android context
* @param originalUri Original log file URI
* @return Pair of File and URI, or null if failed
*/
fun createFilteredLogFile(context: Context, originalUri: Uri): Pair<File, Uri>? {
return try {
val originalFile = DocumentFile.fromSingleUri(context, originalUri)
if (originalFile == null || !originalFile.exists()) {
Log.error("[LogFilter] Original file does not exist: $originalUri")
return null
}
// Create filtered file name
val originalName = originalFile.name ?: "eden_log.txt"
val filteredName = if (originalName.endsWith(".txt")) {
originalName.replace(".txt", "_filtered.txt")
} else {
"${originalName}_filtered"
}
// Create file in app's cache directory
val cacheDir = File(context.cacheDir, "logs")
if (!cacheDir.exists()) {
cacheDir.mkdirs()
}
val filteredFile = File(cacheDir, filteredName)
// Delete existing file if it exists
if (filteredFile.exists()) {
filteredFile.delete()
}
// Create the file
filteredFile.createNewFile()
Log.info("[LogFilter] Created filtered file: ${filteredFile.absolutePath}")
// Use FileProvider to get a content URI for the file
val uri = FileProvider.getUriForFile(
context,
"${context.packageName}.provider",
filteredFile
)
Pair(filteredFile, uri)
} catch (e: Exception) {
Log.error("[LogFilter] Error creating filtered file: ${e.message}")
e.printStackTrace()
null
}
}
}

1584
src/android/android/app/src/main/res/values/strings.xml
File diff suppressed because it is too large
View File

23
src/android/android/build.gradle.kts

@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id("com.android.application") version "8.9.1" apply false
id("com.android.library") version "8.1.4" apply false
id("org.jetbrains.kotlin.android") version "1.9.20" apply false
}
tasks.register("clean").configure {
delete(rootProject.buildDir)
}
buildscript {
val agp_version by extra("8.9.1")
repositories {
google()
}
dependencies {
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:2.8.9")
}
}

19
src/android/android/gradle.properties

@ -0,0 +1,19 @@
# SPDX-FileCopyrightText: Copyright 2025 yuzu Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xms512m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
kotlin.parallel.tasks.in.project=true
# Android Gradle plugin 8.0.2
android.suppressUnsupportedCompileSdk=34
android.native.buildOutput=verbose

BIN
src/android/android/gradle/wrapper/gradle-wrapper.jar

6
src/android/android/gradle/wrapper/gradle-wrapper.properties

@ -0,0 +1,6 @@
#Sun Feb 21 18:16:59 EST 2021
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip

175
src/android/android/gradlew

@ -0,0 +1,175 @@
#!/usr/bin/env sh
# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

87
src/android/android/gradlew.bat

@ -0,0 +1,87 @@
@rem SPDX-FileCopyrightText: 2023 yuzu Emulator Project
@rem SPDX-License-Identifier: GPL-3.0-or-later
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

21
src/android/android/settings.gradle.kts

@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
}
@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
include(":app")

10
src/android/app/src/main/AndroidManifest.xml

@ -110,5 +110,15 @@ SPDX-License-Identifier: GPL-3.0-or-later
</intent-filter>
</provider>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>

30
src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/LogFilter.kt

@ -93,18 +93,41 @@ object LogFilter {
// Create file in app's cache directory
val cacheDir = File(context.cacheDir, "logs")
if (!cacheDir.exists()) {
cacheDir.mkdirs()
val created = cacheDir.mkdirs()
if (!created) {
Log.error("[LogFilter] Failed to create cache directory: ${cacheDir.absolutePath}")
return null
}
}
// Ensure the directory is writable
if (!cacheDir.canWrite()) {
Log.error("[LogFilter] Cache directory is not writable: ${cacheDir.absolutePath}")
return null
}
val filteredFile = File(cacheDir, filteredName)
// Delete existing file if it exists
if (filteredFile.exists()) {
filteredFile.delete()
val deleted = filteredFile.delete()
if (!deleted) {
Log.warning("[LogFilter] Failed to delete existing filtered file: ${filteredFile.absolutePath}")
}
}
// Create the file
filteredFile.createNewFile()
val created = filteredFile.createNewFile()
if (!created) {
Log.error("[LogFilter] Failed to create filtered file: ${filteredFile.absolutePath}")
return null
}
// Verify the file was created and is writable
if (!filteredFile.exists() || !filteredFile.canWrite()) {
Log.error("[LogFilter] Created file is not writable: ${filteredFile.absolutePath}")
return null
}
Log.info("[LogFilter] Created filtered file: ${filteredFile.absolutePath}")
@ -115,6 +138,7 @@ object LogFilter {
filteredFile
)
Log.info("[LogFilter] Generated URI: $uri")
Pair(filteredFile, uri)
} catch (e: Exception) {
Log.error("[LogFilter] Error creating filtered file: ${e.message}")

9
src/android/app/src/main/res/xml/file_paths.xml

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
SPDX-License-Identifier: GPL-3.0-or-later
-->
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="logs" path="logs/" />
<external-cache-path name="external_logs" path="logs/" />
</paths>
Loading…
Cancel
Save