Identify DSiWare titles in the ROM list screen and prevent shortcuts from being created for these titles

This commit is contained in:
Rafael Caetano 2022-10-06 23:29:24 +01:00
parent 23d09b5888
commit 946158402b
15 changed files with 243 additions and 69 deletions

View File

@ -5,10 +5,7 @@ import android.graphics.Bitmap
import android.net.Uri
import io.reactivex.Single
import me.magnum.melonds.common.uridelegates.UriHandler
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.RomConfig
import me.magnum.melonds.domain.model.RomInfo
import me.magnum.melonds.domain.model.SizeUnit
import me.magnum.melonds.domain.model.*
import me.magnum.melonds.extensions.isBlank
import me.magnum.melonds.extensions.nameWithoutExtension
import me.magnum.melonds.impl.NdsRomCache
@ -28,8 +25,9 @@ abstract class CompressedRomFileProcessor(private val context: Context, private
context.contentResolver.openInputStream(romUri)?.use { stream ->
getNdsEntryStreamInFileStream(stream)?.use { romFileStream ->
val romDocument = uriHandler.getUriDocument(romUri)
val romName = getRomNameInZipEntry(romFileStream).takeUnless { it.isBlank() } ?: romDocument?.nameWithoutExtension ?: ""
Rom(romName, romDocument?.name ?: "", romUri, parentUri, RomConfig())
val romMetadata = getRomMetadataInZipEntry(romFileStream)
val romName = romMetadata.romTitle.takeUnless { it.isBlank() } ?: romDocument?.nameWithoutExtension ?: ""
Rom(romName, romDocument?.name ?: "", romUri, parentUri, RomConfig(), null, romMetadata.isDSiWareTitle)
}
}
} catch (e: Exception) {
@ -80,8 +78,8 @@ abstract class CompressedRomFileProcessor(private val context: Context, private
}
}
private fun getRomNameInZipEntry(inputStream: InputStream): String {
return RomProcessor.getRomName(inputStream.buffered())
private fun getRomMetadataInZipEntry(inputStream: InputStream): RomMetadata {
return RomProcessor.getRomMetadata(inputStream.buffered())
}
private fun extractRomFile(rom: Rom): Single<Uri> {

View File

@ -8,6 +8,7 @@ import me.magnum.melonds.common.uridelegates.UriHandler
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.RomConfig
import me.magnum.melonds.domain.model.RomInfo
import me.magnum.melonds.domain.model.RomMetadata
import me.magnum.melonds.extensions.isBlank
import me.magnum.melonds.extensions.nameWithoutExtension
import me.magnum.melonds.utils.RomProcessor
@ -16,10 +17,10 @@ class NdsRomFileProcessor(private val context: Context, private val uriHandler:
override fun getRomFromUri(romUri: Uri, parentUri: Uri): Rom? {
return try {
getRomName(romUri)?.let { name ->
getRomMetadata(romUri)?.let { metadata ->
val romDocument = uriHandler.getUriDocument(romUri)
val romName = name.takeUnless { it.isBlank() } ?: romDocument?.nameWithoutExtension ?: ""
Rom(romName, romDocument?.name ?: "", romUri, parentUri, RomConfig())
val romName = metadata.romTitle.takeUnless { it.isBlank() } ?: romDocument?.nameWithoutExtension ?: ""
Rom(romName, romDocument?.name ?: "", romUri, parentUri, RomConfig(), null, metadata.isDSiWareTitle)
}
} catch (e: Exception) {
e.printStackTrace()
@ -53,9 +54,9 @@ class NdsRomFileProcessor(private val context: Context, private val uriHandler:
return Single.just(rom.uri)
}
private fun getRomName(uri: Uri): String? {
private fun getRomMetadata(uri: Uri): RomMetadata? {
return context.contentResolver.openInputStream(uri)?.use { inputStream ->
RomProcessor.getRomName(inputStream.buffered())
RomProcessor.getRomMetadata(inputStream.buffered())
}
}
}

View File

@ -10,6 +10,7 @@ data class Rom(
val parentTreeUri: Uri,
var config: RomConfig,
var lastPlayed: Date? = null,
val isDsiWareTitle: Boolean,
) {
override fun equals(other: Any?): Boolean {

View File

@ -0,0 +1,6 @@
package me.magnum.melonds.domain.model
data class RomMetadata(
val romTitle: String,
val isDSiWareTitle: Boolean,
)

View File

@ -109,7 +109,7 @@ class XmlCheatDatabaseSAXHandler(private val listener: HandlerListener) : Defaul
if (parsingDatabase) {
if (parsingDatabaseName) {
// Remove everything between parenthesis. Most likely it contains the DB's version
databaseName = textStringBuilder.toString().replace("\\(.*?\\)".toRegex(), "")
databaseName = textStringBuilder.toString().replace("\\(.*?\\)".toRegex(), "").trim()
parsingDatabaseName = false
emitCheatDatabaseName(databaseName!!)
}

View File

@ -6,6 +6,7 @@ import com.google.gson.reflect.TypeToken
import me.magnum.melonds.common.uridelegates.UriHandler
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.migrations.legacy.Rom21
import me.magnum.melonds.utils.RomProcessor
import java.io.File
import java.io.FileReader
import java.io.OutputStreamWriter
@ -28,16 +29,20 @@ class Migration21to22(
override fun migrate() {
val originalRoms = getOriginalRoms()
val newRoms = originalRoms.mapNotNull { rom ->
uriHandler.getUriDocument(rom.uri)?.name?.let { fileName ->
Rom(
rom.name,
fileName,
rom.uri,
rom.parentTreeUri,
rom.config,
rom.lastPlayed,
)
}
val fileName = uriHandler.getUriDocument(rom.uri)?.name ?: return@mapNotNull null
val romMetadata = context.contentResolver.openInputStream(rom.uri)?.use {
RomProcessor.getRomMetadata(it.buffered())
} ?: return@mapNotNull null
Rom(
rom.name,
fileName,
rom.uri,
rom.parentTreeUri,
rom.config,
rom.lastPlayed,
romMetadata.isDSiWareTitle,
)
}
saveNewRoms(newRoms)

View File

@ -22,7 +22,8 @@ class RomParcelable : Parcelable {
val parentTreeUri = parcel.readString()!!.toUri()
val lastPlayed = parcel.readLong().let { if (it == (-1).toLong()) null else Date(it) }
val romConfig = parcel.parcelable<RomConfigParcelable>()
rom = Rom(name!!, fileName!!, uri, parentTreeUri, romConfig!!.romConfig, lastPlayed)
val isDsiWareTitle = parcel.readInt() == 1
rom = Rom(name!!, fileName!!, uri, parentTreeUri, romConfig!!.romConfig, lastPlayed, isDsiWareTitle)
}
override fun writeToParcel(dest: Parcel, flags: Int) {
@ -32,6 +33,7 @@ class RomParcelable : Parcelable {
dest.writeString(rom.parentTreeUri.toString())
dest.writeLong(rom.lastPlayed?.time ?: -1)
dest.writeParcelable(RomConfigParcelable(rom.config), 0)
dest.writeInt(if (rom.isDsiWareTitle) 1 else 0)
}
override fun describeContents(): Int {

View File

@ -184,7 +184,7 @@ class RomListActivity : AppCompatActivity() {
private fun addRomListFragment() {
var romListFragment = supportFragmentManager.findFragmentByTag(FRAGMENT_ROM_LIST) as RomListFragment?
if (romListFragment == null) {
romListFragment = RomListFragment.newInstance(true)
romListFragment = RomListFragment.newInstance(true, RomListFragment.RomEnableCriteria.ENABLE_ALL)
supportFragmentManager.commit {
replace(R.id.layout_main, romListFragment, FRAGMENT_ROM_LIST)
}

View File

@ -1,6 +1,8 @@
package me.magnum.melonds.ui.romlist
import android.content.Context
import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
import android.graphics.drawable.BitmapDrawable
import android.os.Bundle
import android.view.LayoutInflater
@ -8,7 +10,9 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.res.ResourcesCompat
import androidx.core.os.bundleOf
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
@ -30,22 +34,31 @@ import me.magnum.melonds.databinding.RomListFragmentBinding
import me.magnum.melonds.domain.model.Rom
import me.magnum.melonds.domain.model.RomIconFiltering
import me.magnum.melonds.domain.model.RomScanningStatus
import me.magnum.melonds.extensions.setViewEnabledRecursive
import me.magnum.melonds.ui.romlist.RomListFragment.RomEnabledFilter
import me.magnum.melonds.ui.romlist.RomListFragment.RomListAdapter.RomViewHolder
@AndroidEntryPoint
class RomListFragment : Fragment() {
companion object {
private const val KEY_ALLOW_ROM_CONFIGURATION = "allow_rom_configuration"
private const val KEY_ROM_ENABLE_CRITERIA = "rom_enable_criteria"
fun newInstance(allowRomConfiguration: Boolean): RomListFragment {
fun newInstance(allowRomConfiguration: Boolean, enableCriteria: RomEnableCriteria): RomListFragment {
return RomListFragment().also {
it.arguments = bundleOf(
KEY_ALLOW_ROM_CONFIGURATION to allowRomConfiguration
KEY_ALLOW_ROM_CONFIGURATION to allowRomConfiguration,
KEY_ROM_ENABLE_CRITERIA to enableCriteria.toString(),
)
}
}
}
enum class RomEnableCriteria {
ENABLE_ALL,
ENABLE_NON_DSIWARE,
}
private lateinit var binding: RomListFragmentBinding
private val romListViewModel: RomListViewModel by activityViewModels()
private lateinit var romListAdapter: RomListAdapter
@ -63,16 +76,24 @@ class RomListFragment : Fragment() {
binding.swipeRefreshRoms.setOnRefreshListener { romListViewModel.refreshRoms() }
val allowRomConfiguration = arguments?.getBoolean(KEY_ALLOW_ROM_CONFIGURATION) ?: true
romListAdapter = RomListAdapter(allowRomConfiguration, requireContext(), lifecycleScope, object : RomClickListener {
override fun onRomClicked(rom: Rom) {
romListViewModel.setRomLastPlayedNow(rom)
romSelectedListener?.invoke(rom)
}
val romEnableCriteria = arguments?.getString(KEY_ROM_ENABLE_CRITERIA)?.let { RomEnableCriteria.valueOf(it) } ?: RomEnableCriteria.ENABLE_ALL
override fun onRomConfigClicked(rom: Rom) {
RomConfigDialog.newInstance(rom.name, rom.copy()).show(parentFragmentManager, null)
}
})
romListAdapter = RomListAdapter(
allowRomConfiguration = allowRomConfiguration,
context = requireContext(),
coroutineScope = lifecycleScope,
listener = object : RomClickListener {
override fun onRomClicked(rom: Rom) {
romListViewModel.setRomLastPlayedNow(rom)
romSelectedListener?.invoke(rom)
}
override fun onRomConfigClicked(rom: Rom) {
RomConfigDialog.newInstance(rom.name, rom.copy()).show(parentFragmentManager, null)
}
},
romEnabledFilter = buildRomEnabledFilter(romEnableCriteria),
)
binding.listRoms.apply {
val listLayoutManager = LinearLayoutManager(context)
@ -108,6 +129,13 @@ class RomListFragment : Fragment() {
binding.textRomListEmpty.isVisible = emptyViewVisible
}
private fun buildRomEnabledFilter(romEnableCriteria: RomEnableCriteria): RomEnabledFilter {
return when (romEnableCriteria) {
RomEnableCriteria.ENABLE_ALL -> RomEnabledFilter { true }
RomEnableCriteria.ENABLE_NON_DSIWARE -> RomEnabledFilter { !it.isDsiWareTitle}
}
}
fun setRomSelectedListener(listener: (Rom) -> Unit) {
romSelectedListener = listener
}
@ -116,7 +144,8 @@ class RomListFragment : Fragment() {
private val allowRomConfiguration: Boolean,
private val context: Context,
private val coroutineScope: CoroutineScope,
private val listener: RomClickListener
private val listener: RomClickListener,
private val romEnabledFilter: RomEnabledFilter,
) : RecyclerView.Adapter<RomViewHolder>() {
private val roms: ArrayList<Rom> = ArrayList()
@ -146,7 +175,8 @@ class RomListFragment : Fragment() {
override fun onBindViewHolder(romViewHolder: RomViewHolder, i: Int) {
val rom = roms[i]
romViewHolder.setRom(rom)
val isRomEnabled = romEnabledFilter.isRomEnabled(rom)
romViewHolder.setRom(rom, isRomEnabled)
}
override fun onViewRecycled(holder: RomViewHolder) {
@ -161,6 +191,7 @@ class RomListFragment : Fragment() {
private val imageViewRomIcon = itemView.findViewById<ImageView>(R.id.imageRomIcon)
private val textViewRomName = itemView.findViewById<TextView>(R.id.textRomName)
private val textViewRomPath = itemView.findViewById<TextView>(R.id.textRomPath)
private val imagePlatformLogo = itemView.findViewById<ImageView>(R.id.logoPlatform)
private lateinit var rom: Rom
private var romIconLoadJob: Job? = null
@ -175,18 +206,44 @@ class RomListFragment : Fragment() {
romIconLoadJob?.cancel()
}
open fun setRom(rom: Rom) {
open fun setRom(rom: Rom, isEnabled: Boolean) {
this.rom = rom
textViewRomName.text = rom.name
textViewRomPath.text = rom.fileName
imageViewRomIcon.setImageDrawable(null)
imagePlatformLogo.isVisible = rom.isDsiWareTitle
val platformDrawable = if (rom.isDsiWareTitle) {
ResourcesCompat.getDrawable(itemView.resources, R.drawable.logo_dsiware, null)
} else {
null
}
if (platformDrawable != null && !isEnabled) {
platformDrawable.apply {
colorFilter = ColorMatrixColorFilter(ColorMatrix().apply { setSaturation(0f) })
alpha = 127
}
}
imagePlatformLogo.setImageDrawable(platformDrawable)
romIconLoadJob = coroutineScope.launch {
val romIcon = romListViewModel.getRomIcon(rom)
val iconDrawable = BitmapDrawable(itemView.resources, romIcon.bitmap)
iconDrawable.paint.isFilterBitmap = romIcon.filtering == RomIconFiltering.LINEAR
val iconDrawable = BitmapDrawable(itemView.resources, romIcon.bitmap).apply {
paint.isFilterBitmap = romIcon.filtering == RomIconFiltering.LINEAR
if (isEnabled) {
colorFilter = null
alpha = 255
} else {
colorFilter = ColorMatrixColorFilter(ColorMatrix().apply { setSaturation(0f) })
alpha = 127
}
}
imageViewRomIcon.setImageDrawable(iconDrawable)
}
itemView.setViewEnabledRecursive(isEnabled)
}
protected fun getRom() = rom
@ -206,6 +263,11 @@ class RomListFragment : Fragment() {
onRomConfigClick(getRom())
}
}
override fun setRom(rom: Rom, isEnabled: Boolean) {
super.setRom(rom, isEnabled)
imageViewButtonRomConfig.isGone = rom.isDsiWareTitle
}
}
inner class RomsDiffUtilCallback(private val oldRoms: List<Rom>, private val newRoms: List<Rom>) : DiffUtil.Callback() {
@ -241,4 +303,8 @@ class RomListFragment : Fragment() {
fun onRomClicked(rom: Rom)
fun onRomConfigClicked(rom: Rom)
}
fun interface RomEnabledFilter {
fun isRomEnabled(rom: Rom): Boolean
}
}

View File

@ -38,12 +38,17 @@ class ShortcutSetupActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_shortcut_setup)
val fragment = RomListFragment.newInstance(false)
fragment.setRomSelectedListener { onRomSelected(it) }
supportFragmentManager.commit {
replace(R.id.layout_root, fragment, FRAGMENT_ROM_LIST)
val fragment = if (savedInstanceState == null) {
RomListFragment.newInstance(false, RomListFragment.RomEnableCriteria.ENABLE_NON_DSIWARE).also {
supportFragmentManager.commit {
replace(R.id.layout_root, it, FRAGMENT_ROM_LIST)
}
}
} else {
supportFragmentManager.findFragmentByTag(FRAGMENT_ROM_LIST) as RomListFragment
}
fragment.setRomSelectedListener { onRomSelected(it) }
}
private fun onRomSelected(rom: Rom) {

View File

@ -4,7 +4,9 @@ import android.graphics.Bitmap
import android.graphics.Color
import androidx.core.graphics.createBitmap
import me.magnum.melonds.common.Crc32
import me.magnum.melonds.common.cheats.ProgressTrackerInputStream
import me.magnum.melonds.domain.model.RomInfo
import me.magnum.melonds.domain.model.RomMetadata
import java.io.BufferedInputStream
import java.io.InputStream
import java.nio.ByteBuffer
@ -13,21 +15,54 @@ import kotlin.experimental.and
import kotlin.math.min
object RomProcessor {
fun getRomName(inputStream: BufferedInputStream): String {
// Banner offset is at header offset 0x68
inputStream.skipStreamBytes(0x68)
// Obtain the banner offset
val offsetData = ByteArray(4)
inputStream.read(offsetData)
private val DSIWARE_CATEGORY = 0x00030004.toUInt()
private const val KEY_ROM_NAME = "name"
private const val KEY_ROM_IS_DSIWARE_TITLE = "isDsiWareTitle"
val bannerOffset = byteArrayToInt(offsetData)
inputStream.skipStreamBytes(bannerOffset.toLong() + 576 - (0x68 + 4))
val titleData = ByteArray(128)
inputStream.read(titleData)
return String(titleData, StandardCharsets.UTF_16LE)
.trim()
.replaceFirst("\n.*?$".toRegex(), "")
.replace("\n", " ")
fun getRomMetadata(inputStream: BufferedInputStream): RomMetadata {
val romStreamProcessor = RomStreamDataProcessor().apply {
registerProcessor(
RomStreamDataProcessor.SectionProcessor.SubSectionProcessor(
KEY_ROM_NAME,
streamOffset = 0x68,
processor = {
val offsetData = ByteArray(4)
inputStream.read(offsetData)
val bannerOffset = byteArrayToInt(offsetData)
bannerOffset + (0x0340 - 4 * 2).toLong()
},
valueProcessor = {
val titleData = ByteArray(128)
inputStream.read(titleData)
String(titleData, StandardCharsets.UTF_16LE)
.trim()
.substringBeforeLast('\n')
.replace("\n", " ")
}
)
)
registerProcessor(
RomStreamDataProcessor.SectionProcessor.SectionValueProcessor(
KEY_ROM_IS_DSIWARE_TITLE,
streamOffset = 0x230,
processor = {
val categoryData = ByteArray(4)
inputStream.read(categoryData)
val categoryId = byteArrayToInt(categoryData)
categoryId.toUInt() == DSIWARE_CATEGORY
}
)
)
process(inputStream)
}
val romName = romStreamProcessor.getValue<String>(KEY_ROM_NAME)
val isDsiWareTitle = romStreamProcessor.getValue<Boolean>(KEY_ROM_IS_DSIWARE_TITLE)
return RomMetadata(
romName,
isDsiWareTitle,
)
}
fun getRomIcon(inputStream: BufferedInputStream): Bitmap {
@ -160,7 +195,7 @@ object RomProcessor {
* Custom made way to skip bytes in an input stream. When dealing with zipped files, the internal implementations (ZipInputStream and BufferedInputStream) don't work very
* well. This one seems to work when dealing with a BufferedInputStream
*/
private fun BufferedInputStream.skipStreamBytes(bytes: Long) {
private fun InputStream.skipStreamBytes(bytes: Long) {
val buffer = ByteArray(1024)
var remaining = bytes
do {
@ -172,4 +207,43 @@ object RomProcessor {
remaining -= read
} while (remaining > 0)
}
private class RomStreamDataProcessor {
private val processors = mutableListOf<SectionProcessor>()
private val values = mutableMapOf<String, Any>()
fun registerProcessor(processor: SectionProcessor) {
processors.add(processor)
}
fun process(stream: BufferedInputStream) {
val trackedStream = ProgressTrackerInputStream(stream)
val sortedProcessors = processors.sortedBy { it.streamOffset }.toMutableList()
while (sortedProcessors.isNotEmpty()) {
val processor = sortedProcessors.removeFirst()
val bytesToSkip = processor.streamOffset - trackedStream.totalReadBytes
trackedStream.skipStreamBytes(bytesToSkip)
if (processor is SectionProcessor.SectionValueProcessor) {
val value = processor.processor(trackedStream)
values[processor.key] = value
} else if (processor is SectionProcessor.SubSectionProcessor) {
val newOffset = processor.processor(trackedStream)
sortedProcessors.add(SectionProcessor.SectionValueProcessor(processor.key, newOffset, processor.valueProcessor))
sortedProcessors.sortBy { it.streamOffset }
}
}
}
@Suppress("UNCHECKED_CAST")
fun <T> getValue(key: String): T {
return values[key] as T
}
sealed class SectionProcessor(val streamOffset: Long) {
class SectionValueProcessor(val key: String, streamOffset: Long, val processor: (InputStream) -> Any) : SectionProcessor(streamOffset)
class SubSectionProcessor(val key: String, streamOffset: Long, val processor: (InputStream) -> Long, val valueProcessor: (InputStream) -> Any) : SectionProcessor(streamOffset)
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -11,13 +11,26 @@
android:layout_height="48dp"
android:layout_marginEnd="8dp" />
<ImageView
android:id="@+id/logoPlatform"
android:layout_width="50dp"
android:layout_height="wrap_content"
android:scaleType="fitCenter"
android:layout_alignTop="@+id/textRomName"
android:layout_alignBottom="@+id/textRomName"
android:layout_toEndOf="@id/imageRomIcon"
android:layout_marginEnd="4dp"
android:visibility="visible"
tools:src="@drawable/logo_dsiware"
tools:visibility="visible" />
<TextView
android:id="@+id/textRomName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?android:attr/textColorPrimary"
android:textSize="18sp"
android:layout_toEndOf="@id/imageRomIcon"
android:layout_toEndOf="@id/logoPlatform"
android:layout_alignParentEnd="true"
android:singleLine="true"
android:ellipsize="end"

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/layoutRomItem"
android:layout_width="match_parent"
@ -16,8 +16,10 @@
android:id="@+id/layout_rom_base_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_toStartOf="@+id/buttonRomConfig">
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@id/buttonRomConfig"
app:layout_goneMarginEnd="8dp">
<include layout="@layout/item_rom_base"
android:layout_width="match_parent"
@ -29,12 +31,13 @@
android:id="@+id/buttonRomConfig"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/layout_rom_base_content"
android:padding="8dp"
android:contentDescription="@string/rom_settings"
style="@style/Button.Ripple"
app:tint="@color/romConfigButtonDefault"
app:srcCompat="@drawable/ic_settings"
android:nextFocusLeft="@+id/layoutRomItem" />
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>