From 7cc76a2068bb7ccff4b92b280fc7691a53915d9c Mon Sep 17 00:00:00 2001 From: papi Date: Tue, 20 Oct 2026 12:17:29 -0400 Subject: [PATCH] fix(android): guard network callback cleanup for API 21 The first public lint run of 1.5.0-beta.2 failed NewApi at NovaGameDetailActivity.kt:597: getSystemService(Class) needs API 23 while Nova supports API 21. Registration already returns below Android 7.0 and is the only place the callback field is set, so the cleanup never reached that call on older devices, but lint cannot see that invariant. The cleanup now takes and clears the callback as before, then makes the same Android 7.0 check explicit before asking for ConnectivityManager. The callback stays nullable and owned by the activity, and API 21 support, lint settings and the baseline are unchanged. The activity is Android only and is not a Steam Frame build input, so the Frame release guard's RELEASE_ONLY set gains exactly its path. The pinned Frame source, tree, bundle and runtime stay as qualified. The existing readback contract test adds three cases: an activity-only change passes, and a near-name file or another Android source is refused before any release call. Checked: the test's embedded Frame controls pass all 30 cases on this tree and refuse the activity-only case against the previous workflow. Android lint and JUnit were not run locally. --- .github/workflows/build.yml | 1 + .../com/papi/nova/ui/NovaGameDetailActivity.kt | 2 ++ .../com/papi/nova/NovaReleaseMetadataTest.kt | 16 +++++++++++++++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b1636aee6..8895d684f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -560,6 +560,7 @@ jobs: ".github/workflows/build.yml", "CHANGELOG.md", "README.md", + "app/src/main/java/com/papi/nova/ui/NovaGameDetailActivity.kt", "docs/screenshots/beta2/nova-control-detail-aurora.webp", "docs/screenshots/beta2/nova-control-detail-director.webp", "docs/screenshots/beta2/nova-play-setup-aurora.webp", diff --git a/app/src/main/java/com/papi/nova/ui/NovaGameDetailActivity.kt b/app/src/main/java/com/papi/nova/ui/NovaGameDetailActivity.kt index b4edd26a1..9d7ef7e8e 100644 --- a/app/src/main/java/com/papi/nova/ui/NovaGameDetailActivity.kt +++ b/app/src/main/java/com/papi/nova/ui/NovaGameDetailActivity.kt @@ -594,6 +594,8 @@ class NovaGameDetailActivity : NovaActivity() { private fun unregisterNetworkChangeCallback() { val callback = networkChangeCallback ?: return networkChangeCallback = null + // Only the Android 7.0 path above registers a callback; the same check here states that for lint. + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.N) return runCatching { getSystemService(android.net.ConnectivityManager::class.java)?.unregisterNetworkCallback(callback) } } diff --git a/app/src/test/java/com/papi/nova/NovaReleaseMetadataTest.kt b/app/src/test/java/com/papi/nova/NovaReleaseMetadataTest.kt index f994981bb..bb3e95fba 100644 --- a/app/src/test/java/com/papi/nova/NovaReleaseMetadataTest.kt +++ b/app/src/test/java/com/papi/nova/NovaReleaseMetadataTest.kt @@ -654,6 +654,17 @@ class ReleaseReadback(unittest.TestCase): (root / path).write_bytes(b"documentation media fixture\n") git("add", path); git("commit", "-qm", "documentation image successor") release = git("rev-parse", "HEAD"); git("tag", "-f", tag) + android_cases = { + "android-activity": "app/src/main/java/com/papi/nova/ui/NovaGameDetailActivity.kt", + "android-activity-near-name": "app/src/main/java/com/papi/nova/ui/NovaGameDetailActivity.kt.orig", + "android-other-source": "app/src/main/java/com/papi/nova/ui/NovaLibraryActivity.kt", + } + if case in android_cases: + path = android_cases[case] + (root / path).parent.mkdir(parents=True, exist_ok=True) + (root / path).write_text("Android-only source fixture\n") + git("add", path); git("commit", "-qm", "Android-only successor") + release = git("rev-parse", "HEAD"); git("tag", "-f", tag) if case == "tag": git("tag", "-f", tag, source) if case == "notes": (root / "body").write_text("different notes\n") if case == "notes-source": (root / "runner/nova-release-notes.md").write_text("stale notes\n") @@ -673,7 +684,8 @@ class ReleaseReadback(unittest.TestCase): self.assertEqual(before, {p.name:p.read_bytes() for p in (root / "remote").iterdir()}) calls = [json.loads(line) for line in (root / "calls").read_text().splitlines()] if (root / "calls").exists() else [] self.assertTrue(all(a[:2] in (["release", "view"], ["release", "download"]) for a in calls)) - if case in ("held", "tree", "runtime-qml", "readme-near-name", "readme-image-near-name", "readme-other-image"): self.assertEqual([], calls) + if case in ("held", "tree", "runtime-qml", "readme-near-name", "readme-image-near-name", "readme-other-image", + "android-activity-near-name", "android-other-source"): self.assertEqual([], calls) print(case + ": expected " + ("PASS" if succeeds else "REFUSE"), flush=True) def test_changed_release_boundaries(self): @@ -689,6 +701,8 @@ class ReleaseReadback(unittest.TestCase): ("readme-image", "preflight", True), ("readme-director", "preflight", True), ("readme-play-setup", "preflight", True), ("readme-provenance", "preflight", True), ("readme-image-near-name", "preflight", False), ("readme-other-image", "preflight", False), + ("android-activity", "preflight", True), ("android-activity-near-name", "preflight", False), + ("android-other-source", "preflight", False), ("tree", "preflight", False), ("tag", "final", False), ("notes", "final", False), ("notes-source", "final", False), ("held", "preflight", False), ("managed-hash", "final", False)):