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
/release
/release
/src/main/assets/licenses.json
+23 -1
View File
@@ -6,6 +6,7 @@ plugins {
alias(libs.plugins.ksp)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.grgit)
alias(libs.plugins.licensee)
}
val versionFile = file("$rootDir/versionCode.txt")
@@ -139,6 +140,14 @@ android {
buildConfig = true
}
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 {
@@ -227,7 +236,6 @@ dependencies {
// util
implementation(libs.qrcode.kotlin)
implementation(libs.semver4j)
implementation(libs.markdown.compose)
// Ktor
implementation(libs.ktor.client.core)
@@ -263,3 +271,17 @@ tasks.whenTaskAdded {
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.logs.LogsScreen
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.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
@@ -283,6 +284,7 @@ class MainActivity : AppCompatActivity() {
composable<Route.Support> {
SupportScreen(appViewModel = viewModel)
}
composable<Route.License> { LicenseScreen() }
composable<Route.AutoTunnelAdvanced> {
AutoTunnelAdvancedScreen(appUiState, viewModel)
}
@@ -47,13 +47,13 @@ class GitHubUpdateRepository(
withContext(ioDispatcher) {
try {
// clean up old files
context.cacheDir?.listFiles()?.forEach { file ->
context.getExternalFilesDir(null)?.listFiles()?.forEach { file ->
if (file.extension == "apk") file.delete()
}
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 totalBytes: Long = response.contentLength() ?: -1L
@@ -31,6 +31,8 @@ sealed class Route {
@Serializable data object Scanner : Route()
@Serializable data object License : Route()
@Serializable data class Config(val id: Int) : Route()
@Serializable
@@ -213,6 +213,15 @@ fun currentNavBackStackEntryAsNavBarState(
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.SettingsAdvanced::class) ->
NavBarState(
@@ -15,7 +15,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
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.ui.common.SectionDivider
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.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
import dev.jeziellago.compose.markdowntext.MarkdownText
@Composable
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)) },
body = {
Column {
MarkdownText(uiState.appUpdate?.releaseNotes ?: "")
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.Top),
modifier = Modifier.fillMaxWidth(),
) {
Text(uiState.appUpdate?.version ?: "")
Text(uiState.appUpdate?.releaseNotes ?: "")
if (uiState.isLoading) {
LinearProgressIndicator(
progress = { uiState.downloadProgress },
@@ -1,20 +1,24 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support.components
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.Policy
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
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.surface.SelectionItem
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.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
@Composable
fun GeneralSupportOptions(context: android.content.Context) {
val navController = LocalNavController.current
SurfaceSelectionGroupButton(
items =
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="install_updated_permission">This app needs permission to install updates.</string>
<string name="allow">Allow</string>
<string name="licenses">Licenses</string>
</resources>
+1
View File
@@ -7,6 +7,7 @@ plugins {
alias(libs.plugins.androidLibrary) apply false
alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.ktfmt)
alias(libs.plugins.licensee) apply false
}
subprojects {
+5 -2
View File
@@ -1,7 +1,7 @@
object Constants {
const val VERSION_NAME = "3.8.3"
const val VERSION_NAME = "3.8.2"
const val JVM_TARGET = "17"
const val VERSION_CODE = 38300
const val VERSION_CODE = 38200
const val TARGET_SDK = 35
const val MIN_SDK = 26
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
@@ -16,4 +16,7 @@ object Constants {
const val NIGHTLY = "nightly"
const val PRERELEASE = "prerelease"
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"
storage = "1.5.0"
ktfmt = "0.22.0"
licensee = "1.12.0"
[libraries]
@@ -117,4 +118,5 @@ androidLibrary = { id = "com.android.library", version.ref = "androidGradlePlugi
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
grgit = { id = "org.ajoberstar.grgit.service", version.ref = "gradlePlugins-grgit" }
ktfmt = { id = "com.ncorti.ktfmt.gradle", version.ref = "ktfmt" }
licensee = { id = "app.cash.licensee", version.ref = "licensee" }