Browse Source
[qt, android] Implement custom save path setting and migration + Implement custom path settings for Android (#3154)
[qt, android] Implement custom save path setting and migration + Implement custom path settings for Android (#3154)
Needs careful review and especially testing Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3154 Reviewed-by: DraVee <dravee@eden-emu.dev> Reviewed-by: MaranBr <maranbr@eden-emu.dev> Co-authored-by: kleidis <kleidis1@protonmail.com> Co-committed-by: kleidis <kleidis1@protonmail.com>pull/3209/head
committed by
crueter
No known key found for this signature in database
GPG Key ID: 425ACD2D4830EBC6
28 changed files with 867 additions and 24 deletions
-
3src/android/app/src/main/AndroidManifest.xml
-
1src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
-
40src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/PathSetting.kt
-
1src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
-
16src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
-
238src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt
-
50src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
-
22src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsViewModel.kt
-
64src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/PathViewHolder.kt
-
13src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
-
7src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt
-
4src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
-
21src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt
-
97src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PathUtil.kt
-
39src/android/app/src/main/jni/android_config.cpp
-
6src/android/app/src/main/jni/native.cpp
-
36src/android/app/src/main/jni/native_config.cpp
-
28src/android/app/src/main/res/values/strings.xml
-
1src/common/fs/path_util.cpp
-
1src/common/fs/path_util.h
-
1src/common/settings.cpp
-
9src/core/hle/service/filesystem/filesystem.cpp
-
17src/frontend_common/config.cpp
-
3src/frontend_common/data_manager.cpp
-
136src/yuzu/configuration/configure_filesystem.cpp
-
6src/yuzu/configuration/configure_filesystem.h
-
17src/yuzu/configuration/configure_filesystem.ui
-
14src/yuzu/main_window.cpp
@ -0,0 +1,40 @@ |
|||||
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project |
||||
|
// SPDX-License-Identifier: GPL-3.0-or-later |
||||
|
|
||||
|
package org.yuzu.yuzu_emu.features.settings.model.view |
||||
|
|
||||
|
import androidx.annotation.DrawableRes |
||||
|
import androidx.annotation.StringRes |
||||
|
|
||||
|
class PathSetting( |
||||
|
@StringRes titleId: Int = 0, |
||||
|
titleString: String = "", |
||||
|
@StringRes descriptionId: Int = 0, |
||||
|
descriptionString: String = "", |
||||
|
@DrawableRes val iconId: Int = 0, |
||||
|
val pathType: PathType, |
||||
|
val defaultPathGetter: () -> String, |
||||
|
val currentPathGetter: () -> String, |
||||
|
val pathSetter: (String) -> Unit |
||||
|
) : SettingsItem(emptySetting, titleId, titleString, descriptionId, descriptionString) { |
||||
|
|
||||
|
override val type = TYPE_PATH |
||||
|
|
||||
|
enum class PathType { |
||||
|
SAVE_DATA, |
||||
|
NAND, |
||||
|
SDMC |
||||
|
} |
||||
|
|
||||
|
fun getCurrentPath(): String = currentPathGetter() |
||||
|
|
||||
|
fun getDefaultPath(): String = defaultPathGetter() |
||||
|
|
||||
|
fun setPath(path: String) = pathSetter(path) |
||||
|
|
||||
|
fun isUsingDefaultPath(): Boolean = getCurrentPath() == getDefaultPath() |
||||
|
|
||||
|
companion object { |
||||
|
const val TYPE_PATH = 14 |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,64 @@ |
|||||
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project |
||||
|
// SPDX-License-Identifier: GPL-3.0-or-later |
||||
|
|
||||
|
package org.yuzu.yuzu_emu.features.settings.ui.viewholder |
||||
|
|
||||
|
import android.view.View |
||||
|
import androidx.core.content.res.ResourcesCompat |
||||
|
import org.yuzu.yuzu_emu.R |
||||
|
import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding |
||||
|
import org.yuzu.yuzu_emu.features.settings.model.view.PathSetting |
||||
|
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem |
||||
|
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter |
||||
|
import org.yuzu.yuzu_emu.utils.PathUtil |
||||
|
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible |
||||
|
|
||||
|
class PathViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : |
||||
|
SettingViewHolder(binding.root, adapter) { |
||||
|
|
||||
|
private lateinit var setting: PathSetting |
||||
|
|
||||
|
override fun bind(item: SettingsItem) { |
||||
|
setting = item as PathSetting |
||||
|
binding.icon.setVisible(setting.iconId != 0) |
||||
|
if (setting.iconId != 0) { |
||||
|
binding.icon.setImageDrawable( |
||||
|
ResourcesCompat.getDrawable( |
||||
|
binding.icon.resources, |
||||
|
setting.iconId, |
||||
|
binding.icon.context.theme |
||||
|
) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
binding.textSettingName.text = setting.title |
||||
|
binding.textSettingDescription.setVisible(setting.description.isNotEmpty()) |
||||
|
binding.textSettingDescription.text = setting.description |
||||
|
|
||||
|
val currentPath = setting.getCurrentPath() |
||||
|
val displayPath = PathUtil.truncatePathForDisplay(currentPath) |
||||
|
|
||||
|
binding.textSettingValue.setVisible(true) |
||||
|
binding.textSettingValue.text = if (setting.isUsingDefaultPath()) { |
||||
|
binding.root.context.getString(R.string.default_string) |
||||
|
} else { |
||||
|
displayPath |
||||
|
} |
||||
|
|
||||
|
binding.buttonClear.setVisible(!setting.isUsingDefaultPath()) |
||||
|
binding.buttonClear.text = binding.root.context.getString(R.string.reset_to_default) |
||||
|
binding.buttonClear.setOnClickListener { |
||||
|
adapter.onPathReset(setting, bindingAdapterPosition) |
||||
|
} |
||||
|
|
||||
|
setStyle(true, binding) |
||||
|
} |
||||
|
|
||||
|
override fun onClick(clicked: View) { |
||||
|
adapter.onPathClick(setting, bindingAdapterPosition) |
||||
|
} |
||||
|
|
||||
|
override fun onLongClick(clicked: View): Boolean { |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,97 @@ |
|||||
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project |
||||
|
// SPDX-License-Identifier: GPL-3.0-or-later |
||||
|
|
||||
|
package org.yuzu.yuzu_emu.utils |
||||
|
|
||||
|
import android.net.Uri |
||||
|
import android.provider.DocumentsContract |
||||
|
import java.io.File |
||||
|
|
||||
|
object PathUtil { |
||||
|
|
||||
|
/** |
||||
|
* Converts a content:// URI from the Storage Access Framework to a real filesystem path. |
||||
|
*/ |
||||
|
fun getPathFromUri(uri: Uri): String? { |
||||
|
val docId = try { |
||||
|
DocumentsContract.getTreeDocumentId(uri) |
||||
|
} catch (_: Exception) { |
||||
|
return null |
||||
|
} |
||||
|
|
||||
|
if (docId.startsWith("primary:")) { |
||||
|
val relativePath = docId.substringAfter(":") |
||||
|
val primaryStoragePath = android.os.Environment.getExternalStorageDirectory().absolutePath |
||||
|
return "$primaryStoragePath/$relativePath" |
||||
|
} |
||||
|
|
||||
|
// external SD cards and other volumes) |
||||
|
val storageIdString = docId.substringBefore(":") |
||||
|
val removablePath = getRemovableStoragePath(storageIdString) |
||||
|
if (removablePath != null) { |
||||
|
return "$removablePath/${docId.substringAfter(":")}" |
||||
|
} |
||||
|
|
||||
|
return null |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Validates that a path is a valid, writable directory. |
||||
|
* Creates the directory if it doesn't exist. |
||||
|
*/ |
||||
|
fun validateDirectory(path: String): Boolean { |
||||
|
val dir = File(path) |
||||
|
|
||||
|
if (!dir.exists()) { |
||||
|
if (!dir.mkdirs()) { |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return dir.isDirectory && dir.canWrite() |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Copies a directory recursively from source to destination. |
||||
|
*/ |
||||
|
fun copyDirectory(source: File, destination: File, overwrite: Boolean = true): Boolean { |
||||
|
return try { |
||||
|
source.copyRecursively(destination, overwrite) |
||||
|
true |
||||
|
} catch (_: Exception) { |
||||
|
false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Checks if a directory has any content. |
||||
|
*/ |
||||
|
fun hasContent(path: String): Boolean { |
||||
|
val dir = File(path) |
||||
|
return dir.exists() && dir.listFiles()?.isNotEmpty() == true |
||||
|
} |
||||
|
|
||||
|
|
||||
|
fun truncatePathForDisplay(path: String, maxLength: Int = 40): String { |
||||
|
return if (path.length > maxLength) { |
||||
|
"...${path.takeLast(maxLength - 3)}" |
||||
|
} else { |
||||
|
path |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// This really shouldn't be necessary, but the Android API seemingly |
||||
|
// doesn't have a way of doing this? |
||||
|
// Apparently, on certain devices the mount location can vary, so add |
||||
|
// extra cases here if we discover any new ones. |
||||
|
fun getRemovableStoragePath(idString: String): String? { |
||||
|
var pathFile: File |
||||
|
|
||||
|
pathFile = File("/mnt/media_rw/$idString"); |
||||
|
if (pathFile.exists()) { |
||||
|
return pathFile.absolutePath |
||||
|
} |
||||
|
|
||||
|
return null |
||||
|
} |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue