Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
876f54c
fix: stop late screen locks, follow app language, and guard edits and…
1window2 Oct 25, 2026
3401e57
build: shrink the APK by two thirds and slim the build
1window2 Oct 25, 2026
86cb995
refactor: drop dead weekly render bookkeeping and redundant export work
1window2 Oct 25, 2026
e8705fe
fix: treat the Sunday 05:00 reset capture as the final Gunsmoke score
1window2 Oct 25, 2026
5d3a485
fix: keep the Sunday 05:00 capture as the closing Standard checkpoint
1window2 Oct 25, 2026
4d83040
fix: start a mid-week joiner's Standard span at the latest absence
1window2 Oct 25, 2026
a01ba73
fix: publish capture STOPPED only after teardown finishes
1window2 Oct 25, 2026
1bf9713
fix: fail native start when the capture thread cannot attach
1window2 Oct 25, 2026
a8ce8b5
fix: roll back a failed restore before releasing the exclusive lock
1window2 Oct 25, 2026
c34837f
fix: check the CSV undo digest where the checkpoint is installed
1window2 Oct 25, 2026
6ef8914
fix: recover interrupted CSV imports at startup and before capture
1window2 Oct 25, 2026
d737d57
Merge capture stop and CSV undo fixes
1window2 Oct 25, 2026
14d293f
fix: never show a Standard day above the 90-point cap
1window2 Oct 25, 2026
3265d73
fix: keep the open final Saturday's patrol unknown in event aggregates
1window2 Oct 25, 2026
a9208f6
fix: report roster comparison deltas across a counter reset as lower …
1window2 Oct 25, 2026
b210798
fix: continue exhausted Gunsmoke runs with interval bounds
1window2 Oct 25, 2026
3c4c34c
Merge weekly table accuracy fixes
1window2 Oct 25, 2026
3c5ad53
fix: keep looking for the game's Recents card while Recents animates in
1window2 Oct 25, 2026
09c9d36
fix: declare the capture VPN unmetered on Android 10 and newer
1window2 Oct 25, 2026
ff9d47a
fix: refresh live screens and harden smaller UI paths
1window2 Oct 25, 2026
ff6985d
fix: keep the new-Platoon prompt open and show why a confirmation failed
1window2 Oct 25, 2026
7aa28bc
fix: bind every claimed flow before replaying a confirmed Platoon
1window2 Oct 25, 2026
7343011
fix: order weekly history by recording order instead of wall clock
1window2 Oct 25, 2026
cfb4bd7
fix: clean up unpublished roster working files and bound the deduplic…
1window2 Oct 25, 2026
268cf6f
fix: cap the unknown payload types kept in capture diagnostics
1window2 Oct 25, 2026
0f98e85
fix: classify a flow as TLS only on a complete record header
1window2 Oct 25, 2026
260fb4b
fix: keep a crash-left WAL and journal with the restore rollback copy
1window2 Oct 25, 2026
69b14b4
refactor: drop the production-unused pending candidate lookup
1window2 Oct 25, 2026
e1cd7e2
fix: disable discarding a new Platoon while its confirmation runs
1window2 Oct 25, 2026
00e01ba
Merge capture and admission fixes
1window2 Oct 25, 2026
37a3eb7
fix: move the new-Platoon prompt to the screen that just opened
1window2 Oct 25, 2026
a319604
fix: never delete a re-registered Platoon scope from the deletion queue
1window2 Oct 25, 2026
db8a0ec
refactor: remove dead automatic and event-linked weekly-note paths
1window2 Oct 25, 2026
128a059
refactor: reuse the member pre-scan for name edits and deletions
1window2 Oct 25, 2026
7cde056
refactor: share the historical roster ordering rule
1window2 Oct 25, 2026
978181f
fix: date inferred boundaries only from activity before the revealing…
1window2 Oct 25, 2026
5eeec3e
Merge persistence and ingestion fixes
1window2 Oct 25, 2026
90259d7
fix: finish pending Platoon deletions off the main thread
1window2 Oct 25, 2026
49848e5
docs: note the 2.7.1 follow-up fixes
1window2 Oct 25, 2026
fbc84ec
fix: start each automatic recording with a fresh game
1window2 Oct 25, 2026
752ce1f
2.7.1
1window2 Oct 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,13 @@ jobs:
- name: Test, lint, and build unsigned verification release
run: >
./gradlew
:protocol:testDebugUnitTest
:protocol:test
:app:testDebugUnitTest
:app:lintDebug
:app:assembleRelease
-PallowUnsignedRelease=true
-x :app:lintVitalRelease
-x :app:lintVitalAnalyzeRelease
--no-daemon
--stacktrace

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ jobs:
run: >
./gradlew
:app:compileDebugKotlin
:protocol:compileDebugKotlin
:protocol:compileKotlin
--init-script .github/codeql/codeql-kotlin-compat.init.gradle
--rerun-tasks
--no-build-cache
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/deploy-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ name: Deploy project page to GitHub Pages
on:
push:
branches: ["main"]
# The page is built from the READMEs alone.
paths:
- README.md
- README_KR.md
- .github/workflows/deploy-pages.yml
workflow_dispatch:

permissions:
Expand Down
68 changes: 68 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,74 @@

All notable changes to mobileGF2logger are documented here.

## 2.7.1 - 2026-10-25

### Fixed

- Stop a scheduled recording from turning the screen off again about ten
minutes after it finished, while the phone is in use.
- Keep every day of a Gunsmoke Frontline week filled when it is recorded once a
day before the reset: when the exact search gives up, the rest of the week now
continues with safe lower bounds and exact weekly totals instead of dropping
later days.
- Treat a capture at exactly the Sunday 05:00 reset as the closing evidence of
the ending week, keep Standard days at or below the 90-point daily cap, credit
a mid-week joiner's merit from the day after they were last absent, and leave
an open final Saturday's patrol unknown instead of missed.
- Show roster comparison deltas across a weekly reset as lower bounds; the
comparison CSV gains two certainty columns.
- Report capture as stopped only after its teardown finishes, so backup,
restore, CSV import and undo, and Platoon changes started with "Stop capture
and continue" never overlap the last packets; Home shows "Stopping capture"
meanwhile.
- Refuse a CSV undo when newer data arrived after its check, recover
interrupted CSV imports at startup and before capture, roll back a failed
restore under the same lock, and never crash at launch over a recovery.
- Show capture notifications and background messages in the app's chosen
language.
- Reject roster CSV files dated in the future.
- Exclude the management database from Android cloud backup and device
transfer, as documented.
- Ignore Discard while weekly edits are saving, and save exactly the edits that
were tapped.
- Keep member, order, comparison, cutline and history screens open, with their
unsaved edits, when only the Platoon's name or banner changes.
- Avoid a crash on phones without the battery-optimization dialog and an empty
server choice after VPN consent when entering standby mode.
- Close the game from Recents reliably on launchers whose Recents opens slowly.
- Start every automatic recording with a fresh game: a client left in the
background from earlier is closed from Recents first, since resuming it can
stop at a "reconnection failed" notice no automatic tap may press.
- Refresh the Platoon and Weekly screens when a roster is saved while they are
open, keep their scroll position across recreation, and clear stale rows after
a failed load.
- Keep the new-Platoon prompt open while a confirmation runs, show why it
failed, and move it to the screen that just opened; a retried confirmation no
longer duplicates history files.
- Declare the capture VPN unmetered, clean up unfinished roster files, bound
stored diagnostics, and tell TLS from game traffic only by a full record
header.
- Date an inferred join or withdrawal from activity only between the rosters
around the change, order weekly history by recording order instead of the
clock, keep a crash-left journal with a restore's rollback copy, and never
delete a Platoon that was registered again while its deletion was queued.
- Save weekly cutlines off the main thread, report a failed packet deletion,
refuse clipboard copies over the system limit, show only the start of huge raw
packets, and ask before removing the Discord webhook (kept through passing
Keystore errors).
- Keep top and bottom bars at full height under the system bars, ellipsize long
capture notices, and finish pending Platoon deletions off the main thread.

### Changed

- Shrink the release APK from 3.2 MB to 1.1 MB: the Platoon emblem and launcher
artwork ship as lossless WebP at the largest size they are drawn, legacy
launcher PNGs and unused metadata are gone, and v1 JAR signing is off.
- Build the protocol module as a plain Kotlin library and skip the duplicate
release lint pass in CI.
- Reuse one scan of the Platoon's weeks for member edits, and remove dead
weekly-note and render bookkeeping paths.

