Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 13 additions & 8 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read

env:
GITHUB_TOKEN: ${{ github.token }}
packages: read
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
Expand All @@ -37,7 +41,7 @@ jobs:
cp -f google-services.json.example app/google-services.json
- name: Run unit tests (all modules)
run: |
./gradlew --no-daemon --stacktrace testDebugUnitTest
./gradlew --no-daemon --stacktrace testDebugUnitTest :app:testStandardDebugUnitTest :app:testTraceboxDebugUnitTest
- name: Upload test reports
if: always()
uses: actions/upload-artifact@v4
Expand All @@ -54,6 +58,7 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
Expand All @@ -75,14 +80,14 @@ jobs:
chmod +x gradlew
echo "MAPS_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" > local.properties
cp -f google-services.json.example app/google-services.json
- name: Run debug lint
run: ./gradlew lintDebug --no-daemon --stacktrace --console=plain
- name: Run debug lint for both diagnostics flavors
run: ./gradlew :app:lintStandardDebug :app:lintTraceboxDebug --no-daemon --stacktrace --console=plain
- name: Run release lint
run: ./gradlew :app:lintRelease :app:checkReleaseLintReport --no-daemon --stacktrace --console=plain
- name: Build minified release
run: ./gradlew :app:assembleRelease --no-daemon --stacktrace --console=plain
- name: Build diagnostic release (nominify)
run: ./gradlew assembleRelease_nominify --no-daemon --stacktrace --console=plain
run: ./gradlew :app:lintStandardRelease :app:lintTraceboxRelease :app:checkReleaseLintReport --no-daemon --stacktrace --console=plain
- name: Build minified releases
run: ./gradlew :app:assembleStandardRelease :app:assembleTraceboxRelease --no-daemon --stacktrace --console=plain
- name: Build diagnostic releases (nominify)
run: ./gradlew :app:assembleStandardRelease_nominify :app:assembleTraceboxRelease_nominify --no-daemon --stacktrace --console=plain
- name: Check Room schema drift
run: ./gradlew checkRoomSchemaDrift --no-daemon --stacktrace --console=plain
- name: Upload build reports
Expand Down
104 changes: 61 additions & 43 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,18 @@ android {
resourceConfigurations.addAll(listOf("en", "cs-rCZ"))
}

flavorDimensions += "diagnostics"
productFlavors {
create("standard") {
dimension = "diagnostics"
buildConfigField("boolean", "TRACEBOX_AVAILABLE", "false")
}
create("tracebox") {
dimension = "diagnostics"
buildConfigField("boolean", "TRACEBOX_AVAILABLE", "true")
}
}

compileOptions {
isCoreLibraryDesugaringEnabled = true
}
Expand Down Expand Up @@ -166,45 +178,48 @@ android {
includeInBundle = true
}
}
val releaseLintReport = layout.buildDirectory.file("reports/lint-results-release.xml")
val releaseLintReports = listOf(
layout.buildDirectory.file("reports/lint-results-standardRelease.xml"),
layout.buildDirectory.file("reports/lint-results-traceboxRelease.xml"),
)

