--- 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