feat: add app licenses screen

This commit is contained in:
Zane Schepke
2025-04-21 15:33:14 -04:00
parent 256e3f7951
commit cedc2db326
16 changed files with 178 additions and 10 deletions
+2 -1
View File
@@ -1,2 +1,3 @@
/build /build
/release /release
/src/main/assets/licenses.json
+23 -1
View File
@@ -6,6 +6,7 @@ plugins {
alias(libs.plugins.ksp) alias(libs.plugins.ksp)
alias(libs.plugins.compose.compiler) alias(libs.plugins.compose.compiler)
alias(libs.plugins.grgit) alias(libs.plugins.grgit)
alias(libs.plugins.licensee)
} }
val versionFile = file("$rootDir/versionCode.txt") val versionFile = file("$rootDir/versionCode.txt")
@@ -139,6 +140,14 @@ android {
buildConfig = true buildConfig = true
} }
packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
licensee {
Constants.allowedLicenses.forEach { allow(it) }
allowUrl(Constants.XZING_LICENSE_URL)
// Fix for qrcode-kotlin (MIT, custom URL)
allowUrl("https://rafaellins.mit-license.org/2021/")
}
} }
dependencies { dependencies {
@@ -227,7 +236,6 @@ dependencies {
// util // util
implementation(libs.qrcode.kotlin) implementation(libs.qrcode.kotlin)
implementation(libs.semver4j) implementation(libs.semver4j)
implementation(libs.markdown.compose)
// Ktor // Ktor
implementation(libs.ktor.client.core) implementation(libs.ktor.client.core)
@@ -263,3 +271,17 @@ tasks.whenTaskAdded {
dependsOn(incrementVersionCode) dependsOn(incrementVersionCode)
} }
} }
tasks.register<Copy>("copyLicenseeJsonToAssets") {
dependsOn("licensee")
val outputAssets = layout.projectDirectory.dir("src/main/assets")
from(layout.buildDirectory.file("reports/licensee/androidFdroidRelease/artifacts.json")) {
rename("artifacts.json", "licenses.json")
}
into(outputAssets)
}
tasks.named("preBuild") { dependsOn("copyLicenseeJsonToAssets") }
View File
@@ -67,6 +67,7 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.settings.appearance.langua
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.KillSwitchScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.killswitch.KillSwitchScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.LogsScreen import com.zaneschepke.wireguardautotunnel.ui.screens.settings.logs.LogsScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
import com.zaneschepke.wireguardautotunnel.ui.screens.support.license.LicenseScreen
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
@@ -283,6 +284,7 @@ class MainActivity : AppCompatActivity() {
composable<Route.Support> { composable<Route.Support> {
SupportScreen(appViewModel = viewModel) SupportScreen(appViewModel = viewModel)
} }
composable<Route.License> { LicenseScreen() }
composable<Route.AutoTunnelAdvanced> { composable<Route.AutoTunnelAdvanced> {
AutoTunnelAdvancedScreen(appUiState, viewModel) AutoTunnelAdvancedScreen(appUiState, viewModel)
} }
@@ -47,13 +47,13 @@ class GitHubUpdateRepository(
withContext(ioDispatcher) { withContext(ioDispatcher) {
try { try {
// clean up old files // clean up old files
context.cacheDir?.listFiles()?.forEach { file -> context.getExternalFilesDir(null)?.listFiles()?.forEach { file ->
if (file.extension == "apk") file.delete() if (file.extension == "apk") file.delete()
} }
val response: HttpResponse = httpClient.get(apkUrl) val response: HttpResponse = httpClient.get(apkUrl)
val apkFile = File(context.cacheDir, fileName) val apkFile = File(context.getExternalFilesDir(null), fileName)
val channel: ByteReadChannel = response.bodyAsChannel() val channel: ByteReadChannel = response.bodyAsChannel()
val totalBytes: Long = response.contentLength() ?: -1L val totalBytes: Long = response.contentLength() ?: -1L
@@ -31,6 +31,8 @@ sealed class Route {
@Serializable data object Scanner : Route() @Serializable data object Scanner : Route()
@Serializable data object License : Route()
@Serializable data class Config(val id: Int) : Route() @Serializable data class Config(val id: Int) : Route()
@Serializable @Serializable
@@ -213,6 +213,15 @@ fun currentNavBackStackEntryAsNavBarState(
route = Route.Support, route = Route.Support,
) )
backStackEntry.isCurrentRoute(Route.License::class) -> {
NavBarState(
showTop = true,
showBottom = true,
topTitle = { Text(stringResource(R.string.licenses)) },
route = Route.License,
)
}
backStackEntry.isCurrentRoute(Route.AutoTunnelAdvanced::class) || backStackEntry.isCurrentRoute(Route.AutoTunnelAdvanced::class) ||
backStackEntry.isCurrentRoute(Route.SettingsAdvanced::class) -> backStackEntry.isCurrentRoute(Route.SettingsAdvanced::class) ->
NavBarState( NavBarState(
@@ -15,7 +15,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.BuildConfig import com.google.zxing.client.android.BuildConfig
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.SectionDivider import com.zaneschepke.wireguardautotunnel.ui.common.SectionDivider
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
@@ -29,7 +29,6 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.requestInstallPackage
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
import dev.jeziellago.compose.markdowntext.MarkdownText
@Composable @Composable
fun SupportScreen(viewModel: SupportViewModel = hiltViewModel(), appViewModel: AppViewModel) { fun SupportScreen(viewModel: SupportViewModel = hiltViewModel(), appViewModel: AppViewModel) {
@@ -63,8 +62,13 @@ fun SupportScreen(viewModel: SupportViewModel = hiltViewModel(), appViewModel: A
}, },
title = { Text(stringResource(R.string.update_available)) }, title = { Text(stringResource(R.string.update_available)) },
body = { body = {
Column { Column(
MarkdownText(uiState.appUpdate?.releaseNotes ?: "") horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
modifier = Modifier.fillMaxWidth(),
) {
Text(uiState.appUpdate?.version ?: "")
Text(uiState.appUpdate?.releaseNotes ?: "")
if (uiState.isLoading) { if (uiState.isLoading) {
LinearProgressIndicator( LinearProgressIndicator(
progress = { uiState.downloadProgress }, progress = { uiState.downloadProgress },
@@ -1,20 +1,24 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support.components package com.zaneschepke.wireguardautotunnel.ui.screens.support.components
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Balance
import androidx.compose.material.icons.filled.Book import androidx.compose.material.icons.filled.Book
import androidx.compose.material.icons.filled.Policy import androidx.compose.material.icons.filled.Policy
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton import com.zaneschepke.wireguardautotunnel.ui.common.button.ForwardButton
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItemLabel import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItemLabel
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
@Composable @Composable
fun GeneralSupportOptions(context: android.content.Context) { fun GeneralSupportOptions(context: android.content.Context) {
val navController = LocalNavController.current
SurfaceSelectionGroupButton( SurfaceSelectionGroupButton(
items = items =
buildList { buildList {
@@ -54,6 +58,19 @@ fun GeneralSupportOptions(context: android.content.Context) {
}, },
) )
) )
add(
SelectionItem(
leadingIcon = Icons.Filled.Balance,
title = {
SelectionItemLabel(
stringResource(R.string.licenses),
SelectionLabelType.TITLE,
)
},
trailing = { ForwardButton { navController.navigate(Route.License) } },
onClick = { navController.navigate(Route.License) },
)
)
} }
) )
} }
@@ -0,0 +1,15 @@
import kotlinx.serialization.Serializable
@Serializable
data class LicenseFileEntry(
val groupId: String,
val artifactId: String,
val version: String,
val name: String,
val spdxLicenses: List<SpdxLicense> = emptyList(),
val scm: Scm? = null,
)
@Serializable data class SpdxLicense(val identifier: String, val name: String, val url: String)
@Serializable data class Scm(val url: String)
@@ -0,0 +1,45 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support.license
import LicenseFileEntry
import android.content.Context
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.zaneschepke.wireguardautotunnel.ui.screens.support.license.components.LicenseList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
@Composable
fun LicenseScreen() {
val context = LocalContext.current
var licenses by remember { mutableStateOf<List<LicenseFileEntry>>(emptyList()) }
LaunchedEffect(Unit) { licenses = loadLicenseeJson(context) }
if (licenses.isEmpty()) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
} else {
LicenseList(licenses)
}
}
suspend fun loadLicenseeJson(context: Context): List<LicenseFileEntry> {
return withContext(Dispatchers.IO) {
val json = Json { ignoreUnknownKeys = true }
val jsonResult = context.assets.open("licenses.json").bufferedReader().use { it.readText() }
json.decodeFromString(jsonResult)
}
}
@@ -0,0 +1,44 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support.license.components
import LicenseFileEntry
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun LicenseList(licenses: List<LicenseFileEntry>) {
LazyColumn(modifier = Modifier.fillMaxSize().padding(16.dp)) {
items(licenses) { entry ->
Column(modifier = Modifier.padding(bottom = 12.dp)) {
Text(
text = "${entry.name} (${entry.version})",
style = MaterialTheme.typography.titleSmall,
)
entry.spdxLicenses.forEach { license ->
Text(
text = license.name,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary,
)
}
entry.scm?.url?.let { scmUrl ->
Text(
text = scmUrl,
style = MaterialTheme.typography.labelSmall,
modifier = Modifier.padding(top = 4.dp),
color = MaterialTheme.colorScheme.secondary,
)
}
}
}
}
}
+1
View File
@@ -252,4 +252,5 @@
<string name="permission_required">Permission Required</string> <string name="permission_required">Permission Required</string>
<string name="install_updated_permission">This app needs permission to install updates.</string> <string name="install_updated_permission">This app needs permission to install updates.</string>
<string name="allow">Allow</string> <string name="allow">Allow</string>
<string name="licenses">Licenses</string>
</resources> </resources>
+1
View File
@@ -7,6 +7,7 @@ plugins {
alias(libs.plugins.androidLibrary) apply false alias(libs.plugins.androidLibrary) apply false
alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.ktfmt) alias(libs.plugins.ktfmt)
alias(libs.plugins.licensee) apply false
} }
subprojects { subprojects {
+5 -2
View File
@@ -1,7 +1,7 @@
object Constants { object Constants {
const val VERSION_NAME = "3.8.3" const val VERSION_NAME = "3.8.2"
const val JVM_TARGET = "17" const val JVM_TARGET = "17"
const val VERSION_CODE = 38300 const val VERSION_CODE = 38200
const val TARGET_SDK = 35 const val TARGET_SDK = 35
const val MIN_SDK = 26 const val MIN_SDK = 26
const val APP_ID = "com.zaneschepke.wireguardautotunnel" const val APP_ID = "com.zaneschepke.wireguardautotunnel"
@@ -16,4 +16,7 @@ object Constants {
const val NIGHTLY = "nightly" const val NIGHTLY = "nightly"
const val PRERELEASE = "prerelease" const val PRERELEASE = "prerelease"
const val TYPE = "type" const val TYPE = "type"
val allowedLicenses = listOf("MIT", "Apache-2.0", "BSD-3-Clause")
const val XZING_LICENSE_URL: String = "https://github.com/journeyapps/zxing-android-embedded/blob/master/COPYING"
} }
+2
View File
@@ -38,6 +38,7 @@ gradlePlugins-grgit = "5.3.0"
material = "1.12.0" material = "1.12.0"
storage = "1.5.0" storage = "1.5.0"
ktfmt = "0.22.0" ktfmt = "0.22.0"
licensee = "1.12.0"
[libraries] [libraries]
@@ -117,4 +118,5 @@ androidLibrary = { id = "com.android.library", version.ref = "androidGradlePlugi
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
grgit = { id = "org.ajoberstar.grgit.service", version.ref = "gradlePlugins-grgit" } grgit = { id = "org.ajoberstar.grgit.service", version.ref = "gradlePlugins-grgit" }
ktfmt = { id = "com.ncorti.ktfmt.gradle", version.ref = "ktfmt" } ktfmt = { id = "com.ncorti.ktfmt.gradle", version.ref = "ktfmt" }
licensee = { id = "app.cash.licensee", version.ref = "licensee" }