Keep game and firmware art out of the gallery

Two sources, both writing real PNGs to shared storage where Android's media
scanner indexes them and they turn up in the camera roll.

The data root holds firmware assets: trophy icons under dev_hdd0/home, the whole
dev_flash VSH resource set, and an ICON0.PNG per installed game. That was 216
images. A .nomedia now goes in before the core initialises, since that is what
unpacks the firmware and creates most of them.

The ROM folder is the one people actually notice, and only since folder format
games started working. An .iso is a single opaque file so the scanner sees
nothing inside it, but a disc in folder form lays its ICON0.PNG, PIC1.PNG and
every DLC image out in the open. One Minecraft folder accounted for 245 images,
which was every image in the whole ROM tree.

The ROM marker goes at the configured directory root rather than inside a game
folder, so it covers current and future folder games with one file and never
leaves a stray file inside content that gets mounted as a disc. Writing it also
triggers a rescan, because the marker alone does not drop what MediaStore has
already indexed. Zero bytes and reversible either way.
This commit is contained in:
jpolo1224
2026-08-07 00:25:47 -04:00
parent da6cf3bb49
commit cac6590b04
2 changed files with 62 additions and 0 deletions
@@ -57,6 +57,40 @@ class GameLibraryRepository(private val context: Context) {
*
* Both hold games in folder form, so [isPs3GameFolder] is what actually finds them.
*/
/**
* Keep game art out of the user's gallery.
*
* An .iso is one opaque file, so the media scanner sees nothing inside it. A disc in
* FOLDER form lays its ICON0.PNG, PIC1.PNG and every DLC image out on shared storage,
* where the scanner indexes them and they land in the camera roll. One Minecraft folder
* accounted for 245 images, which was every image in the whole ROM tree.
*
* The marker goes at the ROM directory root the user configured, not inside a game
* folder: it covers current and future folder games in one file, and it never puts a
* stray file inside content the emulator mounts as a disc.
*
* Zero bytes and reversible, deleting it restores the old behaviour. Best effort, since
* the ROM folder may be read-only or reached over SAF.
*/
private fun shieldFromMediaScanner(directory: File) {
runCatching {
val marker = File(directory, ".nomedia")
if (marker.exists()) return
if (marker.createNewFile()) {
android.util.Log.i(ScanTag, "wrote .nomedia in ${directory.absolutePath}")
// Adding the marker does not retroactively drop what MediaStore already
// indexed. Re-scanning the path is what makes the provider re-evaluate the
// subtree and forget it.
runCatching {
android.media.MediaScannerConnection.scanFile(
context, arrayOf(directory.absolutePath), null, null,
)
}
}
}
}
/** True when the emulator's own storage holds games, ROM folders or not. */
fun hasInternalGames(): Boolean = internalGameDirectories().any {
runCatching { it.listFiles()?.isNotEmpty() }.getOrNull() == true
@@ -123,6 +157,7 @@ class GameLibraryRepository(private val context: Context) {
val rawRoot = if (canUseRawStorage()) posix?.let(::File) else null
android.util.Log.i(ScanTag, "dir=$rawUri -> posix=$posix isDir=${rawRoot?.isDirectory}")
if (rawRoot?.isDirectory == true) {
shieldFromMediaScanner(rawRoot)
scanRawDirectory(rawRoot, collected, 0)
} else {
val tree = DocumentFile.fromTreeUri(context, uri)
@@ -88,11 +88,38 @@ object Rpcs3Bridge {
@Volatile
private var pumpsStarted = false
/**
* Keep the emulator's data out of the user's gallery.
*
* The data root holds hundreds of real PNGs that are not the user's pictures: trophy
* icons under dev_hdd0/home, the whole dev_flash VSH resource set, and an ICON0.PNG for
* every installed game. On a device where the data root is on shared storage the media
* scanner indexes all of it, and they turn up in the camera roll. Reported with 216
* images already indexed.
*
* An empty ".nomedia" at the root excludes the entire subtree. Writing it before the
* core initialises matters, because that is what unpacks the firmware and creates most
* of those files -- once they are indexed, getting them back out is the hard part.
*
* Cheap and idempotent, so it runs on every startup rather than being gated on a pref:
* a user who wipes their data folder gets it back automatically.
*/
private fun shieldFromMediaScanner(root: String) {
runCatching {
val marker = java.io.File(root, ".nomedia")
if (!marker.exists()) {
marker.parentFile?.mkdirs()
marker.createNewFile()
}
}
}
@JvmStatic
fun initialize(rootPath: String) {
if (RPCSX.initialized) return
RPCSX.rootDirectory = if (rootPath.endsWith("/")) rootPath else "$rootPath/"
shieldFromMediaScanner(RPCSX.rootDirectory)
RPCSX.instance.initialize(RPCSX.rootDirectory, "00000001")
RPCSX.initialized = true