From c642457aa32b35defe99c7de513cdecda47160a4 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 7 Sep 2026 20:52:50 +0200 Subject: [PATCH] List the open-source projects Calendula ships (#284) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a Settings entry listing the open-source projects Calendula ships, with copyright holder, SPDX licence id and a tap-through to each project's source. Apache-2.0 §4(a)/§4(d) are discharged by what the recipient of the APK is shown, not by a notice in the repository. New `ui/licences/` package mirroring Agendula's: - `OpenSourceLicences.kt` — `Attribution(name, copyright, licence, sourceUrl)` list plus a `Licence` enum (SPDX id + licence URL). Hand-maintained; the KDoc says why, and that adding a dependency means adding it here. - `LicencesScreen.kt` — `CollapsingScaffold` + one `GroupedRow` per entry, summary ` · `, tapping a row opens that project's source. Reached from a new row in the **About** group in `SettingsScreen.kt`, before the privacy row so the group still ends on privacy. It uses `Icons.Default.Code`, since Gavel already belongs to the app's own License row. Entries cover everything with `implementation` scope: AndroidX/Compose/Glance/WorkManager/DataStore/DocumentFile, Kotlin and kotlinx, Dagger and Hilt, Material Design icons, and floret-kit (MIT). Test-only dependencies are not shipped and stay off the list. Strings live in `values/strings.xml`. Deviation from the issue: the footer text differs from Agendula's. Agendula's says every change to vendored code is listed in the linked repository — Calendula modifies nothing it ships, so the footer states that instead. Closes #281 Co-authored-by: Jean-Luc Makiola Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/284 --- .../calendula/ui/licences/LicencesScreen.kt | 60 ++++++++++++++++ .../ui/licences/OpenSourceLicences.kt | 68 +++++++++++++++++++ .../calendula/ui/settings/SettingsScreen.kt | 20 +++++- app/src/main/res/values/strings.xml | 7 ++ 4 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/de/jeanlucmakiola/calendula/ui/licences/LicencesScreen.kt create mode 100644 app/src/main/java/de/jeanlucmakiola/calendula/ui/licences/OpenSourceLicences.kt diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/licences/LicencesScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/licences/LicencesScreen.kt new file mode 100644 index 0000000..da70e2d --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/licences/LicencesScreen.kt @@ -0,0 +1,60 @@ +package de.jeanlucmakiola.calendula.ui.licences + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import de.jeanlucmakiola.calendula.R +import de.jeanlucmakiola.floret.components.CollapsingScaffold +import de.jeanlucmakiola.floret.components.GroupedListInset +import de.jeanlucmakiola.floret.components.GroupedRow +import de.jeanlucmakiola.floret.components.positionOf + +/** + * Third-party attribution. + * + * Reachable from Settings rather than buried, because Apache-2.0 §4(a) is about + * what the *recipient of the binary* is told. Each row opens the project's source. + */ +@Composable +internal fun LicencesScreen(onBack: () -> Unit) { + val uriHandler = LocalUriHandler.current + + CollapsingScaffold( + title = stringResource(R.string.licences_title), + onBack = onBack, + predictiveBack = true, + ) { + Text( + stringResource(R.string.licences_intro), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = GroupedListInset), + ) + Spacer(Modifier.height(16.dp)) + + OpenSourceLicences.ALL.forEachIndexed { index, attribution -> + GroupedRow( + title = attribution.name, + summary = "${attribution.copyright} · ${attribution.licence.spdxId}", + position = positionOf(index, OpenSourceLicences.ALL.size), + onClick = { uriHandler.openUri(attribution.sourceUrl) }, + ) + } + + Spacer(Modifier.height(24.dp)) + Text( + stringResource(R.string.licences_footer), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = GroupedListInset), + ) + Spacer(Modifier.height(24.dp)) + } +} diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/licences/OpenSourceLicences.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/licences/OpenSourceLicences.kt new file mode 100644 index 0000000..737bec6 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/licences/OpenSourceLicences.kt @@ -0,0 +1,68 @@ +package de.jeanlucmakiola.calendula.ui.licences + +/** + * The third-party code Calendula ships, and what each licence obliges us to say. + * + * This screen is a **licence obligation, not an about-page nicety**: + * **Apache-2.0 §4(a)** requires the licence to travel with the work and **§4(d)** + * propagates any `NOTICE` file. What discharges that is what the recipient of the + * APK is shown — a notice only visible to someone reading the repository does not + * count. Nearly everything below is Apache-2.0. + * + * Hand-maintained on purpose. Generators read POM metadata, and POM metadata is + * routinely wrong — the licence name is often not an SPDX id and the licence URL + * frequently 404s, which produces confidently incomplete output. A short honest + * list beats a long generated one that omits the entry that mattered. + * + * **When adding a dependency, add it here.** There is no automated check that can + * tell you that you forgot. Test-only dependencies (JUnit, Truth, Turbine) are not + * shipped in the APK and stay off the list. + */ +data class Attribution( + val name: String, + val copyright: String, + val licence: Licence, + /** Where the source can actually be obtained; what each row links to. */ + val sourceUrl: String, +) + +enum class Licence(val spdxId: String, val url: String) { + APACHE_2("Apache-2.0", "https://www.apache.org/licenses/LICENSE-2.0"), + MIT("MIT", "https://opensource.org/license/mit"), +} + +object OpenSourceLicences { + + val ALL: List = listOf( + Attribution( + name = "AndroidX, Jetpack Compose, Glance, WorkManager, DataStore, DocumentFile", + copyright = "© The Android Open Source Project", + licence = Licence.APACHE_2, + sourceUrl = "https://cs.android.com/androidx/platform/frameworks/support", + ), + Attribution( + name = "Kotlin, kotlinx.coroutines, kotlinx.datetime", + copyright = "© JetBrains s.r.o. and Kotlin Programming Language contributors", + licence = Licence.APACHE_2, + sourceUrl = "https://github.com/JetBrains/kotlin", + ), + Attribution( + name = "Dagger and Hilt", + copyright = "© Google LLC and The Dagger Authors", + licence = Licence.APACHE_2, + sourceUrl = "https://github.com/google/dagger", + ), + Attribution( + name = "Material Design icons", + copyright = "© Google LLC", + licence = Licence.APACHE_2, + sourceUrl = "https://github.com/google/material-design-icons", + ), + Attribution( + name = "floret-kit", + copyright = "© Jean-Luc Makiola", + licence = Licence.MIT, + sourceUrl = "https://codeberg.org/jlmakiola/floret-kit", + ), + ) +} diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt index b000e70..eddae09 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.material.icons.filled.Backup import androidx.compose.material.icons.filled.BugReport import androidx.compose.material.icons.filled.Cake import androidx.compose.material.icons.filled.CalendarMonth +import androidx.compose.material.icons.filled.Code import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Gavel import androidx.compose.material.icons.filled.Language @@ -54,6 +55,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import de.jeanlucmakiola.calendula.R import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec +import de.jeanlucmakiola.calendula.ui.licences.LicencesScreen import de.jeanlucmakiola.floret.components.AboutCard import de.jeanlucmakiola.floret.components.CollapsingScaffold import de.jeanlucmakiola.floret.components.GroupedListInset @@ -67,7 +69,9 @@ import de.jeanlucmakiola.floret.crash.submitCrashReport import de.jeanlucmakiola.floret.locale.AppLanguage /** The settings sub-screens reached from the hub's category rows. */ -private enum class SettingsSection { Appearance, Views, EventForm, Notifications, SpecialDates, Widgets } +private enum class SettingsSection { + Appearance, Views, EventForm, Notifications, SpecialDates, Widgets, Licences +} /** * Settings hub: three labelled groups of category rows, each opening a @@ -146,6 +150,13 @@ fun SettingsScreen( ) { WidgetsScreen(state = state, viewModel = viewModel, onBack = { section = null }) } + AnimatedVisibility( + visible = section == SettingsSection.Licences, + enter = slideInHorizontally(slideSpec) { it } + fadeIn(), + exit = slideOutHorizontally(slideSpec) { it } + fadeOut(), + ) { + LicencesScreen(onBack = { section = null }) + } } } @@ -253,6 +264,13 @@ private fun SettingsHub( icon = Icons.Default.Gavel, position = Position.Middle, ) + GroupedRow( + title = stringResource(R.string.settings_licences), + summary = stringResource(R.string.settings_licences_subtitle), + position = Position.Middle, + leading = { CategoryIcon(Icons.Default.Code, ChipAccent.Neutral) }, + onClick = { onOpenSection(SettingsSection.Licences) }, + ) AboutLinkRow( label = stringResource(R.string.settings_about_privacy), url = stringResource(R.string.about_privacy_url), diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 55f9998..982ee2e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -620,6 +620,8 @@ MIT by Jean-Luc Makiola Source + Open source licenses + The projects Calendula is built on Privacy policy Support development Version %1$s @@ -627,6 +629,11 @@ Report a problem Send a crash report or open the issue tracker + + Open source licenses + Calendula includes the work below. Tap any entry to get its source code. + Every project here is included unmodified, under the license shown. + Calendars Your calendars