tasks.register("checkReleaseLintReport") {
group = "verification"
description = "Fails when app release lint reports unbaselined fatal/error issues."
dependsOn("lintReportRelease")
mustRunAfter("lintRelease")
inputs.file(releaseLintReport)
description = "Fails when either app release flavor has unbaselined fatal/error lint issues."
dependsOn("lintStandardRelease", "lintTraceboxRelease")
inputs.files(releaseLintReports)

doLast {
val report = releaseLintReport.get().asFile
if (!report.isFile) {
throw GradleException("Release lint report was not generated: ${report.absolutePath}")
}

val documentBuilderFactory = DocumentBuilderFactory.newInstance().apply {
setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
isExpandEntityReferences = false
}
val document = documentBuilderFactory.newDocumentBuilder().parse(report)
val issues = document.getElementsByTagName("issue")
val blockingIssues = buildList {
for (index in 0 until issues.length) {
val issue = issues.item(index) as? Element ?: continue
val severity = issue.getAttribute("severity")
if (severity != "Fatal" && severity != "Error") continue

val id = issue.getAttribute("id")
val message = issue.getAttribute("message")
val locations = issue.getElementsByTagName("location")
val location = if (locations.length > 0) {
val element = locations.item(0) as Element
val file = element.getAttribute("file")
val line = element.getAttribute("line")
if (line.isBlank()) file else "$file:$line"
} else {
"no location"
releaseLintReports.forEach { reportProvider ->
val report = reportProvider.get().asFile
if (!report.isFile) {
throw GradleException("Release lint report was not generated: ${report.absolutePath}")
}
val documentBuilderFactory = DocumentBuilderFactory.newInstance().apply {
setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
isExpandEntityReferences = false
}
val document = documentBuilderFactory.newDocumentBuilder().parse(report)
val issues = document.getElementsByTagName("issue")
for (index in 0 until issues.length) {
val issue = issues.item(index) as? Element ?: continue
val severity = issue.getAttribute("severity")
if (severity != "Fatal" && severity != "Error") continue

val id = issue.getAttribute("id")
val message = issue.getAttribute("message")
val locations = issue.getElementsByTagName("location")
val location = if (locations.length > 0) {
val element = locations.item(0) as Element
val file = element.getAttribute("file")
val line = element.getAttribute("line")
if (line.isBlank()) file else "$file:$line"
} else {
"no location"
}
add("${report.name}: [$severity][$id] $message ($location)")
}
add("[$severity][$id] $message ($location)")
}
}

Expand Down Expand Up @@ -265,6 +280,10 @@ dependencies {
implementation(libs.androidx.work.runtime.ktx)
androidTestImplementation(libs.androidx.work.testing)

// Published immutable Tracebox foundation artifact. The source branch and its workflow build
// every AAR from pinned source; this variant never resolves Maven Local.
add("traceboxImplementation", libs.tracebox)

implementation(libs.hilt.navigation.compose)
implementation(libs.hilt.work)

Expand Down Expand Up @@ -354,18 +373,17 @@ tasks.configureEach {
}
}

// Fix for KSP running before R class generation
// Ensure KSP waits for resource processing to complete
// Fix for KSP running before R class generation. Flavored variants use names such as
// kspStandardDebugKotlin and kspTraceboxDebugKotlin, so derive the matching resource task
// instead of assuming an unflavored debug/release task exists.
afterEvaluate {
tasks.named("kspDebugKotlin") {
dependsOn("processDebugResources")
}
tasks.named("kspReleaseKotlin") {
dependsOn("processReleaseResources")
tasks.matching { task ->
task.name.startsWith("ksp") && task.name.endsWith("Kotlin")
}.configureEach {
val variantName = name.removePrefix("ksp").removeSuffix("Kotlin")
tasks.findByName("process${variantName}Resources")?.let { resourceTask ->
dependsOn(resourceTask)
}
}
tasks.findByName("kspDevKotlin")?.dependsOn("processDevResources")
}




8 changes: 8 additions & 0 deletions app/src/main/java/com/adsamcik/tracker/app/Application.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.adsamcik.tracker.app

import android.app.ActivityManager
import android.app.ApplicationExitInfo
import android.content.Context
import android.os.Build
import android.database.sqlite.SQLiteException
import androidx.annotation.MainThread
Expand Down Expand Up @@ -122,6 +123,13 @@ class Application : AndroidApplication(), Configuration.Provider {

private val deferredStartupStarted = AtomicBoolean(false)
private val maintenanceStartupStarted = AtomicBoolean(false)

override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
// The flavor implementation is a no-op in standard builds. Tracebox itself initializes
// disabled and performs durable work off the main thread.
com.adsamcik.tracker.app.tracebox.TraceboxStartup.install(this)
}

override val workManagerConfiguration: Configuration
get() = Configuration.Builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import com.adsamcik.tracker.app.settings.map.MapSettingsScreen
import com.adsamcik.tracker.app.settings.root.RootSettingsScreen

import com.adsamcik.tracker.app.settings.tracking.TrackingSettingsScreen
import com.adsamcik.tracker.app.settings.tracebox.TraceboxSettingsScreen
import com.adsamcik.tracker.shared.utils.style.compose.ridgelineSettle

// Contract: Entry route for settings; manages hierarchical navigation & hosts category screens.
Expand Down Expand Up @@ -118,6 +119,7 @@ fun SettingsRoute(
LaunchedEffect(Unit) { currentScreen = SettingsScreen.Root }
}
SettingsScreen.Debug -> DebugSettingsScreen(onNavigateToDebug)
SettingsScreen.Tracebox -> TraceboxSettingsScreen()
}
}
}
Expand Down Expand Up @@ -153,6 +155,9 @@ sealed class SettingsScreen {
data object Debug : SettingsScreen() {
@Composable override fun title() = stringResource(R.string.settings_debug_title)
}
data object Tracebox : SettingsScreen() {
@Composable override fun title() = stringResource(R.string.settings_tracebox_title)
}

internal fun saveKey(): String = when (this) {
Root -> "root"
Expand All @@ -163,6 +168,7 @@ sealed class SettingsScreen {
Game -> "game"
Statistics -> "statistics"
Debug -> "debug"
Tracebox -> "tracebox"
}

internal companion object {
Expand All @@ -175,6 +181,7 @@ sealed class SettingsScreen {
"game" -> Game
"statistics" -> Statistics
"debug" -> Debug
"tracebox" -> Tracebox
else -> null
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,14 @@ fun SettingsItem(
modifier: Modifier = Modifier,
subtitle: String? = null,
icon: ImageVector? = null,
enabled: Boolean = true,
onClick: () -> Unit
) {
Row(
modifier = modifier
.fillMaxWidth()
.heightIn(min = 56.dp)
.clickable(role = Role.Button, onClick = onClick)
.clickable(enabled = enabled, role = Role.Button, onClick = onClick)
.padding(horizontal = 16.dp, vertical = 14.dp)
.semantics(mergeDescendants = true) {
contentDescription = if (subtitle != null) "$title: $subtitle" else title
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@ internal fun RootSettingsContent(
icon = Icons.Default.Folder,
onClick = { onNavigate(SettingsScreen.Data) }
)
SettingsItem(
title = stringResource(R.string.settings_tracebox_title),
subtitle = stringResource(R.string.settings_tracebox_root_subtitle),
icon = Icons.Default.BugReport,
onClick = { onNavigate(SettingsScreen.Tracebox) },
)
SettingsItem(
title = stringResource(com.adsamcik.tracker.activity.R.string.settings_activity_title),
icon = Icons.AutoMirrored.Filled.DirectionsRun,
Expand Down
Loading
Loading