From cedc2db32639c37181f17193173d8cfa1a4cf1ff Mon Sep 17 00:00:00 2001 From: Zane Schepke Date: Mon, 21 Apr 2025 15:33:14 -0400 Subject: [PATCH] feat: add app licenses screen --- app/.gitignore | 3 +- app/build.gradle.kts | 24 +++++++++- app/src/main/assets/.gitkeep | 0 .../wireguardautotunnel/MainActivity.kt | 2 + .../data/repository/GitHubUpdateRepository.kt | 4 +- .../wireguardautotunnel/ui/Route.kt | 2 + .../currentNavBackStackEntryAsNavBarState.kt | 9 ++++ .../ui/screens/support/SupportScreen.kt | 12 +++-- .../components/GeneralSupportOptions.kt | 17 +++++++ .../ui/screens/support/license/License.kt | 15 +++++++ .../screens/support/license/LicenseScreen.kt | 45 +++++++++++++++++++ .../support/license/components/LicenseList.kt | 44 ++++++++++++++++++ app/src/main/res/values/strings.xml | 1 + build.gradle.kts | 1 + buildSrc/src/main/kotlin/Constants.kt | 7 ++- gradle/libs.versions.toml | 2 + 16 files changed, 178 insertions(+), 10 deletions(-) create mode 100644 app/src/main/assets/.gitkeep create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/license/License.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/license/LicenseScreen.kt create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/license/components/LicenseList.kt diff --git a/app/.gitignore b/app/.gitignore index 956c004d..c6e4e49e 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1,2 +1,3 @@ /build -/release \ No newline at end of file +/release +/src/main/assets/licenses.json diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c864781d..f12162bb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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("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") } diff --git a/app/src/main/assets/.gitkeep b/app/src/main/assets/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt index a1bce57f..79f57d5b 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/MainActivity.kt @@ -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 { SupportScreen(appViewModel = viewModel) } + composable { LicenseScreen() } composable { AutoTunnelAdvancedScreen(appUiState, viewModel) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/GitHubUpdateRepository.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/GitHubUpdateRepository.kt index 0e910e4b..0651af28 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/GitHubUpdateRepository.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/repository/GitHubUpdateRepository.kt @@ -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 diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Route.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Route.kt index 650f237c..aa7f6897 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Route.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/Route.kt @@ -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 diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentNavBackStackEntryAsNavBarState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentNavBackStackEntryAsNavBarState.kt index b732076e..14aba9e8 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentNavBackStackEntryAsNavBarState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/navigation/components/currentNavBackStackEntryAsNavBarState.kt @@ -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( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt index fd3a80c1..4976506d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/SupportScreen.kt @@ -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 }, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/components/GeneralSupportOptions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/components/GeneralSupportOptions.kt index 69540525..c742164f 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/components/GeneralSupportOptions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/components/GeneralSupportOptions.kt @@ -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) }, + ) + ) } ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/license/License.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/license/License.kt new file mode 100644 index 00000000..57394707 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/license/License.kt @@ -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 = 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) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/license/LicenseScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/license/LicenseScreen.kt new file mode 100644 index 00000000..42022daf --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/license/LicenseScreen.kt @@ -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>(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 { + return withContext(Dispatchers.IO) { + val json = Json { ignoreUnknownKeys = true } + + val jsonResult = context.assets.open("licenses.json").bufferedReader().use { it.readText() } + json.decodeFromString(jsonResult) + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/license/components/LicenseList.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/license/components/LicenseList.kt new file mode 100644 index 00000000..62d88152 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/support/license/components/LicenseList.kt @@ -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) { + 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, + ) + } + } + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f24ee28a..8211399c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -252,4 +252,5 @@ Permission Required This app needs permission to install updates. Allow + Licenses diff --git a/build.gradle.kts b/build.gradle.kts index 2d41f2e1..1d805ee0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 { diff --git a/buildSrc/src/main/kotlin/Constants.kt b/buildSrc/src/main/kotlin/Constants.kt index 47f4d13b..6568aa0a 100644 --- a/buildSrc/src/main/kotlin/Constants.kt +++ b/buildSrc/src/main/kotlin/Constants.kt @@ -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" } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 74ee227a..2caf2d23 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }