mirror of
https://github.com/Lime3DS/Lime3DS.git
synced 2026-05-12 15:49:39 -06:00
Merge branch 'master' into master
This commit is contained in:
commit
b61ffe8e2c
@ -9,8 +9,14 @@ fi
|
||||
|
||||
cd src/android
|
||||
chmod +x ./gradlew
|
||||
./gradlew assembleRelease
|
||||
./gradlew bundleRelease
|
||||
|
||||
if [[ "$TARGET" == "googleplay" ]]; then
|
||||
./gradlew assembleGooglePlayRelease
|
||||
./gradlew bundleGooglePlayRelease
|
||||
else
|
||||
./gradlew assembleVanillaRelease
|
||||
./gradlew bundleVanillaRelease
|
||||
fi
|
||||
|
||||
ccache -s -v
|
||||
|
||||
|
||||
21
.github/workflows/build.yml
vendored
21
.github/workflows/build.yml
vendored
@ -221,51 +221,64 @@ jobs:
|
||||
|
||||
android:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target: ["vanilla", "googleplay"]
|
||||
env:
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
CCACHE_COMPILERCHECK: content
|
||||
CCACHE_SLOPPINESS: time_macros
|
||||
OS: android
|
||||
TARGET: universal
|
||||
TARGET: ${{ matrix.target }}
|
||||
SHOULD_RUN: ${{ (matrix.target == 'vanilla' || github.ref_type == 'tag') }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
if: ${{ env.SHOULD_RUN == 'true' }}
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Set up cache
|
||||
if: ${{ env.SHOULD_RUN == 'true' }}
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
${{ env.CCACHE_DIR }}
|
||||
key: ${{ runner.os }}-android-${{ github.sha }}
|
||||
key: ${{ runner.os }}-${{ env.OS }}-${{ matrix.target }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-android-
|
||||
${{ runner.os }}-${{ env.OS }}-${{ matrix.target }}-
|
||||
- name: Set tag name
|
||||
if: ${{ env.SHOULD_RUN == 'true' }}
|
||||
run: |
|
||||
if [[ "$GITHUB_REF_TYPE" == "tag" ]]; then
|
||||
echo "GIT_TAG_NAME=$GITHUB_REF_NAME" >> $GITHUB_ENV
|
||||
fi
|
||||
echo $GIT_TAG_NAME
|
||||
- name: Deps
|
||||
- name: Install tools
|
||||
if: ${{ env.SHOULD_RUN == 'true' }}
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install ccache apksigner -y
|
||||
- name: Update Android SDK CMake version
|
||||
if: ${{ env.SHOULD_RUN == 'true' }}
|
||||
run: |
|
||||
echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "cmake;3.30.3"
|
||||
- name: Build
|
||||
if: ${{ env.SHOULD_RUN == 'true' }}
|
||||
run: JAVA_HOME=$JAVA_HOME_17_X64 ./.ci/android.sh
|
||||
env:
|
||||
ANDROID_KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_B64 }}
|
||||
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||
ANDROID_KEYSTORE_PASS: ${{ secrets.ANDROID_KEYSTORE_PASS }}
|
||||
- name: Pack
|
||||
if: ${{ env.SHOULD_RUN == 'true' }}
|
||||
run: ../../../.ci/pack.sh
|
||||
working-directory: src/android/app
|
||||
env:
|
||||
UNPACKED: 1
|
||||
- name: Upload
|
||||
if: ${{ env.SHOULD_RUN == 'true' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.OS }}-${{ env.TARGET }}
|
||||
|
||||
2
externals/boost
vendored
2
externals/boost
vendored
@ -1 +1 @@
|
||||
Subproject commit 04f8fd64dababe6c82151792a134e3a03b395fe6
|
||||
Subproject commit 2c82bd787302398bcae990e3c9ab2b451284f4ca
|
||||
@ -164,6 +164,18 @@ android {
|
||||
|
||||
flavorDimensions.add("version")
|
||||
|
||||
productFlavors {
|
||||
register("vanilla") {
|
||||
isDefault = true
|
||||
dimension = "version"
|
||||
versionNameSuffix = "-vanilla"
|
||||
}
|
||||
register("googlePlay") {
|
||||
dimension = "version"
|
||||
versionNameSuffix = "-googleplay"
|
||||
}
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
version = "3.25.0+"
|
||||
|
||||
8
src/android/app/src/googlePlay/AndroidManifest.xml
Normal file
8
src/android/app/src/googlePlay/AndroidManifest.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- These permissions aren't allowed by Google. We asked, and they declined. -->
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:node="remove" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:node="remove" />
|
||||
|
||||
</manifest>
|
||||
@ -679,6 +679,10 @@ object NativeLibrary {
|
||||
FileUtil.getFileSize(path)
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun getBuildFlavor(): String = BuildConfig.FLAVOR
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun fileExists(path: String): Boolean =
|
||||
@ -717,11 +721,37 @@ object NativeLibrary {
|
||||
)
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun renameFile(path: String, destinationFilename: String): Boolean =
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
try {
|
||||
CitraApplication.documentsTree.renameFile(path, destinationFilename)
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
FileUtil.renameFile(path, destinationFilename)
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun updateDocumentLocation(sourcePath: String, destinationPath: String): Boolean =
|
||||
CitraApplication.documentsTree.updateDocumentLocation(sourcePath, destinationPath)
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun moveFile(filename: String, sourceDirPath: String, destinationDirPath: String): Boolean =
|
||||
if (FileUtil.isNativePath(sourceDirPath)) {
|
||||
try {
|
||||
CitraApplication.documentsTree.moveFile(filename, sourceDirPath, destinationDirPath)
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
FileUtil.moveFile(filename, sourceDirPath, destinationDirPath)
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun deleteDocument(path: String): Boolean =
|
||||
|
||||
@ -19,10 +19,13 @@ import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.ui.main.MainActivity
|
||||
import org.citra.citra_emu.utils.BuildUtil
|
||||
|
||||
class GrantMissingFilesystemPermissionFragment : DialogFragment() {
|
||||
private lateinit var mainActivity: MainActivity
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
BuildUtil.assertNotGooglePlay()
|
||||
mainActivity = requireActivity() as MainActivity
|
||||
|
||||
isCancelable = false
|
||||
@ -71,6 +74,7 @@ class GrantMissingFilesystemPermissionFragment : DialogFragment() {
|
||||
const val TAG = "GrantMissingFilesystemPermissionFragment"
|
||||
|
||||
fun newInstance(): GrantMissingFilesystemPermissionFragment {
|
||||
BuildUtil.assertNotGooglePlay()
|
||||
return GrantMissingFilesystemPermissionFragment()
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,6 +32,7 @@ import androidx.preference.PreferenceManager
|
||||
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.transition.MaterialFadeThrough
|
||||
import org.citra.citra_emu.BuildConfig
|
||||
import org.citra.citra_emu.CitraApplication
|
||||
import org.citra.citra_emu.NativeLibrary
|
||||
import org.citra.citra_emu.R
|
||||
@ -44,6 +45,7 @@ import org.citra.citra_emu.model.PageState
|
||||
import org.citra.citra_emu.model.SetupCallback
|
||||
import org.citra.citra_emu.model.SetupPage
|
||||
import org.citra.citra_emu.ui.main.MainActivity
|
||||
import org.citra.citra_emu.utils.BuildUtil
|
||||
import org.citra.citra_emu.utils.CitraDirectoryHelper
|
||||
import org.citra.citra_emu.utils.GameHelper
|
||||
import org.citra.citra_emu.utils.PermissionsHandler
|
||||
@ -145,53 +147,56 @@ class SetupFragment : Fragment() {
|
||||
false,
|
||||
0,
|
||||
pageButtons = mutableListOf<PageButton>().apply {
|
||||
add(
|
||||
PageButton(
|
||||
R.drawable.ic_folder,
|
||||
R.string.filesystem_permission,
|
||||
R.string.filesystem_permission_description,
|
||||
buttonAction = {
|
||||
pageButtonCallback = it
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
manageExternalStoragePermissionLauncher.launch(
|
||||
Intent(
|
||||
android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
|
||||
Uri.fromParts(
|
||||
"package",
|
||||
requireActivity().packageName,
|
||||
null
|
||||
@Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants")
|
||||
if (BuildConfig.FLAVOR != "googlePlay") {
|
||||
add(
|
||||
PageButton(
|
||||
R.drawable.ic_folder,
|
||||
R.string.filesystem_permission,
|
||||
R.string.filesystem_permission_description,
|
||||
buttonAction = {
|
||||
pageButtonCallback = it
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
manageExternalStoragePermissionLauncher.launch(
|
||||
Intent(
|
||||
android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
|
||||
Uri.fromParts(
|
||||
"package",
|
||||
requireActivity().packageName,
|
||||
null
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
permissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
}
|
||||
},
|
||||
buttonState = {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
if (Environment.isExternalStorageManager()) {
|
||||
ButtonState.BUTTON_ACTION_COMPLETE
|
||||
} else {
|
||||
ButtonState.BUTTON_ACTION_INCOMPLETE
|
||||
permissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
}
|
||||
} else {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
ButtonState.BUTTON_ACTION_COMPLETE
|
||||
},
|
||||
buttonState = {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
if (Environment.isExternalStorageManager()) {
|
||||
ButtonState.BUTTON_ACTION_COMPLETE
|
||||
} else {
|
||||
ButtonState.BUTTON_ACTION_INCOMPLETE
|
||||
}
|
||||
} else {
|
||||
ButtonState.BUTTON_ACTION_INCOMPLETE
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
ButtonState.BUTTON_ACTION_COMPLETE
|
||||
} else {
|
||||
ButtonState.BUTTON_ACTION_INCOMPLETE
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
isUnskippable = true,
|
||||
hasWarning = true,
|
||||
R.string.filesystem_permission_warning,
|
||||
R.string.filesystem_permission_warning_description,
|
||||
},
|
||||
isUnskippable = true,
|
||||
hasWarning = true,
|
||||
R.string.filesystem_permission_warning,
|
||||
R.string.filesystem_permission_warning_description,
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
add(
|
||||
PageButton(
|
||||
@ -279,13 +284,18 @@ class SetupFragment : Fragment() {
|
||||
NotificationManagerCompat.from(requireContext())
|
||||
.areNotificationsEnabled()
|
||||
// External Storage
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
permissionsComplete = (permissionsComplete && Environment.isExternalStorageManager())
|
||||
} else {
|
||||
permissionsComplete = (permissionsComplete && ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
) == PackageManager.PERMISSION_GRANTED)
|
||||
@Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants")
|
||||
if (BuildConfig.FLAVOR != "googlePlay") {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
permissionsComplete =
|
||||
(permissionsComplete && Environment.isExternalStorageManager())
|
||||
} else {
|
||||
permissionsComplete =
|
||||
(permissionsComplete && ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
) == PackageManager.PERMISSION_GRANTED)
|
||||
}
|
||||
}
|
||||
|
||||
if (permissionsComplete) {
|
||||
@ -542,6 +552,7 @@ class SetupFragment : Fragment() {
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
private val manageExternalStoragePermissionLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
BuildUtil.assertNotGooglePlay()
|
||||
if (Environment.isExternalStorageManager()) {
|
||||
checkForButtonState.invoke()
|
||||
return@registerForActivityResult
|
||||
|
||||
@ -40,6 +40,7 @@ import androidx.work.WorkManager
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.navigation.NavigationBarView
|
||||
import kotlinx.coroutines.launch
|
||||
import org.citra.citra_emu.BuildConfig
|
||||
import org.citra.citra_emu.NativeLibrary
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.contracts.OpenFileResultContract
|
||||
@ -198,21 +199,25 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
return
|
||||
}
|
||||
|
||||
fun requestMissingFilesystemPermission() =
|
||||
GrantMissingFilesystemPermissionFragment.newInstance()
|
||||
.show(supportFragmentManager,GrantMissingFilesystemPermissionFragment.TAG)
|
||||
@Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants")
|
||||
if (BuildConfig.FLAVOR != "googlePlay") {
|
||||
fun requestMissingFilesystemPermission() =
|
||||
GrantMissingFilesystemPermissionFragment.newInstance()
|
||||
.show(supportFragmentManager, GrantMissingFilesystemPermissionFragment.TAG)
|
||||
|
||||
if (supportFragmentManager.findFragmentByTag(GrantMissingFilesystemPermissionFragment.TAG) == null) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
if (!Environment.isExternalStorageManager()) {
|
||||
requestMissingFilesystemPermission()
|
||||
}
|
||||
} else {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
) != PackageManager.PERMISSION_GRANTED) {
|
||||
requestMissingFilesystemPermission()
|
||||
if (supportFragmentManager.findFragmentByTag(GrantMissingFilesystemPermissionFragment.TAG) == null) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
if (!Environment.isExternalStorageManager()) {
|
||||
requestMissingFilesystemPermission()
|
||||
}
|
||||
} else {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
requestMissingFilesystemPermission()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -231,10 +236,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
return
|
||||
}
|
||||
|
||||
if (supportFragmentManager.findFragmentByTag(SelectUserDirectoryDialogFragment.TAG) == null) {
|
||||
if (NativeLibrary.getUserDirectory() == "") {
|
||||
SelectUserDirectoryDialogFragment.newInstance(this)
|
||||
.show(supportFragmentManager, SelectUserDirectoryDialogFragment.TAG)
|
||||
@Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants")
|
||||
if (BuildConfig.FLAVOR != "googlePlay") {
|
||||
if (supportFragmentManager.findFragmentByTag(SelectUserDirectoryDialogFragment.TAG) == null) {
|
||||
if (NativeLibrary.getUserDirectory() == "") {
|
||||
SelectUserDirectoryDialogFragment.newInstance(this)
|
||||
.show(supportFragmentManager, SelectUserDirectoryDialogFragment.TAG)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.utils
|
||||
|
||||
import org.citra.citra_emu.BuildConfig
|
||||
|
||||
object BuildUtil {
|
||||
fun assertNotGooglePlay() {
|
||||
|
||||
@Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants")
|
||||
if (BuildConfig.FLAVOR == "googlePlay") {
|
||||
error("Non-GooglePlay code being called in GooglePlay build")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -192,6 +192,33 @@ class DocumentsTree {
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun renameFile(filepath: String, destinationFilename: String): Boolean {
|
||||
val node = resolvePath(filepath) ?: return false
|
||||
try {
|
||||
val filename = URLDecoder.decode(destinationFilename, FileUtil.DECODE_METHOD)
|
||||
val newUri = DocumentsContract.renameDocument(context.contentResolver, node.uri!!, filename)
|
||||
node.rename(filename, newUri)
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
error("[DocumentsTree]: Cannot rename file, error: " + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun moveFile(filename: String, sourceDirPath: String, destDirPath: String): Boolean {
|
||||
val sourceFileNode = resolvePath(sourceDirPath + "/" + filename) ?: return false
|
||||
val sourceDirNode = resolvePath(sourceDirPath) ?: return false
|
||||
val destDirNode = resolvePath(destDirPath) ?: return false
|
||||
try {
|
||||
val newUri = DocumentsContract.moveDocument(context.contentResolver, sourceFileNode.uri!!, sourceDirNode.uri!!, destDirNode.uri!!)
|
||||
updateDocumentLocation("$sourceDirPath/$filename", "$destDirPath/$filename")
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
error("[DocumentsTree]: Cannot move file, error: " + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun deleteDocument(filepath: String): Boolean {
|
||||
val node = resolvePath(filepath) ?: return false
|
||||
@ -210,15 +237,15 @@ class DocumentsTree {
|
||||
|
||||
@Synchronized
|
||||
fun updateDocumentLocation(sourcePath: String, destinationPath: String): Boolean {
|
||||
Log.error("Got paths: $sourcePath, $destinationPath")
|
||||
val sourceNode = resolvePath(sourcePath)
|
||||
val newName = Paths.get(destinationPath).fileName.toString()
|
||||
val parentPath = Paths.get(destinationPath).parent.toString()
|
||||
val newParent = resolvePath(parentPath)
|
||||
val newUri = (getUri(parentPath).toString() + "%2F$newName").toUri() // <- Is there a better way?
|
||||
|
||||
if (sourceNode == null || newParent == null)
|
||||
if (sourceNode == null || newParent == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
sourceNode.parent!!.removeChild(sourceNode)
|
||||
|
||||
@ -300,6 +327,15 @@ class DocumentsTree {
|
||||
loaded = true
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun rename(name: String, uri: Uri?) {
|
||||
parent ?: return
|
||||
parent!!.removeChild(this)
|
||||
this.name = name
|
||||
this.uri = uri
|
||||
parent!!.addChild(this)
|
||||
}
|
||||
|
||||
fun addChild(node: DocumentsNode) {
|
||||
children[node.name.lowercase()] = node
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import android.system.Os
|
||||
import android.util.Pair
|
||||
import androidx.core.net.toUri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import org.citra.citra_emu.CitraApplication
|
||||
import org.citra.citra_emu.model.CheapDocument
|
||||
@ -422,6 +423,32 @@ object FileUtil {
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun renameFile(path: String, destinationFilename: String): Boolean {
|
||||
try {
|
||||
val uri = Uri.parse(path)
|
||||
DocumentsContract.renameDocument(context.contentResolver, uri, destinationFilename)
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
Log.error("[FileUtil]: Cannot rename file, error: " + e.message)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun moveFile(filename: String, sourceDirUriString: String, destDirUriString: String): Boolean {
|
||||
try {
|
||||
val sourceFileUri = ("$sourceDirUriString%2F$filename").toUri()
|
||||
val sourceDirUri = sourceDirUriString.toUri()
|
||||
val destDirUri = destDirUriString.toUri()
|
||||
DocumentsContract.moveDocument(context.contentResolver, sourceFileUri, sourceDirUri, destDirUri)
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
Log.error("[FileUtil]: Cannot move file, error: " + e.message)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun deleteDocument(path: String): Boolean {
|
||||
try {
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
package org.citra.citra_emu.utils
|
||||
|
||||
import org.citra.citra_emu.utils.BuildUtil
|
||||
import java.io.File
|
||||
|
||||
object RemovableStorageHelper {
|
||||
@ -12,6 +13,8 @@ object RemovableStorageHelper {
|
||||
// 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? {
|
||||
BuildUtil.assertNotGooglePlay()
|
||||
|
||||
var pathFile: File
|
||||
|
||||
pathFile = File("/mnt/media_rw/$idString");
|
||||
|
||||
@ -3,7 +3,11 @@
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#ifdef ANDROID
|
||||
#include <boost/uuid/uuid_generators.hpp>
|
||||
#include <boost/uuid/uuid_io.hpp>
|
||||
#include "common/android_storage.h"
|
||||
#include "common/file_util.h"
|
||||
#include "common/logging/log.h"
|
||||
|
||||
namespace AndroidStorage {
|
||||
JNIEnv* GetEnvForThread() {
|
||||
@ -80,8 +84,9 @@ void CleanupJNI() {
|
||||
}
|
||||
|
||||
bool CreateFile(const std::string& directory, const std::string& filename) {
|
||||
if (create_file == nullptr)
|
||||
if (create_file == nullptr) {
|
||||
return false;
|
||||
}
|
||||
auto env = GetEnvForThread();
|
||||
jstring j_directory = env->NewStringUTF(directory.c_str());
|
||||
jstring j_filename = env->NewStringUTF(filename.c_str());
|
||||
@ -89,8 +94,9 @@ bool CreateFile(const std::string& directory, const std::string& filename) {
|
||||
}
|
||||
|
||||
bool CreateDir(const std::string& directory, const std::string& filename) {
|
||||
if (create_dir == nullptr)
|
||||
if (create_dir == nullptr) {
|
||||
return false;
|
||||
}
|
||||
auto env = GetEnvForThread();
|
||||
jstring j_directory = env->NewStringUTF(directory.c_str());
|
||||
jstring j_directory_name = env->NewStringUTF(filename.c_str());
|
||||
@ -98,8 +104,9 @@ bool CreateDir(const std::string& directory, const std::string& filename) {
|
||||
}
|
||||
|
||||
int OpenContentUri(const std::string& filepath, AndroidOpenMode openmode) {
|
||||
if (open_content_uri == nullptr)
|
||||
if (open_content_uri == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* mode = "";
|
||||
switch (openmode) {
|
||||
@ -135,8 +142,9 @@ int OpenContentUri(const std::string& filepath, AndroidOpenMode openmode) {
|
||||
|
||||
std::vector<std::string> GetFilesName(const std::string& filepath) {
|
||||
auto vector = std::vector<std::string>();
|
||||
if (get_files_name == nullptr)
|
||||
if (get_files_name == nullptr) {
|
||||
return vector;
|
||||
}
|
||||
auto env = GetEnvForThread();
|
||||
jstring j_filepath = env->NewStringUTF(filepath.c_str());
|
||||
auto j_object =
|
||||
@ -151,9 +159,10 @@ std::vector<std::string> GetFilesName(const std::string& filepath) {
|
||||
}
|
||||
|
||||
std::optional<std::string> GetUserDirectory() {
|
||||
if (get_user_directory == nullptr)
|
||||
if (get_user_directory == nullptr) {
|
||||
throw std::runtime_error(
|
||||
"Unable to locate user directory: Function with ID 'get_user_directory' is missing");
|
||||
}
|
||||
auto env = GetEnvForThread();
|
||||
auto j_user_directory =
|
||||
(jstring)(env->CallStaticObjectMethod(native_library, get_user_directory, nullptr));
|
||||
@ -164,10 +173,22 @@ std::optional<std::string> GetUserDirectory() {
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string GetBuildFlavor() {
|
||||
if (get_build_flavor == nullptr) {
|
||||
throw std::runtime_error(
|
||||
"Unable get build flavor: Function with ID 'get_build_flavor' is missing");
|
||||
}
|
||||
auto env = GetEnvForThread();
|
||||
const auto jflavor =
|
||||
(jstring)(env->CallStaticObjectMethod(native_library, get_build_flavor, nullptr));
|
||||
return env->GetStringUTFChars(jflavor, nullptr);
|
||||
}
|
||||
|
||||
bool CopyFile(const std::string& source, const std::string& destination_path,
|
||||
const std::string& destination_filename) {
|
||||
if (copy_file == nullptr)
|
||||
if (copy_file == nullptr) {
|
||||
return false;
|
||||
}
|
||||
auto env = GetEnvForThread();
|
||||
jstring j_source_path = env->NewStringUTF(source.c_str());
|
||||
jstring j_destination_path = env->NewStringUTF(destination_path.c_str());
|
||||
@ -176,9 +197,26 @@ bool CopyFile(const std::string& source, const std::string& destination_path,
|
||||
j_destination_path, j_destination_filename);
|
||||
}
|
||||
|
||||
bool UpdateDocumentLocation(const std::string& source_path, const std::string& destination_path) {
|
||||
if (update_document_location == nullptr)
|
||||
bool RenameFile(const std::string& source, const std::string& filename) {
|
||||
if (rename_file == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (std::string(FileUtil::GetFilename(source)) ==
|
||||
std::string(FileUtil::GetFilename(filename))) {
|
||||
// TODO: Should this be treated as a success or failure?
|
||||
return false;
|
||||
}
|
||||
auto env = GetEnvForThread();
|
||||
jstring j_source_path = env->NewStringUTF(source.c_str());
|
||||
jstring j_destination_path = env->NewStringUTF(filename.c_str());
|
||||
return env->CallStaticBooleanMethod(native_library, rename_file, j_source_path,
|
||||
j_destination_path);
|
||||
}
|
||||
|
||||
bool UpdateDocumentLocation(const std::string& source_path, const std::string& destination_path) {
|
||||
if (update_document_location == nullptr) {
|
||||
return false;
|
||||
}
|
||||
auto env = GetEnvForThread();
|
||||
jstring j_source_path = env->NewStringUTF(source_path.c_str());
|
||||
jstring j_destination_path = env->NewStringUTF(destination_path.c_str());
|
||||
@ -186,6 +224,82 @@ bool UpdateDocumentLocation(const std::string& source_path, const std::string& d
|
||||
j_destination_path);
|
||||
}
|
||||
|
||||
bool MoveFile(const std::string& filename, const std::string& source_dir_path,
|
||||
const std::string& destination_dir_path) {
|
||||
if (move_file == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (source_dir_path == destination_dir_path) {
|
||||
// TODO: Should this be treated as a success or failure?
|
||||
return false;
|
||||
}
|
||||
auto env = GetEnvForThread();
|
||||
jstring j_filename = env->NewStringUTF(filename.c_str());
|
||||
jstring j_source_dir_path = env->NewStringUTF(source_dir_path.c_str());
|
||||
jstring j_destination_dir_path = env->NewStringUTF(destination_dir_path.c_str());
|
||||
return env->CallStaticBooleanMethod(native_library, move_file, j_filename, j_source_dir_path,
|
||||
j_destination_dir_path);
|
||||
}
|
||||
|
||||
bool MoveAndRenameFile(const std::string& src_full_path, const std::string& dest_full_path) {
|
||||
if (src_full_path == dest_full_path) {
|
||||
// TODO: Should this be treated as a success or failure?
|
||||
return false;
|
||||
}
|
||||
const auto src_filename = std::string(FileUtil::GetFilename(src_full_path));
|
||||
const auto src_parent_path = std::string(FileUtil::GetParentPath(src_full_path));
|
||||
const auto dest_filename = std::string(FileUtil::GetFilename(dest_full_path));
|
||||
const auto dest_parent_path = std::string(FileUtil::GetParentPath(dest_full_path));
|
||||
bool result;
|
||||
|
||||
const std::string tmp_path = "/tmp";
|
||||
AndroidStorage::CreateDir("/", "tmp");
|
||||
|
||||
// If a simultaneous move and rename are not necessary, use individual methods
|
||||
// TODO: Uncomment this code for 2123.4 RC to allow stress testing of move + rename process in
|
||||
// beta
|
||||
/*
|
||||
if (src_filename == dest_filename || src_parent_path == dest_parent_path) {
|
||||
if (src_filename != dest_filename) {
|
||||
return AndroidStorage::RenameFile(src_full_path, dest_filename);
|
||||
} else if (src_parent_path != dest_parent_path) {
|
||||
return AndroidStorage::MoveFile(src_filename, src_parent_path, dest_parent_path);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// Step 1: Create directory named after UUID inside /tmp to house the moved file.
|
||||
// This prevents clashes if files with the same name are moved simultaneously.
|
||||
const auto uuid = boost::uuids::to_string(boost::uuids::time_generator_v7()());
|
||||
const auto allocated_tmp_path = tmp_path + "/" + uuid;
|
||||
AndroidStorage::CreateDir(tmp_path, uuid);
|
||||
|
||||
// Step 2: Attempt to move to allocated temporary directory.
|
||||
// If this step fails, skip everything except the cleanup.
|
||||
result = AndroidStorage::MoveFile(src_filename, src_parent_path, allocated_tmp_path);
|
||||
if (result == true) {
|
||||
// Step 3: Rename to desired file name.
|
||||
if (src_filename != dest_filename) { // TODO: Remove this if statement in 2123.4 RC, keeping
|
||||
// the RenameFile call
|
||||
AndroidStorage::RenameFile((allocated_tmp_path + "/" + src_filename), dest_filename);
|
||||
}
|
||||
|
||||
// Step 4: If a file with the desired name in the destination exists, remove it.
|
||||
AndroidStorage::DeleteDocument(dest_full_path);
|
||||
|
||||
// Step 5: Attempt to move file to desired location.
|
||||
// If this step fails, move the file back to where it came from.
|
||||
result = AndroidStorage::MoveFile(dest_filename, allocated_tmp_path, dest_parent_path);
|
||||
if (result == false) {
|
||||
AndroidStorage::MoveAndRenameFile((allocated_tmp_path + "/" + dest_filename),
|
||||
src_full_path);
|
||||
}
|
||||
}
|
||||
// Step 6: Clean up the allocated temp directory.
|
||||
AndroidStorage::DeleteDocument(allocated_tmp_path);
|
||||
return result;
|
||||
}
|
||||
|
||||
#define FR(FunctionName, ReturnValue, JMethodID, Caller, JMethodName, Signature) \
|
||||
F(FunctionName, ReturnValue, JMethodID, Caller)
|
||||
#define F(FunctionName, ReturnValue, JMethodID, Caller) \
|
||||
|
||||
@ -25,10 +25,17 @@
|
||||
(const std::string& source, const std::string& destination_path, \
|
||||
const std::string& destination_filename), \
|
||||
copy_file, "copyFile", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Z") \
|
||||
V(RenameFile, bool, (const std::string& source, const std::string& filename), rename_file, \
|
||||
"renameFile", "(Ljava/lang/String;Ljava/lang/String;)Z") \
|
||||
V(UpdateDocumentLocation, bool, \
|
||||
(const std::string& source_path, const std::string& destination_path), \
|
||||
update_document_location, "updateDocumentLocation", \
|
||||
"(Ljava/lang/String;Ljava/lang/String;)Z")
|
||||
"(Ljava/lang/String;Ljava/lang/String;)Z") \
|
||||
V(GetBuildFlavor, std::string, (), get_build_flavor, "getBuildFlavor", "()Ljava/lang/String;") \
|
||||
V(MoveFile, bool, \
|
||||
(const std::string& filename, const std::string& source_dir_path, \
|
||||
const std::string& destination_dir_path), \
|
||||
move_file, "moveFile", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Z")
|
||||
#define ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(V) \
|
||||
V(IsDirectory, bool, is_directory, CallStaticBooleanMethod, "isDirectory", \
|
||||
"(Ljava/lang/String;)Z") \
|
||||
@ -48,6 +55,7 @@ ANDROID_STORAGE_FUNCTIONS(FS)
|
||||
#undef F
|
||||
#undef FS
|
||||
#undef FR
|
||||
bool MoveAndRenameFile(const std::string& src_full_path, const std::string& dest_full_path);
|
||||
// Reference:
|
||||
// https://developer.android.com/reference/android/os/ParcelFileDescriptor#parseMode(java.lang.String)
|
||||
enum class AndroidOpenMode {
|
||||
|
||||
@ -304,25 +304,31 @@ bool DeleteDir(const std::string& filename) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Rename(const std::string& srcFilename, const std::string& destFilename) {
|
||||
LOG_TRACE(Common_Filesystem, "{} --> {}", srcFilename, destFilename);
|
||||
bool Rename(const std::string& srcFullPath, const std::string& destFullPath) {
|
||||
LOG_TRACE(Common_Filesystem, "{} --> {}", srcPath, destFullPath);
|
||||
#ifdef _WIN32
|
||||
if (_wrename(Common::UTF8ToUTF16W(srcFilename).c_str(),
|
||||
Common::UTF8ToUTF16W(destFilename).c_str()) == 0)
|
||||
if (_wrename(Common::UTF8ToUTF16W(srcFullPath).c_str(),
|
||||
Common::UTF8ToUTF16W(destFullPath).c_str()) == 0)
|
||||
return true;
|
||||
#elif ANDROID
|
||||
std::optional<std::string> userDirLocation = AndroidStorage::GetUserDirectory();
|
||||
if (userDirLocation && rename((*userDirLocation + srcFilename).c_str(),
|
||||
(*userDirLocation + destFilename).c_str()) == 0) {
|
||||
AndroidStorage::UpdateDocumentLocation(srcFilename, destFilename);
|
||||
// ^ TODO: This shouldn't fail, but what should we do if it somehow does?
|
||||
return true;
|
||||
// srcFullPath and destFullPath are relative to the user directory
|
||||
if (AndroidStorage::GetBuildFlavor() == "googlePlay") {
|
||||
if (AndroidStorage::MoveAndRenameFile(srcFullPath, destFullPath))
|
||||
return true;
|
||||
} else {
|
||||
std::optional<std::string> userDirLocation = AndroidStorage::GetUserDirectory();
|
||||
if (userDirLocation && rename((*userDirLocation + srcFullPath).c_str(),
|
||||
(*userDirLocation + destFullPath).c_str()) == 0) {
|
||||
AndroidStorage::UpdateDocumentLocation(srcFullPath, destFullPath);
|
||||
// ^ TODO: This shouldn't fail, but what should we do if it somehow does?
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#else
|
||||
if (rename(srcFilename.c_str(), destFilename.c_str()) == 0)
|
||||
if (rename(srcFullPath.c_str(), destFullPath.c_str()) == 0)
|
||||
return true;
|
||||
#endif
|
||||
LOG_ERROR(Common_Filesystem, "failed {} --> {}: {}", srcFilename, destFilename,
|
||||
LOG_ERROR(Common_Filesystem, "failed {} --> {}: {}", srcFullPath, destFullPath,
|
||||
GetLastErrorMsg());
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -135,13 +135,13 @@ bool Delete(const std::string& filename);
|
||||
// Deletes a directory filename, returns true on success
|
||||
bool DeleteDir(const std::string& filename);
|
||||
|
||||
// renames file srcFilename to destFilename, returns true on success
|
||||
bool Rename(const std::string& srcFilename, const std::string& destFilename);
|
||||
// Renames file srcFullPath to destFullPath, returns true on success
|
||||
bool Rename(const std::string& srcFullPath, const std::string& destFullPath);
|
||||
|
||||
// copies file srcFilename to destFilename, returns true on success
|
||||
// Copies file srcFilename to destFilename, returns true on success
|
||||
bool Copy(const std::string& srcFilename, const std::string& destFilename);
|
||||
|
||||
// creates an empty file filename, returns true on success
|
||||
// Creates an empty file filename, returns true on success
|
||||
bool CreateEmptyFile(const std::string& filename);
|
||||
|
||||
/**
|
||||
|
||||
Loading…
Reference in New Issue
Block a user