--- a/buildSrc/build.gradle.kts Thu Apr 13 16:09:27 2023 -0400
+++ b/buildSrc/build.gradle.kts Thu Apr 13 17:11:33 2023 -0400
@@ -57,7 +57,7 @@
dependencies {
implementation("com.android.tools.build:gradle:8.0.0")
- implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0")
+ implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20")
implementation("gradle.plugin.com.hierynomus.gradle.plugins:license-gradle-plugin:0.16.1")
implementation("com.geekorum.gradle.avdl:plugin:0.0.3")
--- a/gradle/libs.versions.toml Thu Apr 13 16:09:27 2023 -0400
+++ b/gradle/libs.versions.toml Thu Apr 13 17:11:33 2023 -0400
@@ -1,25 +1,40 @@
[versions]
com-android-application = "8.0.0"
com-android-library = "8.0.0"
-org-jetbrains-kotlin-android = "1.8.0"
-core-ktx = "1.10.0"
+org-jetbrains-kotlin-android = "1.8.20"
junit = "4.13.2"
androidx-test-ext-junit = "1.1.5"
espresso-core = "3.5.1"
appcompat = "1.6.1"
-material = "1.8.0"
okio = "3.3.0"
kotlinx-coroutines = "1.6.4"
+androidx-activity = "1.7.0"
+androidx-navigation = "2.5.3"
+androidx-compose-bom = "2023.04.00"
+androidx-compose-compiler = "1.4.5"
+androidx-lifecycle = "2.6.1"
+geekdroid = "geekttrss-1.6.2"
+
[libraries]
-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" }
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" }
+
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
-material = { group = "com.google.android.material", name = "material", version.ref = "material" }
okio = { module = "com.squareup.okio:okio", version.ref = "okio"}
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines"}
+geekdroid = { module = "com.geekorum.geekdroid:geekdroid", version.ref = "geekdroid" }
+
+androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref="androidx-lifecycle" }
+androidx-activity = { module = "androidx.activity:activity-ktx", version.ref="androidx-activity" }
+androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref="androidx-activity" }
+
+androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidx-compose-bom" }
+androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref="androidx-navigation" }
+
+androidx-compose-material = { module = "androidx.compose.material:material" }
+androidx-compose-material3 = { module = "androidx.compose.material3:material3", version = "1.1.0-beta02" }
[plugins]
--- a/settings.gradle.kts Thu Apr 13 16:09:27 2023 -0400
+++ b/settings.gradle.kts Thu Apr 13 17:11:33 2023 -0400
@@ -10,6 +10,10 @@
repositories {
google()
mavenCentral()
+ // for geekdroid
+ maven {
+ url = uri("https://jitpack.io")
+ }
}
}
--- a/ui/build.gradle.kts Thu Apr 13 16:09:27 2023 -0400
+++ b/ui/build.gradle.kts Thu Apr 13 17:11:33 2023 -0400
@@ -31,13 +31,30 @@
kotlinOptions {
jvmTarget = "1.8"
}
+
+ buildFeatures {
+ compose = true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get()
+ }
}
dependencies {
+ implementation(project(":core"))
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.compose.material)
+ implementation(libs.androidx.lifecycle.viewmodel)
+ implementation(libs.androidx.activity)
+ implementation(libs.androidx.activity.compose)
+ implementation(libs.androidx.navigation.compose)
+ implementation(libs.geekdroid) {
+ //TODO get rid of dagger platform in geekdroid
+ exclude("com.google.dagger", "dagger-platform")
+ }
- implementation(libs.core.ktx)
implementation(libs.appcompat)
- implementation(libs.material)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.espresso.core)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/ui/src/main/java/com/geekorum/aboutoss/ui/OpenSourceDependenciesListScreen.kt Thu Apr 13 17:11:33 2023 -0400
@@ -0,0 +1,114 @@
+/*
+ * AboutOss is a utility library to retrieve and display
+ * opensource licenses in Android applications.
+ *
+ * Copyright (C) 2023 by Frederic-Charles Barthelery.
+ *
+ * This file is part of AboutOss.
+ *
+ * AboutOss is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AboutOss is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AboutOss. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.aboutoss.ui
+
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.Divider
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.ListItem
+import androidx.compose.material.Scaffold
+import androidx.compose.material.Text
+import androidx.compose.material.TopAppBar
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun OpenSourceDependenciesListScreen(
+ viewModel: OpenSourceLicensesViewModel,
+ onDependencyClick: (String) -> Unit,
+ onUpClick: () -> Unit
+) {
+ val dependencies by viewModel.dependenciesList.collectAsState(initial = emptyList())
+ OpenSourceDependenciesListScreen(
+ dependencies = dependencies,
+ onDependencyClick = onDependencyClick,
+ onUpClick = onUpClick
+ )
+}
+
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+fun OpenSourceDependenciesListScreen(
+ dependencies: List<String>,
+ onDependencyClick: (String) -> Unit,
+ onUpClick: () -> Unit
+) {
+ val lazyListState = rememberLazyListState()
+ val hasScrolled by remember {
+ derivedStateOf {
+ lazyListState.firstVisibleItemIndex != 0 || lazyListState.firstVisibleItemScrollOffset > 0
+ }
+ }
+ val topBarElevation by animateDpAsState(
+ if (hasScrolled) 4.dp else 0.dp
+ )
+ Scaffold(topBar = {
+ TopAppBar(title = { Text(stringResource(R.string.title_oss_licenses)) },
+ navigationIcon = {
+ IconButton(onClick = onUpClick) {
+ Icon(
+ Icons.Default.ArrowBack,
+ contentDescription = null
+ )
+ }
+ },
+ elevation = topBarElevation
+ )
+ }) {
+ LazyColumn(Modifier.fillMaxSize(), state = lazyListState, contentPadding = it) {
+ items(dependencies) {
+ Column {
+ ListItem(
+ Modifier
+ .height(64.dp)
+ .clickable(onClick = { onDependencyClick(it) })
+ ) {
+ Text(
+ it, modifier = Modifier.padding(horizontal = 16.dp),
+ overflow = TextOverflow.Ellipsis, maxLines = 1
+ )
+ }
+ Divider(Modifier.padding(horizontal = 16.dp))
+ }
+ }
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/ui/src/main/java/com/geekorum/aboutoss/ui/OpenSourceLicenseScreen.kt Thu Apr 13 17:11:33 2023 -0400
@@ -0,0 +1,177 @@
+/*
+ * AboutOss is a utility library to retrieve and display
+ * opensource licenses in Android applications.
+ *
+ * Copyright (C) 2023 by Frederic-Charles Barthelery.
+ *
+ * This file is part of AboutOss.
+ *
+ * AboutOss is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AboutOss is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AboutOss. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.aboutoss.ui
+
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Scaffold
+import androidx.compose.material.Text
+import androidx.compose.material.TopAppBar
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.UrlAnnotation
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.text.withAnnotation
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.unit.dp
+import androidx.core.net.toUri
+
+@Composable
+fun OpenSourceLicenseScreen(
+ viewModel: OpenSourceLicensesViewModel,
+ dependency: String,
+ onBackClick: () -> Unit,
+) {
+ val context = LocalContext.current
+ val license by viewModel.getLicenseDependency(dependency).collectAsState("")
+ OpenSourceLicenseScreen(
+ dependency = dependency,
+ license = license,
+ onBackClick = onBackClick,
+ onUrlClick = {
+ viewModel.openLinkInBrowser(context, it)
+ },
+ onUrlsFound = {
+ val uris = it.map { uri -> uri.toUri() }
+ viewModel.mayLaunchUrl(*uris.toTypedArray())
+ }
+ )
+}
+
+@OptIn(ExperimentalLayoutApi::class, ExperimentalTextApi::class)
+@Composable
+fun OpenSourceLicenseScreen(
+ dependency: String,
+ license: String,
+ onBackClick: () -> Unit,
+ onUrlClick: (String) -> Unit,
+ onUrlsFound: (List<String>) -> Unit,
+) {
+ val linkifiedLicense = linkifyText(text = license)
+ LaunchedEffect(linkifiedLicense) {
+ val uris =
+ linkifiedLicense.getUrlAnnotations(0, linkifiedLicense.length).map { it.item.url }
+ onUrlsFound(uris)
+ }
+
+ val scrollState = rememberScrollState()
+ val hasScrolled by remember {
+ derivedStateOf { scrollState.value > 0 }
+ }
+ val topBarElevation by animateDpAsState(
+ if (hasScrolled) 4.dp else 0.dp
+ )
+ Scaffold(topBar = {
+ TopAppBar(title = { Text(dependency, overflow = TextOverflow.Ellipsis, maxLines = 1) },
+ navigationIcon = {
+ IconButton(onClick = onBackClick) {
+ Icon(
+ Icons.Default.ArrowBack,
+ contentDescription = null
+ )
+ }
+ },
+ elevation = topBarElevation
+ )
+ }) { paddingValues ->
+ val layoutResult = remember { mutableStateOf<TextLayoutResult?>(null) }
+ val pressIndicator = Modifier.pointerInput(layoutResult, linkifiedLicense) {
+ detectTapGestures { pos ->
+ layoutResult.value?.let { layoutResult ->
+ val posWithScroll = pos.copy(y = pos.y + scrollState.value)
+ val offset = layoutResult.getOffsetForPosition(posWithScroll)
+ linkifiedLicense.getUrlAnnotations(start = offset, end = offset)
+ .firstOrNull()?.let { annotation ->
+ onUrlClick(annotation.item.url)
+ }
+ }
+ }
+ }
+
+ Text(linkifiedLicense,
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .fillMaxSize()
+ .then(pressIndicator)
+ .verticalScroll(scrollState)
+ .padding(paddingValues)
+ .consumeWindowInsets(paddingValues),
+ onTextLayout = {
+ layoutResult.value = it
+ }
+ )
+ }
+}
+
+/**
+ * https://regexr.com/37i6s
+ */
+private val UrlRegexp = """https?://(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_+.~#?&/=]*)""".toRegex()
+
+@OptIn(ExperimentalTextApi::class)
+@Composable
+private fun linkifyText(text: String): AnnotatedString {
+ val style = SpanStyle(
+ color = MaterialTheme.colors.secondary,
+ textDecoration = TextDecoration.Underline
+ )
+ return remember(text, style) {
+ buildAnnotatedString {
+ var currentIdx = 0
+ for (match in UrlRegexp.findAll(text)) {
+ if (currentIdx < match.range.first) {
+ append(text.substring(currentIdx, match.range.first))
+ }
+ val url = text.substring(match.range)
+ withAnnotation(UrlAnnotation(url)) {
+ withStyle(style) {
+ append(url)
+ }
+ }
+ currentIdx = match.range.last + 1
+ }
+ append(text.substring(currentIdx))
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/ui/src/main/java/com/geekorum/aboutoss/ui/OpenSourceLicensesActivity.kt Thu Apr 13 17:11:33 2023 -0400
@@ -0,0 +1,87 @@
+/*
+ * AboutOss is a utility library to retrieve and display
+ * opensource licenses in Android applications.
+ *
+ * Copyright (C) 2023 by Frederic-Charles Barthelery.
+ *
+ * This file is part of AboutOss.
+ *
+ * AboutOss is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AboutOss is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AboutOss. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.aboutoss.ui
+
+import android.net.Uri
+import android.os.Bundle
+import androidx.activity.compose.setContent
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.material.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+
+class OpenSourceLicensesActivity : AppCompatActivity() {
+
+ private val viewModel: OpenSourceLicensesViewModel by viewModels(
+ factoryProducer = {
+ OpenSourceLicensesViewModel.Factory
+ }
+ )
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ MaterialTheme {
+ DependencyNavHost(
+ openSourceLicensesViewModel = viewModel,
+ navigateUp = {
+ onNavigateUp()
+ }
+ )
+ }
+ }
+ }
+}
+
+
+@Composable
+fun DependencyNavHost(
+ openSourceLicensesViewModel: OpenSourceLicensesViewModel,
+ navigateUp: () -> Unit
+) {
+ val navController = rememberNavController()
+ NavHost(navController, startDestination = "dependencies") {
+ composable("dependencies") {
+ OpenSourceDependenciesListScreen(
+ viewModel = openSourceLicensesViewModel,
+ onDependencyClick = {
+ navController.navigate("dependency_license/${Uri.encode(it)}")
+ },
+ onUpClick = navigateUp
+ )
+ }
+ composable("dependency_license/{dependency}") {
+ val dependency = requireNotNull(it.arguments?.getString("dependency"))
+ OpenSourceLicenseScreen(
+ viewModel = openSourceLicensesViewModel,
+ dependency = dependency,
+ onBackClick = {
+ navController.popBackStack()
+ },
+ )
+ }
+ }
+}
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/ui/src/main/java/com/geekorum/aboutoss/ui/OpenSourceLicensesViewModel.kt Thu Apr 13 17:11:33 2023 -0400
@@ -0,0 +1,87 @@
+/*
+ * AboutOss is a utility library to retrieve and display
+ * opensource licenses in Android applications.
+ *
+ * Copyright (C) 2023 by Frederic-Charles Barthelery.
+ *
+ * This file is part of AboutOss.
+ *
+ * AboutOss is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AboutOss is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AboutOss. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.aboutoss.ui
+
+import android.content.Context
+import android.net.Uri
+import androidx.core.net.toUri
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
+import androidx.lifecycle.viewModelScope
+import androidx.lifecycle.viewmodel.initializer
+import androidx.lifecycle.viewmodel.viewModelFactory
+import com.geekorum.aboutoss.core.LicenseInfoRepository
+import com.geekorum.geekdroid.network.BrowserLauncher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+class OpenSourceLicensesViewModel constructor(
+ private val licenseInfoRepository: LicenseInfoRepository,
+ private val browserLauncher: BrowserLauncher,
+) : ViewModel() {
+ init {
+ browserLauncher.warmUp(null)
+ }
+
+ private val licensesInfo = flow {
+ emit(licenseInfoRepository.getLicensesInfo())
+ }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyMap())
+
+ val dependenciesList = licensesInfo.map { licensesInfo ->
+ licensesInfo.keys.sortedBy { it.lowercase() }
+ }
+
+ fun getLicenseDependency(dependency: String) = flow {
+ emit(licenseInfoRepository.getLicenseFor(dependency))
+ }
+
+ fun openLinkInBrowser(context: Context, link: String) {
+ browserLauncher.launchUrl(context, link.toUri(), null as BrowserLauncher.LaunchCustomizer?)
+ }
+
+ fun mayLaunchUrl(vararg uris: Uri) = browserLauncher.mayLaunchUrl(*uris)
+
+ override fun onCleared() {
+ browserLauncher.shutdown()
+ }
+
+ companion object {
+ val Factory = viewModelFactory {
+ initializer {
+ val application = this[APPLICATION_KEY]!!
+ val licenseInfoRepository = LicenseInfoRepository(
+ appContext = application,
+ mainCoroutineDispatcher = Dispatchers.Main,
+ ioCoroutineDispatcher = Dispatchers.IO
+ )
+ val browserLauncher = BrowserLauncher(application, application.packageManager)
+ OpenSourceLicensesViewModel(
+ licenseInfoRepository,
+ browserLauncher
+ )
+ }
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/ui/src/main/res/values/strings.xml Thu Apr 13 17:11:33 2023 -0400
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+ AboutOss is a utility library to retrieve and display
+ opensource licenses in Android applications.
+
+ Copyright (C) 2023 by Frederic-Charles Barthelery.
+
+ This file is part of AboutOss.
+
+ AboutOss is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ AboutOss is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with AboutOss. If not, see <http://www.gnu.org/licenses/>.
+
+-->
+<resources>
+ <string name="title_oss_licenses">Opensource Licenses</string>
+</resources>
\ No newline at end of file