mirror of
https://github.com/Lime3DS/Lime3DS.git
synced 2026-05-12 15:49:39 -06:00
android: Add support for compressing and decompressing files (#1458)
This commit is contained in:
parent
40d512cafd
commit
f31bd1bcdd
@ -28,6 +28,7 @@ import org.citra.citra_emu.activities.EmulationActivity
|
||||
import org.citra.citra_emu.utils.FileUtil
|
||||
import org.citra.citra_emu.utils.Log
|
||||
import org.citra.citra_emu.utils.RemovableStorageHelper
|
||||
import org.citra.citra_emu.viewmodel.CompressProgressDialogViewModel
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.Date
|
||||
|
||||
@ -600,6 +601,47 @@ object NativeLibrary {
|
||||
*/
|
||||
external fun logDeviceInfo()
|
||||
|
||||
enum class CompressStatus(val value: Int) {
|
||||
SUCCESS(0),
|
||||
COMPRESS_UNSUPPORTED(1),
|
||||
COMPRESS_ALREADY_COMPRESSED(2),
|
||||
COMPRESS_FAILED(3),
|
||||
DECOMPRESS_UNSUPPORTED(4),
|
||||
DECOMPRESS_NOT_COMPRESSED(5),
|
||||
DECOMPRESS_FAILED(6),
|
||||
INSTALLED_APPLICATION(7);
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: Int): CompressStatus =
|
||||
CompressStatus.entries.first { it.value == value }
|
||||
}
|
||||
}
|
||||
|
||||
// Compression / Decompression
|
||||
private external fun compressFileNative(inputPath: String?, outputPath: String): Int
|
||||
|
||||
fun compressFile(inputPath: String?, outputPath: String): CompressStatus {
|
||||
return CompressStatus.fromValue(
|
||||
compressFileNative(inputPath, outputPath)
|
||||
)
|
||||
}
|
||||
|
||||
private external fun decompressFileNative(inputPath: String?, outputPath: String): Int
|
||||
|
||||
fun decompressFile(inputPath: String?, outputPath: String): CompressStatus {
|
||||
return CompressStatus.fromValue(
|
||||
decompressFileNative(inputPath, outputPath)
|
||||
)
|
||||
}
|
||||
|
||||
external fun getRecommendedExtension(inputPath: String?, shouldCompress: Boolean): String
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun onCompressProgress(total: Long, current: Long) {
|
||||
CompressProgressDialogViewModel.update(total, current)
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun createFile(directory: String, filename: String): Boolean =
|
||||
|
||||
@ -57,7 +57,12 @@ import org.citra.citra_emu.utils.FileUtil
|
||||
import org.citra.citra_emu.utils.GameIconUtils
|
||||
import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||
|
||||
class GameAdapter(private val activity: AppCompatActivity, private val inflater: LayoutInflater, private val openImageLauncher: ActivityResultLauncher<String>?) :
|
||||
class GameAdapter(
|
||||
private val activity: AppCompatActivity,
|
||||
private val inflater: LayoutInflater,
|
||||
private val openImageLauncher: ActivityResultLauncher<String>?,
|
||||
private val onRequestCompressOrDecompress: ((inputPath: String, suggestedName: String, shouldCompress: Boolean) -> Unit)? = null
|
||||
) :
|
||||
ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()),
|
||||
View.OnClickListener, View.OnLongClickListener {
|
||||
private var lastClickTime = 0L
|
||||
@ -441,6 +446,27 @@ class GameAdapter(private val activity: AppCompatActivity, private val inflater:
|
||||
bottomSheetDialog.dismiss()
|
||||
}
|
||||
|
||||
val compressDecompressButton = bottomSheetView.findViewById<MaterialButton>(R.id.compress_decompress)
|
||||
if (game.isInstalled) {
|
||||
compressDecompressButton.setOnClickListener {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.compress_decompress_installed_app),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
compressDecompressButton.alpha = 0.38f
|
||||
} else {
|
||||
compressDecompressButton.setOnClickListener {
|
||||
val shouldCompress = !game.isCompressed
|
||||
val recommendedExt = NativeLibrary.getRecommendedExtension(holder.game.path, shouldCompress)
|
||||
val baseName = holder.game.filename.substringBeforeLast('.')
|
||||
onRequestCompressOrDecompress?.invoke(holder.game.path, "$baseName.$recommendedExt", shouldCompress)
|
||||
bottomSheetDialog.dismiss()
|
||||
}
|
||||
}
|
||||
compressDecompressButton.text = context.getString(if (!game.isCompressed) R.string.compress else R.string.decompress)
|
||||
|
||||
bottomSheetView.findViewById<MaterialButton>(R.id.menu_button_open).setOnClickListener {
|
||||
showOpenContextMenu(it, game)
|
||||
}
|
||||
|
||||
@ -0,0 +1,89 @@
|
||||
// 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.fragments
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.ProgressBar
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.launch
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.viewmodel.CompressProgressDialogViewModel
|
||||
import org.citra.citra_emu.NativeLibrary
|
||||
|
||||
class CompressProgressDialogFragment : DialogFragment() {
|
||||
private lateinit var progressBar: ProgressBar
|
||||
private var outputPath: String? = null
|
||||
private var isCompressing: Boolean = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
arguments?.let {
|
||||
isCompressing = it.getBoolean(ARG_IS_COMPRESSING, true)
|
||||
outputPath = it.getString(ARG_OUTPUT_PATH)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val view = layoutInflater.inflate(R.layout.dialog_compress_progress, null)
|
||||
progressBar = view.findViewById(R.id.compress_progress)
|
||||
val label = view.findViewById<android.widget.TextView>(R.id.compress_label)
|
||||
label.text = if (isCompressing) getString(R.string.compressing) else getString(R.string.decompressing)
|
||||
|
||||
isCancelable = false
|
||||
progressBar.isIndeterminate = true
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
combine(CompressProgressDialogViewModel.total, CompressProgressDialogViewModel.progress) { total, progress ->
|
||||
total to progress
|
||||
}.collectLatest { (total, progress) ->
|
||||
if (total <= 0) {
|
||||
progressBar.isIndeterminate = true
|
||||
label.visibility = View.GONE
|
||||
} else {
|
||||
progressBar.isIndeterminate = false
|
||||
label.visibility = View.VISIBLE
|
||||
progressBar.max = total
|
||||
progressBar.setProgress(progress, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val builder = MaterialAlertDialogBuilder(requireContext())
|
||||
.setView(view)
|
||||
.setCancelable(false)
|
||||
.setNegativeButton(android.R.string.cancel) { _: android.content.DialogInterface, _: Int ->
|
||||
outputPath?.let { path ->
|
||||
NativeLibrary.deleteDocument(path)
|
||||
}
|
||||
}
|
||||
|
||||
return builder.show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "CompressProgressDialog"
|
||||
private const val ARG_IS_COMPRESSING = "isCompressing"
|
||||
private const val ARG_OUTPUT_PATH = "outputPath"
|
||||
|
||||
fun newInstance(isCompressing: Boolean, outputPath: String?): CompressProgressDialogFragment {
|
||||
val frag = CompressProgressDialogFragment()
|
||||
val args = Bundle()
|
||||
args.putBoolean(ARG_IS_COMPRESSING, isCompressing)
|
||||
args.putString(ARG_OUTPUT_PATH, outputPath)
|
||||
frag.arguments = args
|
||||
return frag
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -30,14 +30,17 @@ import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.transition.MaterialFadeThrough
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import org.citra.citra_emu.CitraApplication
|
||||
import org.citra.citra_emu.NativeLibrary
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.adapters.GameAdapter
|
||||
import org.citra.citra_emu.databinding.FragmentGamesBinding
|
||||
import org.citra.citra_emu.features.settings.model.Settings
|
||||
import org.citra.citra_emu.model.Game
|
||||
import org.citra.citra_emu.viewmodel.CompressProgressDialogViewModel
|
||||
import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||
import org.citra.citra_emu.viewmodel.HomeViewModel
|
||||
|
||||
@ -56,6 +59,58 @@ class GamesFragment : Fragment() {
|
||||
gameAdapter.handleShortcutImageResult(uri)
|
||||
}
|
||||
|
||||
private var shouldCompress: Boolean = true
|
||||
private var pendingCompressInvocation: String? = null
|
||||
|
||||
companion object {
|
||||
fun doCompression(fragment: Fragment, gamesViewModel: GamesViewModel, inputPath: String?, outputUri: Uri?, shouldCompress: Boolean) {
|
||||
if (outputUri != null) {
|
||||
CompressProgressDialogViewModel.reset()
|
||||
val dialog = CompressProgressDialogFragment.newInstance(shouldCompress, outputUri.toString())
|
||||
dialog.showNow(
|
||||
fragment.requireActivity().supportFragmentManager,
|
||||
CompressProgressDialogFragment.TAG
|
||||
)
|
||||
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
val status = if (shouldCompress) {
|
||||
NativeLibrary.compressFile(inputPath, outputUri.toString())
|
||||
} else {
|
||||
NativeLibrary.decompressFile(inputPath, outputUri.toString())
|
||||
}
|
||||
|
||||
fragment.requireActivity().runOnUiThread {
|
||||
dialog.dismiss()
|
||||
val resId = when (status) {
|
||||
NativeLibrary.CompressStatus.SUCCESS -> if (shouldCompress) R.string.compress_success else R.string.decompress_success
|
||||
NativeLibrary.CompressStatus.COMPRESS_UNSUPPORTED -> R.string.compress_unsupported
|
||||
NativeLibrary.CompressStatus.COMPRESS_ALREADY_COMPRESSED -> R.string.compress_already
|
||||
NativeLibrary.CompressStatus.COMPRESS_FAILED -> R.string.compress_failed
|
||||
NativeLibrary.CompressStatus.DECOMPRESS_UNSUPPORTED -> R.string.decompress_unsupported
|
||||
NativeLibrary.CompressStatus.DECOMPRESS_NOT_COMPRESSED -> R.string.decompress_not_compressed
|
||||
NativeLibrary.CompressStatus.DECOMPRESS_FAILED -> R.string.decompress_failed
|
||||
NativeLibrary.CompressStatus.INSTALLED_APPLICATION -> R.string.compress_decompress_installed_app
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder(fragment.requireContext())
|
||||
.setMessage(fragment.getString(resId))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
|
||||
gamesViewModel.reloadGames(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val onCompressDecompressLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.CreateDocument("application/octet-stream")
|
||||
) { uri: Uri? ->
|
||||
doCompression(this, gamesViewModel, pendingCompressInvocation, uri, shouldCompress)
|
||||
pendingCompressInvocation = null
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enterTransition = MaterialFadeThrough()
|
||||
@ -81,7 +136,12 @@ class GamesFragment : Fragment() {
|
||||
gameAdapter = GameAdapter(
|
||||
requireActivity() as AppCompatActivity,
|
||||
inflater,
|
||||
openImageLauncher
|
||||
openImageLauncher,
|
||||
onRequestCompressOrDecompress = { inputPath, suggestedName, shouldCompress ->
|
||||
pendingCompressInvocation = inputPath
|
||||
onCompressDecompressLauncher.launch(suggestedName)
|
||||
this.shouldCompress = shouldCompress
|
||||
}
|
||||
)
|
||||
|
||||
binding.gridGames.apply {
|
||||
|
||||
@ -7,11 +7,13 @@ package org.citra.citra_emu.fragments
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
@ -26,18 +28,19 @@ import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import info.debatty.java.stringsimilarity.Jaccard
|
||||
import info.debatty.java.stringsimilarity.JaroWinkler
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.citra.citra_emu.CitraApplication
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.NativeLibrary
|
||||
import org.citra.citra_emu.adapters.GameAdapter
|
||||
import org.citra.citra_emu.databinding.FragmentSearchBinding
|
||||
import org.citra.citra_emu.model.Game
|
||||
import org.citra.citra_emu.viewmodel.CompressProgressDialogViewModel
|
||||
import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||
import org.citra.citra_emu.viewmodel.HomeViewModel
|
||||
import java.time.temporal.ChronoField
|
||||
import java.util.Locale
|
||||
import android.net.Uri
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
|
||||
class SearchFragment : Fragment() {
|
||||
private var _binding: FragmentSearchBinding? = null
|
||||
@ -53,6 +56,15 @@ class SearchFragment : Fragment() {
|
||||
gameAdapter.handleShortcutImageResult(uri)
|
||||
}
|
||||
|
||||
private var shouldCompress: Boolean = true
|
||||
private var pendingCompressInvocation: String? = null
|
||||
private val onCompressDecompressLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.CreateDocument("application/octet-stream")
|
||||
) { uri: Uri? ->
|
||||
GamesFragment.doCompression(this, gamesViewModel, pendingCompressInvocation, uri, shouldCompress)
|
||||
pendingCompressInvocation = null
|
||||
}
|
||||
|
||||
private lateinit var preferences: SharedPreferences
|
||||
|
||||
companion object {
|
||||
@ -85,7 +97,13 @@ class SearchFragment : Fragment() {
|
||||
gameAdapter = GameAdapter(
|
||||
requireActivity() as AppCompatActivity,
|
||||
inflater,
|
||||
openImageLauncher
|
||||
openImageLauncher,
|
||||
onRequestCompressOrDecompress = { inputPath, suggestedName, shouldCompress ->
|
||||
pendingCompressInvocation = inputPath
|
||||
onCompressDecompressLauncher.launch(suggestedName)
|
||||
this.shouldCompress = shouldCompress
|
||||
}
|
||||
|
||||
)
|
||||
|
||||
binding.gridGamesSearch.apply {
|
||||
|
||||
@ -27,6 +27,7 @@ class Game(
|
||||
val isVisibleSystemTitle: Boolean = false,
|
||||
val icon: IntArray? = null,
|
||||
val fileType: String = "",
|
||||
val isCompressed: Boolean = false,
|
||||
val filename: String,
|
||||
) : Parcelable {
|
||||
val keyAddedToLibraryTime get() = "${filename}_AddedToLibraryTime"
|
||||
|
||||
@ -90,6 +90,7 @@ object GameHelper {
|
||||
gameInfo?.getIsVisibleSystemTitle() ?: false,
|
||||
gameInfo?.getIcon(),
|
||||
gameInfo?.getFileType() ?: "",
|
||||
gameInfo?.getFileType()?.contains("(Z)") ?: false,
|
||||
if (FileUtil.isNativePath(filePath)) {
|
||||
CitraApplication.documentsTree.getFilename(filePath)
|
||||
} else {
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
// 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.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
object CompressProgressDialogViewModel: ViewModel() {
|
||||
private val _progress = MutableStateFlow(0)
|
||||
val progress = _progress.asStateFlow()
|
||||
|
||||
private val _total = MutableStateFlow(0)
|
||||
val total = _total.asStateFlow()
|
||||
|
||||
private val _message = MutableStateFlow("")
|
||||
val message = _message.asStateFlow()
|
||||
|
||||
fun update(totalBytes: Long, currentBytes: Long) {
|
||||
val percent = ((currentBytes * 100L) / totalBytes).coerceIn(0L, 100L).toInt()
|
||||
_total.value = 100
|
||||
_progress.value = percent
|
||||
_message.value = ""
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
_progress.value = 0
|
||||
_total.value = 0
|
||||
_message.value = ""
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2019 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
@ -40,6 +40,7 @@ static jfieldID s_game_info_pointer;
|
||||
|
||||
static jclass s_disk_cache_progress_class;
|
||||
static jmethodID s_disk_cache_load_progress;
|
||||
static jmethodID s_compress_progress_method;
|
||||
static std::unordered_map<VideoCore::LoadCallbackStage, jobject> s_java_load_callback_stages;
|
||||
|
||||
static jclass s_cia_install_helper_class;
|
||||
@ -131,6 +132,10 @@ jmethodID GetDiskCacheLoadProgress() {
|
||||
return s_disk_cache_load_progress;
|
||||
}
|
||||
|
||||
jmethodID GetCompressProgressMethod() {
|
||||
return s_compress_progress_method;
|
||||
}
|
||||
|
||||
jobject GetJavaLoadCallbackStage(VideoCore::LoadCallbackStage stage) {
|
||||
const auto it = s_java_load_callback_stages.find(stage);
|
||||
ASSERT_MSG(it != s_java_load_callback_stages.end(), "Invalid LoadCallbackStage: {}", stage);
|
||||
@ -205,6 +210,8 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||
s_disk_cache_load_progress = env->GetStaticMethodID(
|
||||
s_disk_cache_progress_class, "loadProgress",
|
||||
"(Lorg/citra/citra_emu/utils/DiskShaderCacheProgress$LoadCallbackStage;II)V");
|
||||
s_compress_progress_method =
|
||||
env->GetStaticMethodID(s_native_library_class, "onCompressProgress", "(JJ)V");
|
||||
// Initialize LoadCallbackStage map
|
||||
const auto to_java_load_callback_stage = [env,
|
||||
load_callback_stage_class](const std::string& stage) {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2019 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
@ -40,6 +40,7 @@ jfieldID GetGameInfoPointer();
|
||||
|
||||
jclass GetDiskCacheProgressClass();
|
||||
jmethodID GetDiskCacheLoadProgress();
|
||||
jmethodID GetCompressProgressMethod();
|
||||
jobject GetJavaLoadCallbackStage(VideoCore::LoadCallbackStage stage);
|
||||
|
||||
jclass GetCiaInstallHelperClass();
|
||||
|
||||
@ -34,10 +34,12 @@
|
||||
#include "common/scope_exit.h"
|
||||
#include "common/settings.h"
|
||||
#include "common/string_util.h"
|
||||
#include "common/zstd_compression.h"
|
||||
#include "core/core.h"
|
||||
#include "core/frontend/applets/default_applets.h"
|
||||
#include "core/frontend/camera/factory.h"
|
||||
#include "core/hle/service/am/am.h"
|
||||
#include "core/hle/service/fs/archive.h"
|
||||
#include "core/hle/service/nfc/nfc.h"
|
||||
#include "core/hw/unique_data.h"
|
||||
#include "core/loader/loader.h"
|
||||
@ -74,6 +76,17 @@ namespace {
|
||||
ANativeWindow* s_surface;
|
||||
ANativeWindow* s_secondary_surface;
|
||||
|
||||
enum class CompressionStatus : jint {
|
||||
Success = 0,
|
||||
Compress_Unsupported = 1,
|
||||
Compress_AlreadyCompressed = 2,
|
||||
Compress_Failed = 3,
|
||||
Decompress_Unsupported = 4,
|
||||
Decompress_NotCompressed = 5,
|
||||
Decompress_Failed = 6,
|
||||
Installed_Application = 7,
|
||||
};
|
||||
|
||||
std::shared_ptr<Common::DynamicLibrary> vulkan_library{};
|
||||
std::unique_ptr<EmuWindow_Android> window;
|
||||
std::unique_ptr<EmuWindow_Android> secondary_window;
|
||||
@ -464,6 +477,163 @@ jstring Java_org_citra_citra_1emu_NativeLibrary_getHomeMenuPath(JNIEnv* env,
|
||||
return ToJString(env, "");
|
||||
}
|
||||
|
||||
static CompressionStatus GetCompressFileInfo(Loader::AppLoader::CompressFileInfo& out_info,
|
||||
size_t& out_frame_size, const std::string& filepath,
|
||||
bool compress) {
|
||||
|
||||
if (Service::FS::IsInstalledApplication(filepath)) {
|
||||
return CompressionStatus::Installed_Application;
|
||||
}
|
||||
|
||||
Loader::AppLoader::CompressFileInfo compress_info{};
|
||||
compress_info.is_supported = false;
|
||||
size_t frame_size{};
|
||||
auto loader = Loader::GetLoader(filepath);
|
||||
if (loader) {
|
||||
compress_info = loader->GetCompressFileInfo();
|
||||
frame_size = FileUtil::Z3DSWriteIOFile::DEFAULT_FRAME_SIZE;
|
||||
} else {
|
||||
bool is_compressed = false;
|
||||
if (Service::AM::CheckCIAToInstall(filepath, is_compressed, compress ? true : false) ==
|
||||
Service::AM::InstallStatus::Success) {
|
||||
compress_info.is_supported = true;
|
||||
compress_info.is_compressed = is_compressed;
|
||||
compress_info.recommended_compressed_extension = "zcia";
|
||||
compress_info.recommended_uncompressed_extension = "cia";
|
||||
compress_info.underlying_magic = std::array<u8, 4>({'C', 'I', 'A', '\0'});
|
||||
frame_size = FileUtil::Z3DSWriteIOFile::DEFAULT_CIA_FRAME_SIZE;
|
||||
if (compress) {
|
||||
auto meta_info = Service::AM::GetCIAInfos(filepath);
|
||||
if (meta_info.Succeeded()) {
|
||||
const auto& meta_info_val = meta_info.Unwrap();
|
||||
std::vector<u8> value(sizeof(Service::AM::TitleInfo));
|
||||
memcpy(value.data(), &meta_info_val.first, sizeof(Service::AM::TitleInfo));
|
||||
compress_info.default_metadata.emplace("titleinfo", value);
|
||||
if (meta_info_val.second) {
|
||||
value.resize(sizeof(Loader::SMDH));
|
||||
memcpy(value.data(), meta_info_val.second.get(), sizeof(Loader::SMDH));
|
||||
compress_info.default_metadata.emplace("smdh", value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!compress_info.is_supported) {
|
||||
LOG_ERROR(Frontend,
|
||||
"Error {} file {}, the selected file is not a compatible 3DS ROM format or is "
|
||||
"encrypted.",
|
||||
compress ? "compressing" : "decompressing", filepath);
|
||||
return compress ? CompressionStatus::Compress_Unsupported
|
||||
: CompressionStatus::Decompress_Unsupported;
|
||||
}
|
||||
if (compress_info.is_compressed && compress) {
|
||||
LOG_ERROR(Frontend, "Error compressing file {}, the selected file is already compressed",
|
||||
filepath);
|
||||
return CompressionStatus::Compress_AlreadyCompressed;
|
||||
}
|
||||
if (!compress_info.is_compressed && !compress) {
|
||||
LOG_ERROR(Frontend,
|
||||
"Error decompressing file {}, the selected file is already decompressed",
|
||||
filepath);
|
||||
return CompressionStatus::Decompress_NotCompressed;
|
||||
}
|
||||
|
||||
out_info = compress_info;
|
||||
out_frame_size = frame_size;
|
||||
return CompressionStatus::Success;
|
||||
}
|
||||
|
||||
jint Java_org_citra_citra_1emu_NativeLibrary_compressFileNative(JNIEnv* env, jobject obj,
|
||||
jstring j_input_path,
|
||||
jstring j_output_path) {
|
||||
const std::string input_path = GetJString(env, j_input_path);
|
||||
const std::string output_path = GetJString(env, j_output_path);
|
||||
|
||||
Loader::AppLoader::CompressFileInfo compress_info{};
|
||||
size_t frame_size{};
|
||||
CompressionStatus stat = GetCompressFileInfo(compress_info, frame_size, input_path, true);
|
||||
if (stat != CompressionStatus::Success) {
|
||||
return static_cast<jint>(stat);
|
||||
}
|
||||
|
||||
auto progress = [](std::size_t processed, std::size_t total) {
|
||||
JNIEnv* env = IDCache::GetEnvForThread();
|
||||
env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(),
|
||||
IDCache::GetCompressProgressMethod(), static_cast<jlong>(total),
|
||||
static_cast<jlong>(processed));
|
||||
};
|
||||
|
||||
bool success =
|
||||
FileUtil::CompressZ3DSFile(input_path, output_path, compress_info.underlying_magic,
|
||||
frame_size, progress, compress_info.default_metadata);
|
||||
if (!success) {
|
||||
FileUtil::Delete(output_path);
|
||||
return static_cast<jint>(CompressionStatus::Compress_Failed);
|
||||
}
|
||||
|
||||
return static_cast<jint>(CompressionStatus::Success);
|
||||
}
|
||||
|
||||
jint Java_org_citra_citra_1emu_NativeLibrary_decompressFileNative(JNIEnv* env, jobject obj,
|
||||
jstring j_input_path,
|
||||
jstring j_output_path) {
|
||||
const std::string input_path = GetJString(env, j_input_path);
|
||||
const std::string output_path = GetJString(env, j_output_path);
|
||||
|
||||
Loader::AppLoader::CompressFileInfo compress_info{};
|
||||
size_t frame_size{};
|
||||
CompressionStatus stat = GetCompressFileInfo(compress_info, frame_size, input_path, false);
|
||||
if (stat != CompressionStatus::Success) {
|
||||
return static_cast<jint>(stat);
|
||||
}
|
||||
|
||||
auto progress = [](std::size_t processed, std::size_t total) {
|
||||
JNIEnv* env = IDCache::GetEnvForThread();
|
||||
env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(),
|
||||
IDCache::GetCompressProgressMethod(), static_cast<jlong>(total),
|
||||
static_cast<jlong>(processed));
|
||||
};
|
||||
|
||||
bool success = FileUtil::DeCompressZ3DSFile(input_path, output_path, progress);
|
||||
if (!success) {
|
||||
FileUtil::Delete(output_path);
|
||||
return static_cast<jint>(CompressionStatus::Decompress_Failed);
|
||||
}
|
||||
|
||||
return static_cast<jint>(CompressionStatus::Success);
|
||||
}
|
||||
|
||||
jstring Java_org_citra_citra_1emu_NativeLibrary_getRecommendedExtension(
|
||||
JNIEnv* env, jobject obj, jstring j_input_path, jboolean j_should_compress) {
|
||||
const std::string input_path = GetJString(env, j_input_path);
|
||||
|
||||
std::string compressed_ext;
|
||||
std::string uncompressed_ext;
|
||||
|
||||
auto loader = Loader::GetLoader(input_path);
|
||||
if (loader) {
|
||||
auto compress_info = loader->GetCompressFileInfo();
|
||||
if (compress_info.is_supported) {
|
||||
compressed_ext = compress_info.recommended_compressed_extension;
|
||||
uncompressed_ext = compress_info.recommended_uncompressed_extension;
|
||||
}
|
||||
} else {
|
||||
bool is_compressed = false;
|
||||
if (Service::AM::CheckCIAToInstall(input_path, is_compressed, true) ==
|
||||
Service::AM::InstallStatus::Success) {
|
||||
compressed_ext = "zcia";
|
||||
uncompressed_ext = "cia";
|
||||
}
|
||||
}
|
||||
|
||||
if (compressed_ext.empty()) {
|
||||
return env->NewStringUTF("");
|
||||
}
|
||||
|
||||
return env->NewStringUTF(j_should_compress ? compressed_ext.c_str() : uncompressed_ext.c_str());
|
||||
}
|
||||
|
||||
void Java_org_citra_citra_1emu_NativeLibrary_setUserDirectory(JNIEnv* env,
|
||||
[[maybe_unused]] jobject obj,
|
||||
jstring j_directory) {
|
||||
|
||||
@ -45,7 +45,8 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="viewStart"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold" app:layout_constraintStart_toStartOf="parent"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Application Title" />
|
||||
|
||||
@ -166,7 +167,6 @@
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="start|center"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/horizontal_layout">
|
||||
@ -179,6 +179,29 @@
|
||||
android:contentDescription="@string/cheats"
|
||||
android:text="@string/cheats" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/compress_tray"
|
||||
style="@style/ThemeOverlay.Material3.Button.IconButton.Filled.Tonal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="start|center"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/game_button_tray">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/compress_decompress"
|
||||
style="@style/Widget.Material3.Button.TonalButton.Icon"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/compress"
|
||||
android:text="@string/compress" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/compress_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:visibility="visible"
|
||||
android:paddingBottom="8dp" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/compress_progress"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:indeterminate="false"
|
||||
android:max="100"
|
||||
android:progress="0"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
</LinearLayout>
|
||||
@ -912,4 +912,19 @@
|
||||
<string name="emulation_occupied_quicksave_slot">Quicksave - %1$tF %1$tR</string>
|
||||
<string name="quickload_not_found">No Quicksave available.</string>
|
||||
|
||||
<!-- File Compression -->
|
||||
<string name="compress">Compress</string>
|
||||
<string name="compressing">Compressing…</string>
|
||||
<string name="decompress">Decompress</string>
|
||||
<string name="decompressing">Decompressing…</string>
|
||||
<string name="compress_success">Compression completed successfully.</string>
|
||||
<string name="compress_unsupported">Compression not supported for this file.</string>
|
||||
<string name="compress_already">File is already compressed.</string>
|
||||
<string name="compress_failed">Compression failed.</string>
|
||||
<string name="decompress_success">Decompression completed successfully.</string>
|
||||
<string name="decompress_unsupported">Decompression not supported for this file.</string>
|
||||
<string name="decompress_not_compressed">File is not compressed.</string>
|
||||
<string name="decompress_failed">Decompression failed.</string>
|
||||
<string name="compress_decompress_installed_app">Already installed applications cannot be compressed or decompressed.</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2014 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
@ -30,11 +30,16 @@
|
||||
|
||||
namespace Service::FS {
|
||||
|
||||
bool IsInstalledApplication(std::string_view path) {
|
||||
return path.rfind(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + "title", 0) == 0 ||
|
||||
path.rfind(FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) + "Nintendo 3DS", 0) == 0;
|
||||
}
|
||||
|
||||
MediaType GetMediaTypeFromPath(std::string_view path) {
|
||||
if (path.rfind(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir), 0) == 0) {
|
||||
if (path.rfind(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + "title", 0) == 0) {
|
||||
return MediaType::NAND;
|
||||
}
|
||||
if (path.rfind(FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), 0) == 0) {
|
||||
if (path.rfind(FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) + "Nintendo 3DS", 0) == 0) {
|
||||
return MediaType::SDMC;
|
||||
}
|
||||
return MediaType::GameCard;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2014 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
@ -51,6 +51,7 @@ enum class ArchiveIdCode : u32 {
|
||||
/// Media types for the archives
|
||||
enum class MediaType : u32 { NAND = 0, SDMC = 1, GameCard = 2 };
|
||||
|
||||
bool IsInstalledApplication(std::string_view path);
|
||||
MediaType GetMediaTypeFromPath(std::string_view path);
|
||||
|
||||
enum class SpecialContentType : u8 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user