diff --git a/install.ps1 b/install.ps1 index 0b44aa3b4..f20e65a2c 100644 --- a/install.ps1 +++ b/install.ps1 @@ -16,7 +16,7 @@ # and Invoke-WebRequest throws on a cut transfer before the hash is read: # the site's build rewrites this line there (scripts/sitebootstrap.py). $mismatchAdvice = "The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that." - $defaultInstallSha256 = "bba0f7cd7677fb7f4d767504412843cf3809c03b91808c7c7772f17758893d50" + $defaultInstallSha256 = "192ecdd781447b69967d5d335911fb34887757ee1bf9b6c000ad68a62b17de4b" $maximumInstallerBytes = 2MB $installerExit = 0 $installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null } diff --git a/install.py b/install.py index a7a272964..4e24fd57b 100644 --- a/install.py +++ b/install.py @@ -3132,6 +3132,29 @@ def fold_case_twins(files: list[dict]) -> tuple[list[dict], list[str]]: return kept, folded +# Android's media scanner marks a folder holding a .nomedia file hidden, and +# every folder below it (MediaProvider a183e2b9, FileUtils.java:1587-1629 +# isDirectoryHidden, ModernMediaScanner.java:845-846 and 1017-1020). Without +# one, the samples, overlays and videos of a BIOS folder in shared storage +# showed in the phone's gallery and music apps. RetroArch writes none +# (libretro-common/file/file_path_io.c:194 only skips one). +NOMEDIA = ".nomedia" + + +def hide_from_media_scanner(folder: Path) -> bool: + """Leave an empty .nomedia in folder, True when this call wrote it.""" + marker = folder / NOMEDIA + if not _is_dir(folder) or _exists(marker): + return False + try: + marker.touch() + except OSError as exc: + print(f" {NOMEDIA} not written in {shown_path(folder)}: {exc.strerror}", + file=sys.stderr) + return False + return True + + def installed_cores(dirs: list[Path]) -> list[str]: """The cores present in these directories, by the name the buildbot uses. @@ -16343,6 +16366,10 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: failures.append((where, f"{NOT_KEPT} ({reason})")) total_errors += 1 + if os_type == "android" and not args.check and hide_from_media_scanner(bios_path): + print(f" Added {NOMEDIA} to {shown_path(bios_path)}: the gallery and music " + "apps leave its images, sounds and videos out") + # Standalone copies if ( manifest.get("standalone_copies") diff --git a/install.sh b/install.sh index 1e47b7f91..3eb6c581b 100755 --- a/install.sh +++ b/install.sh @@ -23,7 +23,7 @@ DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/ins # curl or wget stop on a cut transfer before the hash is read: the site's # build rewrites this line there (scripts/sitebootstrap.py). MISMATCH_ADVICE="The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that." -DEFAULT_INSTALL_SHA256="bba0f7cd7677fb7f4d767504412843cf3809c03b91808c7c7772f17758893d50" +DEFAULT_INSTALL_SHA256="192ecdd781447b69967d5d335911fb34887757ee1bf9b6c000ad68a62b17de4b" MAX_INSTALLER_BYTES=2097152 cleanup() { diff --git a/install/launchers/index.json b/install/launchers/index.json index 8db6e297a..1aaa624f4 100644 --- a/install/launchers/index.json +++ b/install/launchers/index.json @@ -27,8 +27,8 @@ { "dest": "roms/ports/retrobios/install.py", "repo_path": "install.py", - "size": 1125268, - "sha256": "bba0f7cd7677fb7f4d767504412843cf3809c03b91808c7c7772f17758893d50", + "size": 1126407, + "sha256": "192ecdd781447b69967d5d335911fb34887757ee1bf9b6c000ad68a62b17de4b", "executable": false }, { @@ -71,8 +71,8 @@ "muos": { "archive": { "dest": "ARCHIVE/RetroBIOS.muxapp", - "size": 1178895, - "sha256": "814d80f123e59f5192631842d53a4808bf373599ced857f4622612ffbc25322b" + "size": 1180034, + "sha256": "14c4e4b762ec84fe30d2cf0baed58d1022c3423e05118b43dbccfbaff5e4d84a" }, "members": [ { @@ -92,8 +92,8 @@ { "name": "RetroBIOS/install.py", "repo_path": "install.py", - "size": 1125268, - "sha256": "bba0f7cd7677fb7f4d767504412843cf3809c03b91808c7c7772f17758893d50", + "size": 1126407, + "sha256": "192ecdd781447b69967d5d335911fb34887757ee1bf9b6c000ad68a62b17de4b", "executable": false }, { @@ -166,8 +166,8 @@ { "dest": ".config/modules/retrobios/install.py", "repo_path": "install.py", - "size": 1125268, - "sha256": "bba0f7cd7677fb7f4d767504412843cf3809c03b91808c7c7772f17758893d50", + "size": 1126407, + "sha256": "192ecdd781447b69967d5d335911fb34887757ee1bf9b6c000ad68a62b17de4b", "executable": false }, { diff --git a/tests/test_install_filesystems.py b/tests/test_install_filesystems.py new file mode 100644 index 000000000..010011134 --- /dev/null +++ b/tests/test_install_filesystems.py @@ -0,0 +1,64 @@ +"""What an install does for the device and file system it lands on.""" + +from __future__ import annotations + +import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache + +import subprocess +import sys +import unittest +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(REPO_ROOT)) +sys.path.insert(0, str(REPO_ROOT / "tests")) + +from test_install_selection import FakeRepository # noqa: E402 + + +class AndroidMediaScanner(unittest.TestCase): + """Without a .nomedia the BIOS folder's samples, overlays and videos + showed in the phone's gallery and music apps.""" + + def setUp(self): + self.repo = FakeRepository().__enter__() + self.addCleanup(self.repo.__exit__) + + def _run(self, os_name: str, *options: str): + proc = subprocess.run(self.repo.command("--yes", "--no-input", *options), + env={**self.repo.env(), "RETROBIOS_OS": os_name}, + capture_output=True, text=True, timeout=60, + stdin=subprocess.DEVNULL) + return self.repo, proc + + def test_an_android_install_writes_one(self): + repo, proc = self._run("android") + self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) + marker = repo.dest / ".nomedia" + self.assertTrue(marker.is_file()) + self.assertEqual(marker.stat().st_size, 0) + self.assertIn("Added .nomedia", proc.stdout) + + def test_a_second_run_leaves_it_and_says_nothing(self): + self._run("android") + _, proc = self._run("android") + self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) + self.assertNotIn("Added .nomedia", proc.stdout) + # The marker is not a file of the list: the check finds all in place. + _, check = self._run("android", "--check") + self.assertEqual(check.returncode, 0, check.stdout + check.stderr) + + def test_a_check_and_a_dry_run_write_nothing(self): + for options in (("--check",), ("--dry-run",)): + with self.subTest(options=options): + self._run("android", *options) + self.assertFalse((self.repo.dest / ".nomedia").exists()) + + def test_other_systems_get_none(self): + repo, proc = self._run("linux") + self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr) + self.assertFalse((repo.dest / ".nomedia").exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/wiki/faq.md b/wiki/faq.md index f3429d353..4afe294ef 100644 --- a/wiki/faq.md +++ b/wiki/faq.md @@ -101,6 +101,11 @@ Fill it from a computer or point `--from-pack` at your ZIP: [Without a network]( `--from` copies BIOS files from an old card or backup: [Copy from an old card](installer.md#copy-from-an-old-card). +## BIOS pictures or sounds in my phone's gallery? + +An empty file named `.nomedia` in the BIOS folder hides them. The installer +adds it on Android: [Android](installer.md#android). + ## My setup is not in the list - Name your emulator's BIOS folder with `--dest`. diff --git a/wiki/installer.md b/wiki/installer.md index 894674a5d..c9bc26a51 100644 --- a/wiki/installer.md +++ b/wiki/installer.md @@ -1461,6 +1461,13 @@ elsewhere. A custom BIOS directory is still named directly: python install.py --platform retroarch --dest /storage/emulated/0/RetroArch/system ``` +An install on Android leaves an empty `.nomedia` in the BIOS folder. Android's +media scanner hides a folder holding one, and every folder below it +(MediaProvider, `FileUtils.java` `isDirectoryHidden`), so the samples, +overlays and videos of the BIOS folder stay out of the gallery and music +apps. RetroArch writes none. A run that finds one already there leaves it, +and `--check` and `--dry-run` write nothing. + ## On a handheld { #on-a-handheld-the-retrobios-port } RetroBIOS also runs on the handheld itself, in the