mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +01:00
generate third-party licenses at build and show them from settings
Add AboutLibraries, whose Android Gradle plugin collects the licence and attribution of every dependency into a generated resource at build time, and surface it under a new About section in Settings.
This commit is contained in:
@@ -77,6 +77,7 @@ import com.subtracks.ui.playback.MiniPlayer
|
||||
import com.subtracks.ui.playback.NowPlayingRoute
|
||||
import com.subtracks.ui.playback.QueueRoute
|
||||
import com.subtracks.ui.settings.AddSourceRoute
|
||||
import com.subtracks.ui.settings.LicensesRoute
|
||||
import com.subtracks.ui.settings.SettingsRoute
|
||||
import com.subtracks.ui.theme.ArtworkTheme
|
||||
import com.subtracks.ui.theme.rememberArtworkColors
|
||||
@@ -110,6 +111,7 @@ private object Routes {
|
||||
const val LIBRARY = "library"
|
||||
const val SETTINGS = "settings"
|
||||
const val DOWNLOADS = "downloads"
|
||||
const val LICENSES = "licenses"
|
||||
const val ADD_SERVER = "add-server"
|
||||
const val EDIT_SERVER = "edit-server/{sourceId}"
|
||||
const val ALBUM_DETAIL = "album/{albumId}?coverArt={coverArt}"
|
||||
@@ -358,6 +360,7 @@ private fun MainNavigation() {
|
||||
SettingsRoute(
|
||||
onAddServer = { navController.navigate(Routes.ADD_SERVER) },
|
||||
onOpenDownloads = { navController.navigate(Routes.DOWNLOADS) },
|
||||
onOpenLicenses = { navController.navigate(Routes.LICENSES) },
|
||||
onEditServer = { id -> navController.navigate(Routes.editServer(id)) },
|
||||
onBack = { navController.popBackStack() },
|
||||
)
|
||||
@@ -365,6 +368,9 @@ private fun MainNavigation() {
|
||||
composable(Routes.DOWNLOADS) {
|
||||
DownloadsRoute(onBack = { navController.popBackStack() })
|
||||
}
|
||||
composable(Routes.LICENSES) {
|
||||
LicensesRoute(onBack = { navController.popBackStack() })
|
||||
}
|
||||
composable(
|
||||
route = Routes.ALBUM_DETAIL,
|
||||
arguments =
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.subtracks.ui.settings
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.mikepenz.aboutlibraries.Libs
|
||||
import com.mikepenz.aboutlibraries.ui.compose.android.produceLibraries
|
||||
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
||||
import com.subtracks.R
|
||||
|
||||
@Composable
|
||||
fun LicensesRoute(
|
||||
onBack: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val libs by produceLibraries(R.raw.aboutlibraries)
|
||||
LicensesScreen(libs = libs, onBack = onBack, modifier = modifier)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LicensesScreen(
|
||||
libs: Libs?,
|
||||
onBack: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Licenses") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
) { padding ->
|
||||
LibrariesContainer(
|
||||
libraries = libs,
|
||||
modifier = Modifier.fillMaxSize().padding(padding),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,7 @@ private enum class SettingsDialog { WifiQuality, MobileQuality, DownloadQuality,
|
||||
fun SettingsRoute(
|
||||
onAddServer: () -> Unit,
|
||||
onOpenDownloads: () -> Unit,
|
||||
onOpenLicenses: () -> Unit,
|
||||
onEditServer: (Long) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
viewModel: SettingsViewModel = koinViewModel(),
|
||||
@@ -96,6 +97,7 @@ fun SettingsRoute(
|
||||
onOfflineChange = viewModel::setOfflineMode,
|
||||
onAddServer = onAddServer,
|
||||
onOpenDownloads = onOpenDownloads,
|
||||
onOpenLicenses = onOpenLicenses,
|
||||
onBack = onBack,
|
||||
)
|
||||
}
|
||||
@@ -123,6 +125,7 @@ fun SettingsScreen(
|
||||
onOfflineChange: (Boolean) -> Unit = {},
|
||||
onAddServer: () -> Unit,
|
||||
onOpenDownloads: () -> Unit,
|
||||
onOpenLicenses: () -> Unit,
|
||||
onBack: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -277,6 +280,14 @@ fun SettingsScreen(
|
||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||
)
|
||||
}
|
||||
item { SectionHeader("About") }
|
||||
item {
|
||||
ListItem(
|
||||
headlineContent = { Text("Licenses") },
|
||||
modifier = Modifier.clickable(onClick = onOpenLicenses),
|
||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||
)
|
||||
}
|
||||
item { Spacer(Modifier.height(fill)) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,6 +361,7 @@ class AppScreenshotTest {
|
||||
onScrobblingChange = {},
|
||||
onAddServer = {},
|
||||
onOpenDownloads = {},
|
||||
onOpenLicenses = {},
|
||||
onBack = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
package com.subtracks.ui.settings
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.hasScrollToNodeAction
|
||||
import androidx.compose.ui.test.hasText
|
||||
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.onRoot
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollToNode
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
|
||||
import com.github.takahirom.roborazzi.captureRoboImage
|
||||
import com.mikepenz.aboutlibraries.Libs
|
||||
import com.subtracks.data.prefs.StreamQuality
|
||||
import com.subtracks.ui.theme.SubtracksTheme
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.annotation.Config
|
||||
import org.robolectric.annotation.GraphicsMode
|
||||
|
||||
/** Render smoke test: asserts the awaited node exists and writes a PNG for local review; it does not verify pixels. */
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||
@Config(qualifiers = RobolectricDeviceQualifiers.PixelXL)
|
||||
class LicensesScreenTest {
|
||||
@get:Rule
|
||||
val composeRule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun licenses() {
|
||||
val libs = Libs.Builder().withJson(fixture).build()
|
||||
composeRule.setContent {
|
||||
SubtracksTheme {
|
||||
LicensesScreen(libs = libs, onBack = {})
|
||||
}
|
||||
}
|
||||
awaitText("OkHttp")
|
||||
composeRule.onRoot().captureRoboImage("src/test/screenshots/licenses.png")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun settingsOpensLicenses() {
|
||||
var opened = false
|
||||
composeRule.setContent {
|
||||
SubtracksTheme {
|
||||
SettingsScreen(
|
||||
sources = emptyList(),
|
||||
activeSourceId = null,
|
||||
wifiQuality = StreamQuality(),
|
||||
mobileQuality = StreamQuality(),
|
||||
syncConcurrency = 1,
|
||||
downloadQuality = StreamQuality(),
|
||||
downloadOverMetered = false,
|
||||
scrobbling = false,
|
||||
onSelectSource = {},
|
||||
onEditServer = {},
|
||||
onWifiQualityChange = {},
|
||||
onMobileQualityChange = {},
|
||||
onSyncConcurrencyChange = {},
|
||||
onDownloadQualityChange = {},
|
||||
onDownloadOverMeteredChange = {},
|
||||
onScrobblingChange = {},
|
||||
onAddServer = {},
|
||||
onOpenDownloads = {},
|
||||
onOpenLicenses = { opened = true },
|
||||
onBack = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
awaitText("Add server")
|
||||
composeRule.onNode(hasScrollToNodeAction()).performScrollToNode(hasText("Licenses"))
|
||||
composeRule.onNodeWithText("Licenses").performClick()
|
||||
composeRule.waitForIdle()
|
||||
assertTrue(opened)
|
||||
}
|
||||
|
||||
private fun awaitText(text: String) {
|
||||
composeRule.waitUntil(timeoutMillis = 5_000) {
|
||||
composeRule.onAllNodesWithText(text).fetchSemanticsNodes().isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private val fixture =
|
||||
"""
|
||||
{
|
||||
"libraries": [
|
||||
{
|
||||
"uniqueId": "com.squareup.okhttp3:okhttp",
|
||||
"artifactVersion": "5.5.0",
|
||||
"name": "OkHttp",
|
||||
"description": "Square's HTTP client",
|
||||
"website": "https://square.github.io/okhttp/",
|
||||
"licenses": ["Apache-2.0"]
|
||||
},
|
||||
{
|
||||
"uniqueId": "io.coil-kt.coil3:coil-compose",
|
||||
"artifactVersion": "3.6.3",
|
||||
"name": "Coil",
|
||||
"description": "Image loading for Android",
|
||||
"website": "https://coil-kt.github.io/coil/",
|
||||
"licenses": ["Apache-2.0"]
|
||||
},
|
||||
{
|
||||
"uniqueId": "com.google.guava:guava",
|
||||
"artifactVersion": "33.0.0",
|
||||
"name": "Guava",
|
||||
"description": "Google core libraries",
|
||||
"website": "https://github.com/google/guava",
|
||||
"licenses": ["MIT"]
|
||||
}
|
||||
],
|
||||
"licenses": {
|
||||
"Apache-2.0": {
|
||||
"name": "Apache License 2.0",
|
||||
"url": "https://spdx.org/licenses/Apache-2.0.html",
|
||||
"spdxId": "Apache-2.0",
|
||||
"hash": "Apache-2.0"
|
||||
},
|
||||
"MIT": {
|
||||
"name": "MIT License",
|
||||
"url": "https://spdx.org/licenses/MIT.html",
|
||||
"spdxId": "MIT",
|
||||
"hash": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
}
|
||||
Reference in New Issue
Block a user