List the open-source projects Calendula ships (#284)

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 `<copyright> · <SPDX id>`, 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 <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/284
This commit is contained in:
Jean-Luc Makiola
2026-09-07 20:52:50 +02:00
co-authored by makiolaj
parent c2062da5d8
commit c642457aa3
4 changed files with 154 additions and 1 deletions
@@ -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))
}
}
@@ -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<Attribution> = 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",
),
)
}
@@ -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),
+7
View File
@@ -620,6 +620,8 @@
<string name="settings_license_value">MIT</string>
<string name="settings_about_author">by Jean-Luc Makiola</string>
<string name="settings_about_source">Source</string>
<string name="settings_licences">Open source licenses</string>
<string name="settings_licences_subtitle">The projects Calendula is built on</string>
<string name="settings_about_privacy">Privacy policy</string>
<string name="settings_about_support">Support development</string>
<string name="settings_about_version">Version %1$s</string>
@@ -627,6 +629,11 @@
<string name="settings_report_problem">Report a problem</string>
<string name="settings_report_problem_hint">Send a crash report or open the issue tracker</string>
<!-- Open source licences -->
<string name="licences_title">Open source licenses</string>
<string name="licences_intro">Calendula includes the work below. Tap any entry to get its source code.</string>
<string name="licences_footer">Every project here is included unmodified, under the license shown.</string>
<!-- Calendar manager -->
<string name="calendars_title">Calendars</string>
<string name="calendars_local_header">Your calendars</string>