## 2.7.0 - 2026-10-25

### Added
Expand Down
2 changes: 1 addition & 1 deletion PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ is the binding visual direction for the v2.3.0 frontend:
feature guidance, visible progress, top-right Skip, and bottom navigation.

The shipped emblem remains the existing launcher artwork at
`app/src/main/res/drawable-nodpi/ic_launcher_art.png`.
`app/src/main/res/drawable-nodpi/ic_launcher_art.webp`.

## Design principles

Expand Down
20 changes: 17 additions & 3 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,8 @@ android {
applicationId = 'dev.gf2log'
minSdk = 26
targetSdk = 36
versionCode = 20700
versionName = '2.7.0'
versionCode = 20701
versionName = '2.7.1'
testInstrumentationRunner = 'androidx.test.runner.AndroidJUnitRunner'

ndk {
Expand All @@ -178,7 +178,8 @@ android {
storePassword = releaseSigningProperties.getProperty('storePassword')
keyAlias = releaseSigningProperties.getProperty('keyAlias')
keyPassword = releaseSigningProperties.getProperty('keyPassword')
enableV1Signing = true
// minSdk 26 verifies v2+ signatures; the JAR (v1) signature only adds bytes.
enableV1Signing = false
enableV2Signing = true
enableV3Signing = true
enableV4Signing = true
Expand Down Expand Up @@ -209,6 +210,19 @@ android {
}
}

packaging {
resources {
// Kotlin reflection metadata and library version/licence stamps; nothing reads them at
// run time (no kotlin-reflect, no coroutines debug probes).
excludes += [
'**/*.kotlin_builtins',
'DebugProbesKt.bin',
'META-INF/*.version',
'META-INF/androidx/**/LICENSE.txt',
]
}
}

bundle {
language {
enableSplit = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import dev.gf2log.protocol.model.GuildMember
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.EOFException
import java.io.File
import java.io.InputStream
import java.time.Instant
import java.time.LocalDate
Expand Down Expand Up @@ -314,6 +315,62 @@ class PlatoonBackupManagerIntegrationTest {
assertFalse(FilePaths.preRestoreDatabase(context).exists())
}

@Test
fun failedRestoreKeepsTheWalACrashLeftBesideTheCurrentDatabase() {
seedDatabase(ARCHIVED_UID, "Archived member", "archived-source.csv")
val archive = ByteArrayOutputStream().also {
PlatoonBackupManager(context).exportFull(it)
}.toByteArray()
replaceDatabaseWithCurrentState()
settingsStore.replace(currentSettings())
val databaseFile = context.getDatabasePath(storageScope.databaseName)
val crashCopy = File(context.cacheDir, "crash-left-database").apply {
deleteRecursively()
check(mkdirs())
}
PlatoonRepository.withExclusiveDatabase(storageScope) {
// Copying the files while the WAL connection is open leaves the committed update
// only in the WAL, as a process death before the next checkpoint would.
SQLiteDatabase.openDatabase(
databaseFile.path,
null,
SQLiteDatabase.OPEN_READWRITE or SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING,
).use { db ->
db.rawQuery("PRAGMA wal_autocheckpoint = 0", null).use { it.moveToFirst() }
db.execSQL(
"UPDATE members SET note = ? WHERE uid = ?",
arrayOf<Any>(CRASH_LEFT_NOTE, CURRENT_UID),
)
databaseFile.copyTo(File(crashCopy, "database"))
File(databaseFile.path + "-wal").copyTo(File(crashCopy, "wal"))
}
assertTrue(context.deleteDatabase(storageScope.databaseName))
File(crashCopy, "database").copyTo(databaseFile)
File(crashCopy, "wal").copyTo(File(databaseFile.path + "-wal"))
}
crashCopy.deleteRecursively()

assertThrows(IllegalStateException::class.java) {
PlatoonBackupManager(
context = context,
settingsStore = settingsStore,
restoreObserver = { checkpoint ->
if (checkpoint == PlatoonBackupManager.RestoreCheckpoint.DATABASE_INSTALLED) {
error("simulated restore failure")
}
},
).restoreFull(ByteArrayInputStream(archive))
}

PlatoonDatabase(context).use { database ->
val db = database.readableDatabase
assertEquals(1L, count(db, "members", "note = ?", CRASH_LEFT_NOTE))
assertEquals(0L, count(db, "members", "uid = ?", ARCHIVED_UID))
}
assertFalse(FilePaths.preRestoreDatabase(context).exists())
assertFalse(File(FilePaths.preRestoreDatabase(context).path + "-wal").exists())
}

@Test
fun freshInstallFailureRestoresTheMissingDatabaseState() {
seedDatabase(ARCHIVED_UID, "Archived member", "archived-source.csv")
Expand Down Expand Up @@ -686,6 +743,57 @@ class PlatoonBackupManagerIntegrationTest {
assertFalse(FilePaths.csvCheckpointDirectory(context).exists())
}

@Test
fun checkpointInstallRefusesADatabaseThatChangedSinceTheUndoCheck() {
seedDatabase(ARCHIVED_UID, "Before import", "before-import.csv")
val checkpoint = ByteArrayOutputStream().also { output ->
PlatoonBackupManager(context, storageScope).export(output)
}.toByteArray()
val manager = PlatoonBackupManager(context, storageScope)
val checkedDigest = manager.currentDatabaseSha256()
// A capture ingests after the undo's own digest check but before the install.
replaceDatabaseForCheckpoint(CURRENT_UID, "Newer capture", "newer-capture.csv")

assertThrows(IllegalStateException::class.java) {
manager.restoreCheckpoint(ByteArrayInputStream(checkpoint), checkedDigest)
}

PlatoonDatabase(context).use { database ->
assertEquals(1L, count(database.readableDatabase, "members", "uid = ?", CURRENT_UID))
assertEquals(0L, count(database.readableDatabase, "members", "uid = ?", ARCHIVED_UID))
}
assertFalse(FilePaths.restoreTransactionDirectory(context).exists())
}

@Test
fun startupRecoveryFinishesInterruptedImportsAndSurvivesABrokenScope() {
seedDatabase(ARCHIVED_UID, "Before interrupted import", "before-interrupted-import.csv")
val checkpoint = FilePaths.csvCheckpointDirectory(context)
assertTrue(checkpoint.mkdirs())
java.io.FileOutputStream(java.io.File(checkpoint, "platoon.gf2backup")).use { output ->
PlatoonBackupManager(context).export(output)
}
java.io.File(checkpoint, "planned-files.txt").writeText("\n", Charsets.UTF_8)
replaceDatabaseForCheckpoint(CURRENT_UID, "Interrupted import", "interrupted-import.csv")
val brokenScope = PlatoonStorageScope(
PlatoonProfileRegistry(context).upsertDetected(
SupportedGamePackages.DARKWINTER,
GameServerRegion.DARKWINTER_GLOBAL,
dev.gf2log.protocol.model.PlatoonProfileData(303u, "Broken checkpoint", 0u, 0u),
).storageId,
)
// A checkpoint without its archive cannot be recovered and must not stop the others.
assertTrue(java.io.File(brokenScope.rootDirectory(context), "csv-import-checkpoint").mkdirs())

assertEquals(1, CsvImportCheckpointManager.recoverInterruptedImports(context))

PlatoonDatabase(context).use { database ->
assertEquals(1L, count(database.readableDatabase, "members", "uid = ?", ARCHIVED_UID))
assertEquals(0L, count(database.readableDatabase, "members", "uid = ?", CURRENT_UID))
}
assertFalse(checkpoint.exists())
}

@Test
fun malformedCompleteBackupDoesNotMutateExistingState() {
seedDatabase(CURRENT_UID, "Current member", "current-source.csv")
Expand Down Expand Up @@ -1182,6 +1290,7 @@ class PlatoonBackupManagerIntegrationTest {
const val CURRENT_UID = 2002L
const val THIRD_UID = 3003L
const val ARCHIVED_NOTE = "Weekly review"
const val CRASH_LEFT_NOTE = "Committed only to the WAL"
const val INVALID_TARGET_PACKAGE = "not a package"
val PERIOD_START: LocalDate = LocalDate.of(2026, 7, 26)
}
Expand Down
Loading
Loading