Skip to content

Commit

Permalink
Allow DSiWare data files to be imported and exported
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaelvcaetano committed Jan 23, 2024
1 parent 32b1721 commit 3374059
Show file tree
Hide file tree
Showing 19 changed files with 491 additions and 50 deletions.
54 changes: 49 additions & 5 deletions app/src/main/cpp/MelonDSNandJNI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
#define TITLE_IMPORT_TITLE_ALREADY_IMPORTED 4
#define TITLE_IMPORT_INSATLL_FAILED 5

const u32 DSI_NAND_FILE_CATEGORY = 0x00030004;

bool isNandOpen = false;

jobject getTitleData(JNIEnv* env, u32 category, u32 titleId);
Expand Down Expand Up @@ -61,7 +63,7 @@ Java_me_magnum_melonds_MelonDSiNand_openNand(JNIEnv* env, jobject thiz, jobject
JNIEXPORT jobject JNICALL
Java_me_magnum_melonds_MelonDSiNand_listTitles(JNIEnv* env, jobject thiz)
{
const u32 category = 0x00030004;
const u32 category = DSI_NAND_FILE_CATEGORY;
std::vector<u32> titleList;
DSi_NAND::ListTitles(category, titleList);

Expand Down Expand Up @@ -104,7 +106,7 @@ Java_me_magnum_melonds_MelonDSiNand_importTitle(JNIEnv* env, jobject thiz, jstri
fread(titleId, 8, 1, titleFile);
fclose(titleFile);

if (titleId[1] != 0x00030004)
if (titleId[1] != DSI_NAND_FILE_CATEGORY)
{
// Not a DSiWare title
env->ReleaseStringUTFChars(titleUri, titlePath);
Expand Down Expand Up @@ -136,7 +138,39 @@ Java_me_magnum_melonds_MelonDSiNand_importTitle(JNIEnv* env, jobject thiz, jstri
JNIEXPORT void JNICALL
Java_me_magnum_melonds_MelonDSiNand_deleteTitle(JNIEnv* env, jobject thiz, jint titleId)
{
DSi_NAND::DeleteTitle(0x00030004, (u32) titleId);
DSi_NAND::DeleteTitle(DSI_NAND_FILE_CATEGORY, (u32) titleId);
}

JNIEXPORT jboolean JNICALL
Java_me_magnum_melonds_MelonDSiNand_importTitleFile(JNIEnv* env, jobject thiz, jint titleId, jint fileType, jstring fileUri)
{
jboolean isFilePathCopy;
const char* filePath = env->GetStringUTFChars(fileUri, &isFilePathCopy);

bool result = DSi_NAND::ImportTitleData(DSI_NAND_FILE_CATEGORY, (u32) titleId, fileType, filePath);

if (isFilePathCopy)
{
env->ReleaseStringUTFChars(fileUri, filePath);
}

return result;
}

JNIEXPORT jboolean JNICALL
Java_me_magnum_melonds_MelonDSiNand_exportTitleFile(JNIEnv* env, jobject thiz, jint titleId, jint fileType, jstring fileUri)
{
jboolean isFilePathCopy;
const char* filePath = env->GetStringUTFChars(fileUri, &isFilePathCopy);

bool result = DSi_NAND::ExportTitleData(DSI_NAND_FILE_CATEGORY, (u32) titleId, fileType, filePath);

if (isFilePathCopy)
{
env->ReleaseStringUTFChars(fileUri, filePath);
}

return result;
}

JNIEXPORT void JNICALL
Expand Down Expand Up @@ -168,7 +202,7 @@ jobject getTitleData(JNIEnv* env, u32 category, u32 titleId)
env->ReleaseByteArrayElements(iconBytes, iconArrayElements, 0);

jclass dsiWareTitleClass = env->FindClass("me/magnum/melonds/domain/model/DSiWareTitle");
jmethodID dsiWareTitleConstructor = env->GetMethodID(dsiWareTitleClass, "<init>", "(Ljava/lang/String;Ljava/lang/String;J[B)V");
jmethodID dsiWareTitleConstructor = env->GetMethodID(dsiWareTitleClass, "<init>", "(Ljava/lang/String;Ljava/lang/String;J[BJJI)V");

std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> convert;
std::string englishTitle = convert.to_bytes(banner.EnglishTitle);
Expand All @@ -177,6 +211,16 @@ jobject getTitleData(JNIEnv* env, u32 category, u32 titleId)
std::string title = englishTitle.substr(0, pos);
std::string producer = englishTitle.substr(pos + 1);

jobject titleObject = env->NewObject(dsiWareTitleClass, dsiWareTitleConstructor, env->NewStringUTF(title.c_str()), env->NewStringUTF(producer.c_str()), (jlong) titleId, iconBytes);
jobject titleObject = env->NewObject(
dsiWareTitleClass,
dsiWareTitleConstructor,
env->NewStringUTF(title.c_str()),
env->NewStringUTF(producer.c_str()),
(jlong) titleId,
iconBytes,
(jlong) header.DSiPublicSavSize,
(jlong) header.DSiPrivateSavSize,
header.AppFlags
);
return titleObject;
}
2 changes: 2 additions & 0 deletions app/src/main/java/me/magnum/melonds/MelonDSiNand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ object MelonDSiNand {
external fun listTitles(): ArrayList<DSiWareTitle>
external fun importTitle(titleUri: String, tmdMetadata: ByteArray): Int
external fun deleteTitle(titleId: Int)
external fun importTitleFile(titleId: Int, fileType: Int, fileUri: String): Boolean
external fun exportTitleFile(titleId: Int, fileType: Int, fileUri: String): Boolean
external fun closeNand()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package me.magnum.melonds.common.contracts

import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.activity.result.contract.ActivityResultContract

class CreateFileContract : ActivityResultContract<String, Uri?>() {
override fun createIntent(context: Context, input: String): Intent {
return Intent(Intent.ACTION_CREATE_DOCUMENT)
.putExtra(Intent.EXTRA_TITLE, input)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType("application/octet-stream")
}

override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
return if (intent == null || resultCode != Activity.RESULT_OK) {
null
} else {
intent.data
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,6 @@ class DirectoryPickerContract(private val permissions: Permission) : ActivityRes
return intent
}

override fun getSynchronousResult(context: Context, input: Uri?): SynchronousResult<Uri?>? {
return null
}

override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
return if (intent == null || resultCode != Activity.RESULT_OK) {
null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class FilePickerContract(private val permission: Permission) : ActivityResultCon
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
.putExtra(Intent.EXTRA_MIME_TYPES, input.second ?: arrayOf("*/*"))
.setType("*/*")
.addCategory(Intent.CATEGORY_OPENABLE)
.addFlags(permission.toFlags())

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && input.first != null) {
Expand All @@ -30,10 +31,6 @@ class FilePickerContract(private val permission: Permission) : ActivityResultCon
return intent
}

override fun getSynchronousResult(context: Context, input: Pair<Uri?, Array<String>?>): SynchronousResult<Uri?>? {
return null
}

override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
return if (intent == null || resultCode != Activity.RESULT_OK) {
null
Expand Down
12 changes: 11 additions & 1 deletion app/src/main/java/me/magnum/melonds/domain/model/DSiWareTitle.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,14 @@ class DSiWareTitle(
val producer: String,
val titleId: Long,
val icon: ByteArray,
)
val publicSavSize: Long,
val privateSavSize: Long,
val appFlags: Int,
) {

fun hasPublicSavFile() = publicSavSize != 0L

fun hasPrivateSavFile() = privateSavSize != 0L

fun hasBannerSavFile() = (appFlags and (0x04)) != 0
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package me.magnum.melonds.domain.model.dsinand

enum class DSiWareTitleFileType(val fileName: String) {
PUBLIC_SAV("public.sav"),
PRIVATE_SAV("private.sav"),
BANNER_SAV("banner.sav"),
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import android.net.Uri
import me.magnum.melonds.domain.model.DSiWareTitle
import me.magnum.melonds.domain.model.dsinand.ImportDSiWareTitleResult
import me.magnum.melonds.domain.model.dsinand.OpenDSiNandResult
import me.magnum.melonds.domain.model.dsinand.DSiWareTitleFileType

interface DSiNandManager {
suspend fun openNand(): OpenDSiNandResult
suspend fun listTitles(): List<DSiWareTitle>
suspend fun importTitle(titleUri: Uri): ImportDSiWareTitleResult
suspend fun deleteTitle(title: DSiWareTitle)
suspend fun importTitleFile(title: DSiWareTitle, fileType: DSiWareTitleFileType, fileUri: Uri): Boolean
suspend fun exportTitleFile(title: DSiWareTitle, fileType: DSiWareTitleFileType, fileUri: Uri): Boolean
fun closeNand()
}
17 changes: 17 additions & 0 deletions app/src/main/java/me/magnum/melonds/impl/AndroidDSiNandManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import me.magnum.melonds.MelonDSiNand
import me.magnum.melonds.common.suspendRunCatching
import me.magnum.melonds.domain.model.ConfigurationDirResult
import me.magnum.melonds.domain.model.DSiWareTitle
import me.magnum.melonds.domain.model.dsinand.DSiWareTitleFileType
import me.magnum.melonds.domain.model.dsinand.ImportDSiWareTitleResult
import me.magnum.melonds.domain.model.dsinand.OpenDSiNandResult
import me.magnum.melonds.domain.repositories.DSiWareMetadataRepository
Expand Down Expand Up @@ -90,6 +91,22 @@ class AndroidDSiNandManager(
MelonDSiNand.deleteTitle((title.titleId and 0xFFFFFFFF).toInt())
}

override suspend fun importTitleFile(title: DSiWareTitle, fileType: DSiWareTitleFileType, fileUri: Uri): Boolean {
if (!isNandOpen.get()) {
return false
}

return MelonDSiNand.importTitleFile((title.titleId and 0xFFFFFFFF).toInt(), fileType.ordinal, fileUri.toString())
}

override suspend fun exportTitleFile(title: DSiWareTitle, fileType: DSiWareTitleFileType, fileUri: Uri): Boolean {
if (!isNandOpen.get()) {
return false
}

return MelonDSiNand.exportTitleFile((title.titleId and 0xFFFFFFFF).toInt(), fileType.ordinal, fileUri.toString())
}

override fun closeNand() {
if (!isNandOpen.compareAndSet(true, false)) {
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collectLatest
import me.magnum.melonds.R
import me.magnum.melonds.domain.model.dsinand.ImportDSiWareTitleResult
import me.magnum.melonds.ui.dsiwaremanager.model.ImportExportDSiWareTitleFileEvent
import me.magnum.melonds.ui.dsiwaremanager.ui.DSiWareManager
import me.magnum.melonds.ui.dsiwaremanager.ui.rememberDSiWareTitleExportFilePicker
import me.magnum.melonds.ui.dsiwaremanager.ui.rememberDSiWareTitleImportFilePicker
import me.magnum.melonds.ui.theme.MelonTheme

@AndroidEntryPoint
Expand All @@ -35,13 +38,22 @@ class DSiWareManagerActivity : AppCompatActivity() {
val state = viewModel.state.collectAsState()
val importingTitle = viewModel.importingTitle.collectAsState(false)

val importTitleFilePickLauncher = rememberDSiWareTitleImportFilePicker(
onFilePicked = viewModel::importDSiWareTitleFile,
)
val exportTitleFilePickLauncher = rememberDSiWareTitleExportFilePicker(
onFilePicked = viewModel::exportDSiWareTitleFile,
)

DSiWareManager(
modifier = Modifier.fillMaxSize(),
state = state.value,
onImportTitle = { viewModel.importTitleToNand(it) },
onDeleteTitle = { viewModel.deleteTitle(it) },
onBiosConfigurationFinished = { viewModel.revalidateBiosConfiguration() },
retrieveTitleIcon = { viewModel.getTitleIcon(it) },
onImportTitle = viewModel::importTitleToNand,
onDeleteTitle = viewModel::deleteTitle,
onImportTitleFile = { title, fileType -> importTitleFilePickLauncher.launch(title, fileType) },
onExportTitleFile = { title, fileType -> exportTitleFilePickLauncher.launch(title, fileType) },
onBiosConfigurationFinished = viewModel::revalidateBiosConfiguration,
retrieveTitleIcon = viewModel::getTitleIcon,
)

if (importingTitle.value) {
Expand All @@ -58,6 +70,12 @@ class DSiWareManagerActivity : AppCompatActivity() {
Toast.makeText(this@DSiWareManagerActivity, getImportTitleResultMessage(it), Toast.LENGTH_LONG).show()
}
}

LaunchedEffect(null) {
viewModel.importExportFileEvent.collectLatest {
Toast.makeText(this@DSiWareManagerActivity, getImportExportFileErrorMessage(it), Toast.LENGTH_SHORT).show()
}
}
}
}
}
Expand All @@ -75,4 +93,13 @@ class DSiWareManagerActivity : AppCompatActivity() {
ImportDSiWareTitleResult.UNKNOWN -> getString(R.string.dsiware_manager_import_title_error_unknown)
}
}

private fun getImportExportFileErrorMessage(result: ImportExportDSiWareTitleFileEvent): String {
return when (result) {
is ImportExportDSiWareTitleFileEvent.ImportSuccess -> getString(R.string.dsiware_manager_import_file_success, result.fileName)
is ImportExportDSiWareTitleFileEvent.ImportError -> getString(R.string.dsiware_manager_import_file_error)
is ImportExportDSiWareTitleFileEvent.ExportSuccess -> getString(R.string.dsiware_manager_export_file_success, result.fileName)
is ImportExportDSiWareTitleFileEvent.ExportError -> getString(R.string.dsiware_manager_export_file_error)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import me.magnum.melonds.domain.repositories.SettingsRepository
import me.magnum.melonds.domain.services.ConfigurationDirectoryVerifier
import me.magnum.melonds.domain.services.DSiNandManager
import me.magnum.melonds.ui.dsiwaremanager.model.DSiWareManagerUiState
import me.magnum.melonds.domain.model.dsinand.DSiWareTitleFileType
import me.magnum.melonds.ui.dsiwaremanager.model.ImportExportDSiWareTitleFileEvent
import me.magnum.melonds.ui.romlist.RomIcon
import java.nio.ByteBuffer
import javax.inject.Inject
Expand All @@ -38,6 +40,9 @@ class DSiWareManagerViewModel @Inject constructor(
private val _importTitleError = MutableSharedFlow<ImportDSiWareTitleResult>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
val importTitleError: SharedFlow<ImportDSiWareTitleResult> = _importTitleError.asSharedFlow()

private val _importExportFileEvent = MutableSharedFlow<ImportExportDSiWareTitleFileEvent>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
val importExportFileEvent: SharedFlow<ImportExportDSiWareTitleFileEvent> = _importExportFileEvent.asSharedFlow()

init {
loadDSiWareData()
}
Expand Down Expand Up @@ -69,6 +74,38 @@ class DSiWareManagerViewModel @Inject constructor(
}
}

fun importDSiWareTitleFile(title: DSiWareTitle, fileType: DSiWareTitleFileType, fileUri: Uri) {
_importingTitle.value = true

viewModelScope.launch {
withContext(Dispatchers.Default) {
val success = dsiNandManager.importTitleFile(title, fileType, fileUri)
if (success) {
_importExportFileEvent.tryEmit(ImportExportDSiWareTitleFileEvent.ImportSuccess(fileType.fileName))
} else {
_importExportFileEvent.tryEmit(ImportExportDSiWareTitleFileEvent.ImportError)
}
_importingTitle.value = false
}
}
}

fun exportDSiWareTitleFile(title: DSiWareTitle, fileType: DSiWareTitleFileType, fileUri: Uri) {
_importingTitle.value = true

viewModelScope.launch {
withContext(Dispatchers.Default) {
val success = dsiNandManager.exportTitleFile(title, fileType, fileUri)
if (success) {
_importExportFileEvent.tryEmit(ImportExportDSiWareTitleFileEvent.ExportSuccess(fileType.fileName))
} else {
_importExportFileEvent.tryEmit(ImportExportDSiWareTitleFileEvent.ExportError)
}
_importingTitle.value = false
}
}
}

fun getTitleIcon(title: DSiWareTitle): RomIcon {
val bitmap = createBitmap(32, 32).apply {
copyPixelsFromBuffer(ByteBuffer.wrap(title.icon))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package me.magnum.melonds.ui.dsiwaremanager.model

enum class DSiWareItemDropdownMenu {
NONE,
MAIN,
IMPORT,
EXPORT,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package me.magnum.melonds.ui.dsiwaremanager.model

sealed class ImportExportDSiWareTitleFileEvent {
data class ImportSuccess(val fileName: String) : ImportExportDSiWareTitleFileEvent()
data object ImportError : ImportExportDSiWareTitleFileEvent()
data class ExportSuccess(val fileName: String) : ImportExportDSiWareTitleFileEvent()
data object ExportError : ImportExportDSiWareTitleFileEvent()
}
Loading

0 comments on commit 3374059

Please sign in to comment.