Repository: element-hq/element-x-android (language: kotlin)

Issue:
F-Droid can't build
### Steps to reproduce

https://gitlab.com/fdroid/fdroiddata/-/jobs/7697782544#L6769

difflog:

```
│ │ │   --- app-fdroid-arm64-v8a-release-signed/res/raw/third_party_license_metadata                                           
│ │ ├── +++ io.element.android.x_40005012/res/raw/third_party_license_metadata                                                 
│ │ │┄ Ordering differences only                                                                                               
│ │ │ @@ -1,103 +1,103 @@                                                                                                      
│ │ │ -0:46 Compose Navigation                                                                                                 
│ │ │  0:46 Android Navigation Runtime Kotlin Extensions                                                                       
│ │ │ +0:46 Compose Navigation                                                                                                 
│ │ │  47:47 coil-gif                                                                                                          
│ │ │  95:50 Sentry SDK                                                                                                        
│ │ │  0:46 Material Components for Android                                                                                    
│ │ │  47:47 FlowRedux                                                                                                         
│ │ │  0:46 Lifecycle-Common                                                                                                   
│ │ │  0:46 Compose UI Preview Tooling                                                                                         
│ │ │  0:46 Compound Android                                                                                                   
│ │ │  0:46 Activity                                                                                                           
│ │ │  47:47 OpusEncoder                                                                                                       
│ │ │  146:0 OpusEncoder                                                                                                       
│ │ │ -0:46 Telephoto: Coil extensions for Zoomable Image                                                                      
│ │ │  147:43 Molecule                                                                                                         
│ │ │ -0:46 Media3 common module                                                                                               
│ │ │ +0:46 Telephoto: Coil extensions for Zoomable Image                                                                      
│ │ │  0:46 zxing-cpp                                                                                                          
│ │ │ +0:46 Media3 common module                                                                                               
│ │ │  0:46 Android Support ExifInterface                                                                                      
│ │ │  0:46 Browser                                                                                                            
│ │ │  0:46 SQLDelight Async Driver Extensions                                                                                 
│ │ │  0:46 Compose Tooling                                                                                                    
│ │ │ -0:46 Telephoto: Zoomable Modifier                                                                                       
│ │ │  0:46 Kotlin Android Extensions Runtime                                                                                  
│ │ │ +0:46 Telephoto: Zoomable Modifier                                                                                       
│ │ │  47:47 MAD StateMachine  
```

### Outcome

already reported a month ago https://github.com/element-hq/element-x-android/issues/3151#issuecomment-2255801566

### Your phone model

_No response_

### Operating system version

_No response_

### Application version and app store

_No response_

### Homeserver

_No response_

### Will you send logs?

Yes

### Are you willing to provide a PR?

No


Discussion hints:
nan

Pull request: Replace OSS licenses plugin with Licensee and some manually done UI.
<!-- Please read [CONTRIBUTING.md](https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->
 
## Content

<!-- Describe shortly what has been changed -->
Use https://github.com/cashapp/licensee to build a licenses file and parse it at runtime to generate licenses screens.

This should fix both configuration cache and reproducible F-droid builds. Closes #3361

Cleanup and remove gplay/fdroid diff on open source licenses.

Co-authored by @jmartinesp
## Motivation and context

<!-- Provide link to the corresponding issue if applicable or explain the context -->
Fix reproducible build.

Provide open source licenses screen on F-Droid build as well, since it does not rely on proprietary code from Google anymore.

## Screenshots / GIFs

<!--
We have screenshot tests in the project, so attaching screenshots to a PR is not mandatory, as far as there
is a Composable Preview covering the changes. In this case, the change will appear in the file diff.
Note that all the UI composables should be covered by a Composable Preview.

Providing a video of the change is still very useful for the reviewer and for the history of the project.

You can use a table like this to show screenshots comparison.
Uncomment this markdown table below and edit the last line `|||`:
|copy screenshot of before here|copy screenshot of after here|

|Before|After|
|-|-|
|||
 -->

## Tests

<!-- Explain how you tested your development -->

- Navigate to settings / about / open source license.

## Tested devices

- [ ] Physical
- [x] Emulator
- OS version(s):

## Checklist

<!-- Depending on the Pull Request content, it can be acceptable if some of the following checkboxes stay unchecked. -->

- [ ] Changes have been tested on an Android device or Android emulator with API 23
- [ ] UI change has been tested on both light and dark themes
- [ ] Accessibility has been taken into account. See https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#accessibility
- [ ] Pull request is based on the develop branch
- [ ] Pull request title will be used in the release note, it clearly define what will change for the user
- [ ] Pull request includes screenshots or videos if containing UI changes
- [ ] Pull request includes a [sign off](https://matrix-org.github.io/synapse/latest/development/contributing_guide.html#sign-off)
- [ ] You've made a self review of your PR


Test command: ./gradlew :tests:konsist:testDebugUnitTest --tests "io.element.android.tests.konsist.KonsistClassNameTest" :features:preferences:impl:testDebugUnitTest --tests "io.element.android.features.preferences.impl.about.AboutPresenterTest" --tests "io.element.android.features.preferences.impl.about.AboutViewTest" :features:licenses:impl:testDebugUnitTest --tests "io.element.android.features.licenses.impl.list.DependencyLicensesListPresenterTest"

Fix patch:
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 7778a3246e6..49641a16cdf 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -17,15 +17,19 @@
 @file:Suppress("UnstableApiUsage")
 
 import com.android.build.api.variant.FilterConfiguration.FilterType.ABI
+import com.android.build.gradle.internal.tasks.factory.dependsOn
+import com.android.build.gradle.tasks.GenerateBuildConfig
+import extension.AssetCopyTask
+import extension.GitBranchNameValueSource
+import extension.GitRevisionValueSource
 import extension.allEnterpriseImpl
 import extension.allFeaturesImpl
 import extension.allLibrariesImpl
 import extension.allServicesImpl
-import extension.gitBranchName
-import extension.gitRevision
 import extension.koverDependencies
 import extension.locales
 import extension.setupKover
+import java.util.Locale
 
 plugins {
     id("io.element.android-compose-application")
@@ -36,7 +40,8 @@ plugins {
     id(libs.plugins.firebaseAppDistribution.get().pluginId)
     alias(libs.plugins.knit)
     id("kotlin-parcelize")
-    id("com.google.android.gms.oss-licenses-plugin")
+    alias(libs.plugins.licensee)
+    alias(libs.plugins.kotlin.serialization)
     // To be able to update the firebase.xml files, uncomment and build the project
     // id("com.google.gms.google-services")
 }
@@ -61,9 +66,6 @@ android {
             abiFilters += listOf("armeabi-v7a", "x86", "arm64-v8a", "x86_64")
         }
 
-        buildConfigField("String", "GIT_REVISION", "\"${gitRevision()}\"")
-        buildConfigField("String", "GIT_BRANCH_NAME", "\"${gitBranchName()}\"")
-
         // Ref: https://developer.android.com/studio/build/configure-apk-splits.html#configure-abi-split
         splits {
             // Configures multiple APKs based on ABI.
@@ -215,6 +217,9 @@ androidComponents {
             output.versionCode.set((output.versionCode.orNull ?: 0) * 10 + abiCode)
         }
     }
+
+    val reportingExtension: ReportingExtension = project.extensions.getByType(ReportingExtension::class.java)
+    configureLicensesTasks(reportingExtension)
 }
 
 // Knit
@@ -259,8 +264,6 @@ dependencies {
     // Comment to not include unified push in the project
     implementation(projects.libraries.pushproviders.unifiedpush)
 
-    "gplayImplementation"(libs.play.services.oss.licenses)
-
     implementation(libs.appyx.core)
     implementation(libs.androidx.splash)
     implementation(libs.androidx.core)
@@ -291,3 +294,51 @@ dependencies {
 
     koverDependencies()
 }
+
+tasks.withType<GenerateBuildConfig>().configureEach {
+    outputs.upToDateWhen { false }
+    val gitRevision = providers.of(GitRevisionValueSource::class.java) {}.get()
+    val gitBranchName = providers.of(GitBranchNameValueSource::class.java) {}.get()
+    android.defaultConfig.buildConfigField("String", "GIT_REVISION", "\"$gitRevision\"")
+    android.defaultConfig.buildConfigField("String", "GIT_BRANCH_NAME", "\"$gitBranchName\"")
+}
+
+licensee {
+    allow("Apache-2.0")
+    allow("MIT")
+    allow("GPL-2.0-with-classpath-exception")
+    allow("BSD-2-Clause")
+    allowUrl("https://opensource.org/licenses/MIT")
+    allowUrl("https://developer.android.com/studio/terms.html")
+    allowUrl("http://openjdk.java.net/legal/gplv2+ce.html")
+    allowUrl("https://www.zetetic.net/sqlcipher/license/")
+    allowUrl("https://jsoup.org/license")
+    allowUrl("https://asm.ow2.io/license.html")
+    ignoreDependencies("com.github.matrix-org", "matrix-analytics-events")
+}
+
+fun Project.configureLicensesTasks(reportingExtension: ReportingExtension) {
+    androidComponents {
+        onVariants { variant ->
+            val capitalizedVariantName = variant.name.replaceFirstChar {
+                if (it.isLowerCase()) {
+                    it.titlecase(Locale.getDefault())
+                } else {
+                    it.toString()
+                }
+            }
+            val artifactsFile = reportingExtension.file("licensee/android$capitalizedVariantName/artifacts.json")
+
+            val copyArtifactsTask =
+                project.tasks.register<AssetCopyTask>("copy${capitalizedVariantName}LicenseeReportToAssets") {
+                    inputFile.set(artifactsFile)
+                    targetFileName.set("licensee-artifacts.json")
+                }
+            variant.sources.assets?.addGeneratedSourceDirectory(
+                copyArtifactsTask,
+                AssetCopyTask::outputDirectory,
+            )
+            copyArtifactsTask.dependsOn("licenseeAndroid$capitalizedVariantName")
+        }
+    }
+}
diff --git a/app/src/gplay/AndroidManifest.xml b/app/src/gplay/AndroidManifest.xml
deleted file mode 100644
index 234003d953d..00000000000
--- a/app/src/gplay/AndroidManifest.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-<manifest xmlns:android="http://schemas.android.com/apk/res/android">
-
-    <application>
-        <activity
-            android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"
-            android:theme="@style/Theme.OssLicenses" />
-        <activity
-            android:name="com.google.android.gms.oss.licenses.OssLicensesActivity"
-            android:theme="@style/Theme.OssLicenses" />
-    </application>
-
-</manifest>
diff --git a/app/src/gplay/kotlin/io/element/android/x/licenses/OssOpenSourcesLicensesProvider.kt b/app/src/gplay/kotlin/io/element/android/x/licenses/OssOpenSourcesLicensesProvider.kt
deleted file mode 100644
index 93848c438d2..00000000000
--- a/app/src/gplay/kotlin/io/element/android/x/licenses/OssOpenSourcesLicensesProvider.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (c) 2024 New Vector Ltd
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package io.element.android.x.licenses
-
-import android.app.Activity
-import android.content.Intent
-import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
-import com.squareup.anvil.annotations.ContributesBinding
-import io.element.android.features.preferences.api.OpenSourceLicensesProvider
-import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.ui.strings.CommonStrings
-import javax.inject.Inject
-
-@ContributesBinding(AppScope::class)
-class OssOpenSourcesLicensesProvider @Inject constructor() : OpenSourceLicensesProvider {
-    override val hasOpenSourceLicenses: Boolean = true
-
-    override fun navigateToOpenSourceLicenses(activity: Activity) {
-        val title = activity.getString(CommonStrings.common_open_source_licenses)
-        OssLicensesMenuActivity.setActivityTitle(title)
-        activity.startActivity(Intent(activity, OssLicensesMenuActivity::class.java))
-    }
-}
diff --git a/app/src/gplay/res/values-night/colors.xml b/app/src/gplay/res/values-night/colors.xml
deleted file mode 100644
index e830bc1cb15..00000000000
--- a/app/src/gplay/res/values-night/colors.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
-  ~ Copyright (c) 2024 New Vector Ltd
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~     https://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<resources>
-
-    <!-- Use a few colors from compoundColorsDark -->
-    <!-- DarkColorTokens.colorThemeBg -->
-    <color name="colorThemeBg">#FF101317</color>
-    <!-- DarkColorTokens.colorGray1400 -->
-    <color name="textPrimary">#FFEBEEF2</color>
-    <!-- DarkColorTokens.colorGray900 -->
-    <color name="textSecondary">#ff808994</color>
-    <!-- DarkColorTokens.colorBlue900 -->
-    <color name="textLinkExternal">#FF4187EB</color>
-
-    <bool name="windowLightStatusBar">false</bool>
-    <bool name="windowLightNavigationBar">false</bool>
-
-</resources>
diff --git a/app/src/gplay/res/values-v27/themes.xml b/app/src/gplay/res/values-v27/themes.xml
deleted file mode 100644
index 99fe605d0a1..00000000000
--- a/app/src/gplay/res/values-v27/themes.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
-  ~ Copyright (c) 2024 New Vector Ltd
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~     https://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<resources>
-
-    <style name="Theme.OssLicenses.Light.v27" parent="Base.Theme.OssLicenses">
-        <item name="android:windowLightNavigationBar">@bool/windowLightNavigationBar</item>
-    </style>
-
-    <style name="Theme.OssLicenses" parent="Theme.OssLicenses.Light.v27"/>
-
-</resources>
diff --git a/app/src/gplay/res/values/colors.xml b/app/src/gplay/res/values/colors.xml
deleted file mode 100644
index 04f5f153d19..00000000000
--- a/app/src/gplay/res/values/colors.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
-  ~ Copyright (c) 2024 New Vector Ltd
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~     https://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<resources>
-
-    <!-- Use a few colors from compoundColorsLight -->
-    <!-- LightColorTokens.colorThemeBg -->
-    <color name="colorThemeBg">#FFFFFFFF</color>
-    <!-- LightColorTokens.colorGray1400 -->
-    <color name="textPrimary">#FF1B1D22</color>
-    <!-- LightColorTokens.colorGray900 -->
-    <color name="textSecondary">#FF656D77</color>
-    <!-- LightColorTokens.colorBlue900 -->
-    <color name="textLinkExternal">#FF0467DD</color>
-
-    <bool name="windowLightStatusBar">true</bool>
-    <bool name="windowLightNavigationBar">true</bool>
-
-</resources>
diff --git a/app/src/gplay/res/values/styles.xml b/app/src/gplay/res/values/styles.xml
deleted file mode 100644
index 1e8c8369196..00000000000
--- a/app/src/gplay/res/values/styles.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
-  ~ Copyright (c) 2024 New Vector Ltd
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~     https://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<resources>
-
-    <style name="NoElevationToolbar" parent="Widget.MaterialComponents.Toolbar">
-        <item name="android:elevation">0dp</item>
-    </style>
-
-</resources>
diff --git a/app/src/gplay/res/values/themes.xml b/app/src/gplay/res/values/themes.xml
deleted file mode 100644
index 95f0429e0da..00000000000
--- a/app/src/gplay/res/values/themes.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
-  ~ Copyright (c) 2024 New Vector Ltd
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~     https://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<resources>
-
-    <style name="Base.Theme.OssLicenses" parent="Theme.MaterialComponents.DayNight">
-        <!-- Background of title bar -->
-        <item name="colorPrimary">@color/colorThemeBg</item>
-        <!-- Background of the screen -->
-        <item name="android:colorBackground">@color/colorThemeBg</item>
-        <!-- Text of the licenses -->
-        <item name="android:textColor">@color/textSecondary</item>
-        <!-- Link text color -->
-        <item name="android:textColorLink">@color/textLinkExternal</item>
-        <!-- Title, back button and license item text color -->
-        <item name="android:textColorPrimary">@color/textPrimary</item>
-        <!-- Background of status bar -->
-        <item name="android:statusBarColor">@color/colorThemeBg</item>
-        <item name="android:windowLightStatusBar">@bool/windowLightStatusBar</item>
-        <!-- Background of navigation bar -->
-        <item name="android:navigationBarColor">@color/colorThemeBg</item>
-        <!-- Try to remove Toolbar elevation, but it does not work :/ -->
-        <item name="toolbarStyle">@style/NoElevationToolbar</item>
-    </style>
-
-    <style name="Theme.OssLicenses" parent="Base.Theme.OssLicenses" />
-
-</resources>
diff --git a/build.gradle.kts b/build.gradle.kts
index a9a0d72462e..22938c9fd5a 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,11 +1,7 @@
-import com.google.devtools.ksp.gradle.KspTask
-import org.apache.tools.ant.taskdefs.optional.ReplaceRegExp
-
 buildscript {
     dependencies {
         classpath(libs.kotlin.gradle.plugin)
         classpath(libs.gms.google.services)
-        classpath(libs.oss.licenses.plugin)
     }
 }
 
@@ -202,24 +198,6 @@ subprojects {
     tasks.findByName("recordPaparazziRelease")?.dependsOn(removeOldScreenshotsTask)
 }
 
-// Workaround for https://github.com/airbnb/Showkase/issues/335
-subprojects {
-    tasks.withType<KspTask> {
-        doLast {
-            fileTree(layout.buildDirectory).apply { include("**/*ShowkaseExtension*.kt") }.files.forEach { file ->
-                ReplaceRegExp().apply {
-                    setMatch("^public fun Showkase.getMetadata")
-                    setReplace("@Suppress(\"DEPRECATION\") public fun Showkase.getMetadata")
-                    setFlags("g")
-                    setByLine(true)
-                    setFile(file)
-                    execute()
-                }
-            }
-        }
-    }
-}
-
 subprojects {
     tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
         kotlinOptions {
diff --git a/features/licenses/api/build.gradle.kts b/features/licenses/api/build.gradle.kts
new file mode 100644
index 00000000000..2ce08c2ce5b
--- /dev/null
+++ b/features/licenses/api/build.gradle.kts
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2024 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+plugins {
+    id("io.element.android-compose-library")
+}
+
+android {
+    namespace = "io.element.android.features.licenses.api"
+}
+
+dependencies {
+    implementation(projects.libraries.architecture)
+    implementation(projects.libraries.designsystem)
+    implementation(projects.libraries.uiStrings)
+}
diff --git a/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/OpenSourceLicensesProvider.kt b/features/licenses/api/src/main/kotlin/io/element/android/features/licenses/api/OpenSourceLicensesEntryPoint.kt
similarity index 71%
rename from features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/OpenSourceLicensesProvider.kt
rename to features/licenses/api/src/main/kotlin/io/element/android/features/licenses/api/OpenSourceLicensesEntryPoint.kt
index 561ad6add0e..6841bbdcea4 100644
--- a/features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/OpenSourceLicensesProvider.kt
+++ b/features/licenses/api/src/main/kotlin/io/element/android/features/licenses/api/OpenSourceLicensesEntryPoint.kt
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-package io.element.android.features.preferences.api
+package io.element.android.features.licenses.api
 
-import android.app.Activity
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
 
-interface OpenSourceLicensesProvider {
-    val hasOpenSourceLicenses: Boolean
-    fun navigateToOpenSourceLicenses(activity: Activity)
+interface OpenSourceLicensesEntryPoint {
+    fun getNode(node: Node, buildContext: BuildContext): Node
 }
diff --git a/features/licenses/impl/build.gradle.kts b/features/licenses/impl/build.gradle.kts
new file mode 100644
index 00000000000..e46548d90f3
--- /dev/null
+++ b/features/licenses/impl/build.gradle.kts
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+    id("io.element.android-compose-library")
+    id("kotlin-parcelize")
+    alias(libs.plugins.anvil)
+    alias(libs.plugins.kotlin.serialization)
+}
+
+android {
+    namespace = "io.element.android.features.licenses.impl"
+}
+
+anvil {
+    generateDaggerFactories.set(true)
+}
+
+dependencies {
+    implementation(projects.anvilannotations)
+    anvil(projects.anvilcodegen)
+    implementation(libs.serialization.json)
+    implementation(projects.libraries.architecture)
+    implementation(projects.libraries.designsystem)
+    implementation(projects.libraries.core)
+    implementation(projects.libraries.uiStrings)
+    api(projects.features.licenses.api)
+    testImplementation(libs.test.junit)
+    testImplementation(libs.coroutines.test)
+    testImplementation(libs.coroutines.core)
+    testImplementation(libs.molecule.runtime)
+    testImplementation(libs.test.truth)
+    testImplementation(libs.test.turbine)
+    testImplementation(projects.libraries.matrix.test)
+    testImplementation(projects.tests.testutils)
+}
diff --git a/app/src/fdroid/kotlin/io/element/android/x/licenses/FdroidOpenSourceLicensesProvider.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/DefaultOpenSourcesLicensesEntryPoint.kt
similarity index 60%
rename from app/src/fdroid/kotlin/io/element/android/x/licenses/FdroidOpenSourceLicensesProvider.kt
rename to features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/DefaultOpenSourcesLicensesEntryPoint.kt
index 2e926f487e5..ea6957ec4f8 100644
--- a/app/src/fdroid/kotlin/io/element/android/x/licenses/FdroidOpenSourceLicensesProvider.kt
+++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/DefaultOpenSourcesLicensesEntryPoint.kt
@@ -14,19 +14,19 @@
  * limitations under the License.
  */
 
-package io.element.android.x.licenses
+package io.element.android.features.licenses.impl
 
-import android.app.Activity
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
 import com.squareup.anvil.annotations.ContributesBinding
-import io.element.android.features.preferences.api.OpenSourceLicensesProvider
+import io.element.android.features.licenses.api.OpenSourceLicensesEntryPoint
+import io.element.android.libraries.architecture.createNode
 import io.element.android.libraries.di.AppScope
 import javax.inject.Inject
 
 @ContributesBinding(AppScope::class)
-class FdroidOpenSourceLicensesProvider @Inject constructor() : OpenSourceLicensesProvider {
-    override val hasOpenSourceLicenses: Boolean = false
-
-    override fun navigateToOpenSourceLicenses(activity: Activity) {
-        error("Not supported, please ensure that hasOpenSourcesLicenses is true before calling this method")
+class DefaultOpenSourcesLicensesEntryPoint @Inject constructor() : OpenSourceLicensesEntryPoint {
+    override fun getNode(node: Node, buildContext: BuildContext): Node {
+        return node.createNode<DependenciesFlowNode>(buildContext)
     }
 }
diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/DependenciesFlowNode.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/DependenciesFlowNode.kt
new file mode 100644
index 00000000000..64ad0d61670
--- /dev/null
+++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/DependenciesFlowNode.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2024 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.licenses.impl
+
+import android.os.Parcelable
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.core.plugin.Plugin
+import com.bumble.appyx.navmodel.backstack.BackStack
+import com.bumble.appyx.navmodel.backstack.operation.push
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import io.element.android.anvilannotations.ContributesNode
+import io.element.android.features.licenses.impl.details.DependenciesDetailsNode
+import io.element.android.features.licenses.impl.list.DependencyLicensesListNode
+import io.element.android.features.licenses.impl.model.DependencyLicenseItem
+import io.element.android.libraries.architecture.BackstackView
+import io.element.android.libraries.architecture.BaseFlowNode
+import io.element.android.libraries.architecture.createNode
+import io.element.android.libraries.di.AppScope
+import kotlinx.parcelize.Parcelize
+
+@ContributesNode(AppScope::class)
+class DependenciesFlowNode @AssistedInject constructor(
+    @Assisted buildContext: BuildContext,
+    @Assisted plugins: List<Plugin>,
+) : BaseFlowNode<DependenciesFlowNode.NavTarget>(
+    backstack = BackStack(
+        initialElement = NavTarget.LicensesList,
+        savedStateMap = buildContext.savedStateMap,
+    ),
+    buildContext = buildContext,
+    plugins = plugins,
+) {
+    sealed interface NavTarget : Parcelable {
+        @Parcelize
+        data object LicensesList : NavTarget
+
+        @Parcelize
+        data class LicenseDetails(val license: DependencyLicenseItem) : NavTarget
+    }
+
+    override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
+        return when (navTarget) {
+            is NavTarget.LicensesList -> {
+                val callback = object : DependencyLicensesListNode.Callback {
+                    override fun onOpenLicense(license: DependencyLicenseItem) {
+                        backstack.push(NavTarget.LicenseDetails(license))
+                    }
+                }
+                createNode<DependencyLicensesListNode>(buildContext, listOf(callback))
+            }
+            is NavTarget.LicenseDetails -> {
+                createNode<DependenciesDetailsNode>(buildContext, listOf(DependenciesDetailsNode.Inputs(navTarget.license)))
+            }
+        }
+    }
+
+    @Composable
+    override fun View(modifier: Modifier) {
+        BackstackView(modifier)
+    }
+}
diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/LicensesProvider.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/LicensesProvider.kt
new file mode 100644
index 00000000000..f601d6e1305
--- /dev/null
+++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/LicensesProvider.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2024 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.licenses.impl
+
+import android.content.Context
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.features.licenses.impl.model.DependencyLicenseItem
+import io.element.android.libraries.core.coroutine.CoroutineDispatchers
+import io.element.android.libraries.di.AppScope
+import io.element.android.libraries.di.ApplicationContext
+import kotlinx.coroutines.withContext
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.decodeFromStream
+import javax.inject.Inject
+
+interface LicensesProvider {
+    suspend fun provides(): List<DependencyLicenseItem>
+}
+
+@ContributesBinding(AppScope::class)
+class AssetLicensesProvider @Inject constructor(
+    @ApplicationContext private val context: Context,
+    private val dispatchers: CoroutineDispatchers,
+) : LicensesProvider {
+    @OptIn(ExperimentalSerializationApi::class)
+    override suspend fun provides(): List<DependencyLicenseItem> {
+        return withContext(dispatchers.io) {
+            context.assets.open("licensee-artifacts.json").use { inputStream ->
+                val json = Json {
+                    ignoreUnknownKeys = true
+                    explicitNulls = false
+                }
+                json.decodeFromStream<List<DependencyLicenseItem>>(inputStream)
+                    .sortedBy { it.safeName.lowercase() }
+            }
+        }
+    }
+}
diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/details/DependenciesDetailsNode.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/details/DependenciesDetailsNode.kt
new file mode 100644
index 00000000000..0db2453eb53
--- /dev/null
+++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/details/DependenciesDetailsNode.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2024 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.licenses.impl.details
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.core.plugin.Plugin
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import io.element.android.anvilannotations.ContributesNode
+import io.element.android.features.licenses.impl.model.DependencyLicenseItem
+import io.element.android.libraries.architecture.NodeInputs
+import io.element.android.libraries.architecture.inputs
+import io.element.android.libraries.di.AppScope
+
+@ContributesNode(AppScope::class)
+class DependenciesDetailsNode @AssistedInject constructor(
+    @Assisted buildContext: BuildContext,
+    @Assisted plugins: List<Plugin>,
+) : Node(
+    buildContext = buildContext,
+    plugins = plugins
+) {
+    data class Inputs(
+        val licenseItem: DependencyLicenseItem,
+    ) : NodeInputs
+
+    private val licenseItem = inputs<Inputs>().licenseItem
+
+    @Composable
+    override fun View(modifier: Modifier) {
+        DependenciesDetailsView(
+            modifier = modifier,
+            licenseItem = licenseItem,
+            onBack = ::navigateUp
+        )
+    }
+}
diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/details/DependenciesDetailsView.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/details/DependenciesDetailsView.kt
new file mode 100644
index 00000000000..cb4136a017f
--- /dev/null
+++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/details/DependenciesDetailsView.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2024 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.licenses.impl.details
+
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import io.element.android.features.licenses.impl.list.aDependencyLicenseItem
+import io.element.android.features.licenses.impl.model.DependencyLicenseItem
+import io.element.android.libraries.designsystem.components.ClickableLinkText
+import io.element.android.libraries.designsystem.components.button.BackButton
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.designsystem.theme.components.ListItem
+import io.element.android.libraries.designsystem.theme.components.Scaffold
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.designsystem.theme.components.TopAppBar
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun DependenciesDetailsView(
+    licenseItem: DependencyLicenseItem,
+    onBack: () -> Unit,
+    modifier: Modifier = Modifier,
+) {
+    Scaffold(
+        modifier = modifier,
+        topBar = {
+            TopAppBar(
+                title = { Text(text = licenseItem.safeName) },
+                navigationIcon = { BackButton(onClick = onBack) },
+            )
+        },
+    ) { contentPadding ->
+        LazyColumn(
+            modifier = Modifier.padding(contentPadding),
+        ) {
+            val licenses = licenseItem.licenses.orEmpty() +
+                licenseItem.unknownLicenses.orEmpty()
+            items(licenses) { license ->
+                val text = buildString {
+                    if (license.name != null) {
+                        append(license.name)
+                        append("\n")
+                        append("\n")
+                    }
+                    if (license.url != null) {
+                        append(license.url)
+                    }
+                }
+                ListItem(
+                    headlineContent = {
+                        ClickableLinkText(
+                            text = text,
+                            interactionSource = remember { MutableInteractionSource() },
+                        )
+                    }
+                )
+            }
+        }
+    }
+}
+
+@PreviewsDayNight
+@Composable
+internal fun DependenciesDetailsViewPreview() = ElementPreview {
+    DependenciesDetailsView(
+        licenseItem = aDependencyLicenseItem(),
+        onBack = {}
+    )
+}
diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListNode.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListNode.kt
new file mode 100644
index 00000000000..5801f3debc4
--- /dev/null
+++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListNode.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2024 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.licenses.impl.list
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.core.plugin.Plugin
+import com.bumble.appyx.core.plugin.plugins
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import io.element.android.anvilannotations.ContributesNode
+import io.element.android.features.licenses.impl.model.DependencyLicenseItem
+import io.element.android.libraries.di.AppScope
+
+@ContributesNode(AppScope::class)
+class DependencyLicensesListNode @AssistedInject constructor(
+    @Assisted buildContext: BuildContext,
+    @Assisted plugins: List<Plugin>,
+    private val presenter: DependencyLicensesListPresenter,
+) : Node(
+    buildContext = buildContext,
+    plugins = plugins
+) {
+    interface Callback : Plugin {
+        fun onOpenLicense(license: DependencyLicenseItem)
+    }
+
+    private fun onOpenLicense(license: DependencyLicenseItem) {
+        plugins<Callback>()
+            .forEach { it.onOpenLicense(license) }
+    }
+
+    @Composable
+    override fun View(modifier: Modifier) {
+        val state = presenter.present()
+        DependencyLicensesListView(
+            state = state,
+            onBackClick = ::navigateUp,
+            onOpenLicense = ::onOpenLicense,
+        )
+    }
+}
diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListPresenter.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListPresenter.kt
new file mode 100644
index 00000000000..21f633aded5
--- /dev/null
+++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListPresenter.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2024 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.licenses.impl.list
+
+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 io.element.android.features.licenses.impl.LicensesProvider
+import io.element.android.features.licenses.impl.model.DependencyLicenseItem
+import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.architecture.Presenter
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.toPersistentList
+import javax.inject.Inject
+
+class DependencyLicensesListPresenter @Inject constructor(
+    private val licensesProvider: LicensesProvider,
+) : Presenter<DependencyLicensesListState> {
+    @Composable
+    override fun present(): DependencyLicensesListState {
+        var licenses by remember {
+            mutableStateOf<AsyncData<ImmutableList<DependencyLicenseItem>>>(AsyncData.Loading())
+        }
+        LaunchedEffect(Unit) {
+            runCatching {
+                licenses = AsyncData.Success(licensesProvider.provides().toPersistentList())
+            }.onFailure {
+                licenses = AsyncData.Failure(it)
+            }
+        }
+        return DependencyLicensesListState(licenses = licenses)
+    }
+}
diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/about/FakeOpenSourceLicensesProvider.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListState.kt
similarity index 62%
rename from features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/about/FakeOpenSourceLicensesProvider.kt
rename to features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListState.kt
index c7f1c25a332..ebd54692edd 100644
--- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/about/FakeOpenSourceLicensesProvider.kt
+++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListState.kt
@@ -14,13 +14,12 @@
  * limitations under the License.
  */
 
-package io.element.android.features.preferences.impl.about
+package io.element.android.features.licenses.impl.list
 
-import android.app.Activity
-import io.element.android.features.preferences.api.OpenSourceLicensesProvider
+import io.element.android.features.licenses.impl.model.DependencyLicenseItem
+import io.element.android.libraries.architecture.AsyncData
+import kotlinx.collections.immutable.ImmutableList
 
-class FakeOpenSourceLicensesProvider(
-    override val hasOpenSourceLicenses: Boolean,
-) : OpenSourceLicensesProvider {
-    override fun navigateToOpenSourceLicenses(activity: Activity) = Unit
-}
+data class DependencyLicensesListState(
+    val licenses: AsyncData<ImmutableList<DependencyLicenseItem>>,
+)
diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListStateProvider.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListStateProvider.kt
new file mode 100644
index 00000000000..fcd83c8f7bd
--- /dev/null
+++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListStateProvider.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2024 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.licenses.impl.list
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.features.licenses.impl.model.DependencyLicenseItem
+import io.element.android.features.licenses.impl.model.License
+import io.element.android.libraries.architecture.AsyncData
+import kotlinx.collections.immutable.persistentListOf
+
+open class DependencyLicensesListStateProvider : PreviewParameterProvider<DependencyLicensesListState> {
+    override val values: Sequence<DependencyLicensesListState>
+        get() = sequenceOf(
+            DependencyLicensesListState(
+                licenses = AsyncData.Loading()
+            ),
+            DependencyLicensesListState(
+                licenses = AsyncData.Failure(Exception("Failed to load licenses"))
+            ),
+            DependencyLicensesListState(
+                licenses = AsyncData.Success(
+                    persistentListOf(
+                        aDependencyLicenseItem(),
+                        aDependencyLicenseItem(name = null),
+                    )
+                )
+            )
+        )
+}
+
+internal fun aDependencyLicenseItem(
+    name: String? = "A dependency",
+) = DependencyLicenseItem(
+    groupId = "org.some.group",
+    artifactId = "a-dependency",
+    version = "1.0.0",
+    name = name,
+    licenses = listOf(
+        License(
+            identifier = "Apache 2.0",
+            name = "Apache 2.0",
+            url = "https://www.apache.org/licenses/LICENSE-2.0"
+        )
+    ),
+    unknownLicenses = listOf(),
+    scm = null,
+)
diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListView.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListView.kt
new file mode 100644
index 00000000000..7fb3b2b5af5
--- /dev/null
+++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListView.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (c) 2024 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.licenses.impl.list
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import io.element.android.features.licenses.impl.model.DependencyLicenseItem
+import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.designsystem.components.button.BackButton
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
+import io.element.android.libraries.designsystem.theme.components.ListItem
+import io.element.android.libraries.designsystem.theme.components.Scaffold
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.designsystem.theme.components.TopAppBar
+import io.element.android.libraries.ui.strings.CommonStrings
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun DependencyLicensesListView(
+    state: DependencyLicensesListState,
+    onBackClick: () -> Unit,
+    onOpenLicense: (DependencyLicenseItem) -> Unit,
+    modifier: Modifier = Modifier,
+) {
+    Scaffold(
+        modifier = modifier,
+        topBar = {
+            TopAppBar(
+                title = { Text(text = stringResource(CommonStrings.common_open_source_licenses)) },
+                navigationIcon = { BackButton(onClick = onBackClick) },
+            )
+        },
+    ) { contentPadding ->
+        LazyColumn(
+            modifier = Modifier
+                .padding(contentPadding)
+                .padding(horizontal = 16.dp)
+        ) {
+            when (state.licenses) {
+                is AsyncData.Failure -> item {
+                    Text(
+                        text = stringResource(CommonStrings.common_error),
+                        modifier = Modifier.padding(16.dp)
+                    )
+                }
+                AsyncData.Uninitialized,
+                is AsyncData.Loading -> item {
+                    Box(
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .padding(top = 64.dp)
+                    ) {
+                        CircularProgressIndicator(
+                            modifier = Modifier.align(Alignment.Center)
+                        )
+                    }
+                }
+                is AsyncData.Success -> items(state.licenses.data) { license ->
+                    ListItem(
+                        headlineContent = { Text(license.safeName) },
+                        supportingContent = {
+                            Text(
+                                buildString {
+                                    append(license.groupId)
+                                    append(":")
+                                    append(license.artifactId)
+                                    append(":")
+                                    append(license.version)
+                                }
+                            )
+                        },
+                        onClick = {
+                            onOpenLicense(license)
+                        }
+                    )
+                }
+            }
+        }
+    }
+}
+
+@PreviewsDayNight
+@Composable
+internal fun DependencyLicensesListViewPreview(
+    @PreviewParameter(DependencyLicensesListStateProvider::class) state: DependencyLicensesListState
+) = ElementPreview {
+    DependencyLicensesListView(
+        state = state,
+        onBackClick = {},
+        onOpenLicense = {},
+    )
+}
diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/model/DependencyLicenseItem.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/model/DependencyLicenseItem.kt
new file mode 100644
index 00000000000..b46a08f26f2
--- /dev/null
+++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/model/DependencyLicenseItem.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2024 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.licenses.impl.model
+
+import android.os.Parcelable
+import kotlinx.parcelize.IgnoredOnParcel
+import kotlinx.parcelize.Parcelize
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+@Parcelize
+data class DependencyLicenseItem(
+    val groupId: String,
+    val artifactId: String,
+    val version: String,
+    @SerialName("spdxLicenses")
+    val licenses: List<License>?,
+    val unknownLicenses: List<License>?,
+    val name: String?,
+    val scm: Scm?,
+) : Parcelable {
+    @IgnoredOnParcel
+    val safeName = name?.takeIf { name -> name != "null" } ?: "$groupId:$artifactId"
+}
+
+@Serializable
+@Parcelize
+data class License(
+    val identifier: String?,
+    val name: String?,
+    val url: String?,
+) : Parcelable
+
+@Serializable
+@Parcelize
+data class Scm(
+    val url: String,
+) : Parcelable
diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts
index ec265d739e9..247efe5dd87 100644
--- a/features/preferences/impl/build.gradle.kts
+++ b/features/preferences/impl/build.gradle.kts
@@ -62,6 +62,7 @@ dependencies {
     implementation(projects.features.lockscreen.api)
     implementation(projects.features.analytics.api)
     implementation(projects.features.ftue.api)
+    implementation(projects.features.licenses.api)
     implementation(projects.features.logout.api)
     implementation(projects.features.roomlist.api)
     implementation(projects.services.analytics.api)
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt
index 6d3661aace7..4c7c243ba49 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt
@@ -29,6 +29,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedInject
 import io.element.android.anvilannotations.ContributesNode
+import io.element.android.features.licenses.api.OpenSourceLicensesEntryPoint
 import io.element.android.features.lockscreen.api.LockScreenEntryPoint
 import io.element.android.features.logout.api.LogoutEntryPoint
 import io.element.android.features.preferences.api.PreferencesEntryPoint
@@ -59,6 +60,7 @@ class PreferencesFlowNode @AssistedInject constructor(
     private val lockScreenEntryPoint: LockScreenEntryPoint,
     private val notificationTroubleShootEntryPoint: NotificationTroubleShootEntryPoint,
     private val logoutEntryPoint: LogoutEntryPoint,
+    private val openSourceLicensesEntryPoint: OpenSourceLicensesEntryPoint,
 ) : BaseFlowNode<PreferencesFlowNode.NavTarget>(
     backstack = BackStack(
         initialElement = plugins.filterIsInstance<PreferencesEntryPoint.Params>().first().initialElement.toNavTarget(),
@@ -106,6 +108,9 @@ class PreferencesFlowNode @AssistedInject constructor(
 
         @Parcelize
         data object SignOut : NavTarget
+
+        @Parcelize
+        data object OssLicenses : NavTarget
     }
 
     override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@@ -170,7 +175,12 @@ class PreferencesFlowNode @AssistedInject constructor(
                 createNode<ConfigureTracingNode>(buildContext)
             }
             NavTarget.About -> {
-                createNode<AboutNode>(buildContext)
+                val callback = object : AboutNode.Callback {
+                    override fun openOssLicenses() {
+                        backstack.push(NavTarget.OssLicenses)
+                    }
+                }
+                createNode<AboutNode>(buildContext, listOf(callback))
             }
             NavTarget.AnalyticsSettings -> {
                 createNode<AnalyticsSettingsNode>(buildContext)
@@ -232,6 +242,9 @@ class PreferencesFlowNode @AssistedInject constructor(
                     .callback(callBack)
                     .build()
             }
+            is NavTarget.OssLicenses -> {
+                openSourceLicensesEntryPoint.getNode(this, buildContext)
+            }
         }
     }
 
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutNode.kt
index 691bb09ae54..8c49bc2cfb0 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutNode.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutNode.kt
@@ -27,7 +27,6 @@ import dagger.assisted.Assisted
 import dagger.assisted.AssistedInject
 import io.element.android.anvilannotations.ContributesNode
 import io.element.android.compound.theme.ElementTheme
-import io.element.android.features.preferences.api.OpenSourceLicensesProvider
 import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
 import io.element.android.libraries.di.SessionScope
 
@@ -36,8 +35,11 @@ class AboutNode @AssistedInject constructor(
     @Assisted buildContext: BuildContext,
     @Assisted plugins: List<Plugin>,
     private val presenter: AboutPresenter,
-    private val openSourceLicensesProvider: OpenSourceLicensesProvider,
 ) : Node(buildContext, plugins = plugins) {
+    interface Callback : Plugin {
+        fun openOssLicenses()
+    }
+
     private fun onElementLegalClick(
         activity: Activity,
         darkTheme: Boolean,
@@ -58,7 +60,7 @@ class AboutNode @AssistedInject constructor(
                 onElementLegalClick(activity, isDark, elementLegal)
             },
             onOpenSourceLicensesClick = {
-                openSourceLicensesProvider.navigateToOpenSourceLicenses(activity)
+                plugins.filterIsInstance<Callback>().forEach { it.openOssLicenses() }
             },
             modifier = modifier
         )
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutPresenter.kt
index 60395784d4d..76c3054f61e 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutPresenter.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutPresenter.kt
@@ -17,18 +17,14 @@
 package io.element.android.features.preferences.impl.about
 
 import androidx.compose.runtime.Composable
-import io.element.android.features.preferences.api.OpenSourceLicensesProvider
 import io.element.android.libraries.architecture.Presenter
 import javax.inject.Inject
 
-class AboutPresenter @Inject constructor(
-    private val openSourceLicensesProvider: OpenSourceLicensesProvider,
-) : Presenter<AboutState> {
+class AboutPresenter @Inject constructor() : Presenter<AboutState> {
     @Composable
     override fun present(): AboutState {
         return AboutState(
             elementLegals = getAllLegals(),
-            hasOpenSourcesLicenses = openSourceLicensesProvider.hasOpenSourceLicenses,
         )
     }
 }
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutState.kt
index a058f3c138d..7aa473d021d 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutState.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutState.kt
@@ -18,5 +18,4 @@ package io.element.android.features.preferences.impl.about
 
 data class AboutState(
     val elementLegals: List<ElementLegal>,
-    val hasOpenSourcesLicenses: Boolean,
 )
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutStateProvider.kt
index 75dbad1fd28..51f150eebb3 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutStateProvider.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutStateProvider.kt
@@ -22,13 +22,11 @@ open class AboutStateProvider : PreviewParameterProvider<AboutState> {
     override val values: Sequence<AboutState>
         get() = sequenceOf(
             anAboutState(),
-            anAboutState(hasOpenSourcesLicenses = true),
         )
 }
 
 fun anAboutState(
-    hasOpenSourcesLicenses: Boolean = false,
+    elementLegals: List<ElementLegal> = getAllLegals(),
 ) = AboutState(
-    elementLegals = getAllLegals(),
-    hasOpenSourcesLicenses = hasOpenSourcesLicenses,
+    elementLegals = elementLegals,
 )
diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutView.kt
index 413ecb6df9c..16b381359fc 100644
--- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutView.kt
+++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutView.kt
@@ -45,12 +45,10 @@ fun AboutView(
                 onClick = { onElementLegalClick(elementLegal) }
             )
         }
-        if (state.hasOpenSourcesLicenses) {
-            PreferenceText(
-                title = stringResource(id = CommonStrings.common_open_source_licenses),
-                onClick = onOpenSourceLicensesClick,
-            )
-        }
+        PreferenceText(
+            title = stringResource(id = CommonStrings.common_open_source_licenses),
+            onClick = onOpenSourceLicensesClick,
+        )
     }
 }
 
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 3e161529599..901151cac87 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -70,7 +70,6 @@ gms_google_services = "com.google.gms:google-services:4.4.2"
 google_firebase_bom = "com.google.firebase:firebase-bom:33.2.0"
 firebase_appdistribution_gradle = { module = "com.google.firebase:firebase-appdistribution-gradle", version.ref = "firebaseAppDistribution" }
 autonomousapps_dependencyanalysis_plugin = { module = "com.autonomousapps:dependency-analysis-gradle-plugin", version.ref = "dependencyAnalysis" }
-oss_licenses_plugin = "com.google.android.gms:oss-licenses-plugin:0.10.6"
 
 # AndroidX
 androidx_core = { module = "androidx.core:core", version.ref = "core" }
@@ -184,7 +183,6 @@ mapbox_android_gestures = "com.mapbox.mapboxsdk:mapbox-android-gestures:0.7.0"
 opusencoder = "io.element.android:opusencoder:1.1.0"
 kotlinpoet = "com.squareup:kotlinpoet:1.18.1"
 zxing_cpp = "io.github.zxing-cpp:android:2.2.0"
-play_services_oss_licenses = "com.google.android.gms:play-services-oss-licenses:17.1.0"
 
 # Analytics
 posthog = "com.posthog:posthog-android:3.6.0"
@@ -234,3 +232,4 @@ sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
 firebaseAppDistribution = { id = "com.google.firebase.appdistribution", version.ref = "firebaseAppDistribution" }
 knit = { id = "org.jetbrains.kotlinx.knit", version = "0.5.0" }
 sonarqube = "org.sonarqube:5.1.0.4882"
+licensee = "app.cash.licensee:1.11.0"
diff --git a/plugins/src/main/kotlin/extension/AssetCopyTask.kt b/plugins/src/main/kotlin/extension/AssetCopyTask.kt
new file mode 100644
index 00000000000..42357e6bc7e
--- /dev/null
+++ b/plugins/src/main/kotlin/extension/AssetCopyTask.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2024 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package extension
+
+import java.io.File
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.CacheableTask
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+
+@CacheableTask
+abstract class AssetCopyTask : DefaultTask() {
+    @get:OutputDirectory
+    abstract val outputDirectory: DirectoryProperty
+
+    @get:PathSensitive(PathSensitivity.RELATIVE)
+    @get:InputFile
+    abstract val inputFile: RegularFileProperty
+
+    @get:Input
+    abstract val targetFileName: Property<String>
+
+    @TaskAction
+    fun action() {
+        println("Copying ${inputFile.get()} to ${outputDirectory.get().asFile}/${targetFileName.get()}")
+        inputFile.get().asFile.copyTo(
+            target = File(
+                outputDirectory.get().asFile,
+                targetFileName.get(),
+            ),
+            overwrite = true,
+        )
+    }
+}
diff --git a/plugins/src/main/kotlin/extension/Utils.kt b/plugins/src/main/kotlin/extension/Utils.kt
index 31fdd4f952c..94977bb274e 100644
--- a/plugins/src/main/kotlin/extension/Utils.kt
+++ b/plugins/src/main/kotlin/extension/Utils.kt
@@ -17,13 +17,35 @@
 package extension
 
 import org.gradle.api.Project
+import org.gradle.api.provider.ValueSource
+import org.gradle.api.provider.ValueSourceParameters
+import org.gradle.process.ExecOperations
 import java.io.ByteArrayOutputStream
 import java.io.IOException
+import javax.inject.Inject
 
-private fun Project.runCommand(cmd: String): String {
+abstract class GitRevisionValueSource : ValueSource<String, ValueSourceParameters.None> {
+    @get:Inject
+    abstract val execOperations: ExecOperations
+
+    override fun obtain(): String? {
+        return execOperations.runCommand("git rev-parse --short=8 HEAD")
+    }
+}
+
+abstract class GitBranchNameValueSource : ValueSource<String, ValueSourceParameters.None> {
+    @get:Inject
+    abstract val execOperations: ExecOperations
+
+    override fun obtain(): String? {
+        return execOperations.runCommand("git rev-parse --abbrev-ref HEAD")
+    }
+}
+
+private fun ExecOperations.runCommand(cmd: String): String {
     val outputStream = ByteArrayOutputStream()
     val errorStream = ByteArrayOutputStream()
-    project.exec {
+    exec {
         commandLine = cmd.split(" ")
         standardOutput = outputStream
         errorOutput = errorStream
@@ -34,7 +56,3 @@ private fun Project.runCommand(cmd: String): String {
     }
     return String(outputStream.toByteArray()).trim()
 }
-
-fun Project.gitRevision() = runCommand("git rev-parse --short=8 HEAD")
-
-fun Project.gitBranchName() = runCommand("git rev-parse --abbrev-ref HEAD")


Test patch:
diff --git a/features/licenses/impl/src/test/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListPresenterTest.kt b/features/licenses/impl/src/test/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListPresenterTest.kt
new file mode 100644
index 00000000000..55cb062ecd9
--- /dev/null
+++ b/features/licenses/impl/src/test/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListPresenterTest.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2024 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.licenses.impl.list
+
+import app.cash.molecule.RecompositionMode
+import app.cash.molecule.moleculeFlow
+import app.cash.turbine.test
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.licenses.impl.model.DependencyLicenseItem
+import io.element.android.libraries.architecture.AsyncData
+import io.element.android.tests.testutils.WarmUpRule
+import kotlinx.coroutines.test.runTest
+import org.junit.Rule
+import org.junit.Test
+
+class DependencyLicensesListPresenterTest {
+    @get:Rule
+    val warmUpRule = WarmUpRule()
+
+    @Test
+    fun `present - initial state, no licenses`() = runTest {
+        val presenter = createPresenter { emptyList() }
+        moleculeFlow(RecompositionMode.Immediate) {
+            presenter.present()
+        }.test {
+            val initialState = awaitItem()
+            assertThat(initialState.licenses).isInstanceOf(AsyncData.Loading::class.java)
+            val finalState = awaitItem()
+            assertThat(finalState.licenses.isSuccess()).isTrue()
+            assertThat(finalState.licenses.dataOrNull()).isEmpty()
+        }
+    }
+
+    @Test
+    fun `present - initial state, one license`() = runTest {
+        val anItem = aDependencyLicenseItem()
+        val presenter = createPresenter {
+            listOf(anItem)
+        }
+        moleculeFlow(RecompositionMode.Immediate) {
+            presenter.present()
+        }.test {
+            val initialState = awaitItem()
+            assertThat(initialState.licenses).isInstanceOf(AsyncData.Loading::class.java)
+            val finalState = awaitItem()
+            assertThat(finalState.licenses.isSuccess()).isTrue()
+            assertThat(finalState.licenses.dataOrNull()!!.size).isEqualTo(1)
+            assertThat(finalState.licenses.dataOrNull()!!.get(0)).isEqualTo(anItem)
+        }
+    }
+
+    private fun createPresenter(
+        provideResult: () -> List<DependencyLicenseItem>
+    ) = DependencyLicensesListPresenter(
+        licensesProvider = FakeLicensesProvider(provideResult),
+    )
+}
diff --git a/features/licenses/impl/src/test/kotlin/io/element/android/features/licenses/impl/list/FakeLicensesProvider.kt b/features/licenses/impl/src/test/kotlin/io/element/android/features/licenses/impl/list/FakeLicensesProvider.kt
new file mode 100644
index 00000000000..2a18788cbd2
--- /dev/null
+++ b/features/licenses/impl/src/test/kotlin/io/element/android/features/licenses/impl/list/FakeLicensesProvider.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2024 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.licenses.impl.list
+
+import io.element.android.features.licenses.impl.LicensesProvider
+import io.element.android.features.licenses.impl.model.DependencyLicenseItem
+import io.element.android.tests.testutils.lambda.lambdaError
+
+class FakeLicensesProvider(
+    private val provideResult: () -> List<DependencyLicenseItem> = { lambdaError() }
+) : LicensesProvider {
+    override suspend fun provides(): List<DependencyLicenseItem> {
+        return provideResult()
+    }
+}
diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/about/AboutPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/about/AboutPresenterTest.kt
index 64a209dd9e4..be2742c80ed 100644
--- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/about/AboutPresenterTest.kt
+++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/about/AboutPresenterTest.kt
@@ -31,25 +31,12 @@ class AboutPresenterTest {
 
     @Test
     fun `present - initial state`() = runTest {
-        val presenter = AboutPresenter(FakeOpenSourceLicensesProvider(hasOpenSourceLicenses = true))
+        val presenter = AboutPresenter()
         moleculeFlow(RecompositionMode.Immediate) {
             presenter.present()
         }.test {
             val initialState = awaitItem()
             assertThat(initialState.elementLegals).isEqualTo(getAllLegals())
-            assertThat(initialState.hasOpenSourcesLicenses).isTrue()
-        }
-    }
-
-    @Test
-    fun `present - initial state, no open source licenses`() = runTest {
-        val presenter = AboutPresenter(FakeOpenSourceLicensesProvider(hasOpenSourceLicenses = false))
-        moleculeFlow(RecompositionMode.Immediate) {
-            presenter.present()
-        }.test {
-            val initialState = awaitItem()
-            assertThat(initialState.elementLegals).isEqualTo(getAllLegals())
-            assertThat(initialState.hasOpenSourcesLicenses).isFalse()
         }
     }
 }
diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/about/AboutViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/about/AboutViewTest.kt
index 851fcdd8e91..d3ca106f4cc 100644
--- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/about/AboutViewTest.kt
+++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/about/AboutViewTest.kt
@@ -19,7 +19,6 @@ package io.element.android.features.preferences.impl.about
 import androidx.activity.ComponentActivity
 import androidx.compose.ui.test.junit4.AndroidComposeTestRule
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
-import androidx.compose.ui.test.onNodeWithText
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import io.element.android.libraries.ui.strings.CommonStrings
 import io.element.android.tests.testutils.EnsureNeverCalled
@@ -61,21 +60,10 @@ class AboutViewTest {
     }
 
     @Test
-    fun `if open source licenses are not available, the entry is not displayed`() {
-        rule.setAboutView(
-            anAboutState(),
-        )
-        val text = rule.activity.getString(CommonStrings.common_open_source_licenses)
-        rule.onNodeWithText(text).assertDoesNotExist()
-    }
-
-    @Test
-    fun `if open source licenses are available, clicking on the entry invokes the expected callback`() {
+    fun `clicking on the open source licenses invokes the expected callback`() {
         ensureCalledOnce { callback ->
             rule.setAboutView(
-                anAboutState(
-                    hasOpenSourcesLicenses = true,
-                ),
+                anAboutState(),
                 onOpenSourceLicensesClick = callback,
             )
             rule.clickOn(CommonStrings.common_open_source_licenses)
diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt
index 8393f1c6fe8..482819e4f2c 100644
--- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt
+++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt
@@ -108,6 +108,7 @@ class KonsistClassNameTest {
                 "Accompanist",
                 "AES",
                 "Android",
+                "Asset",
                 "Database",
                 "DBov",
                 "Default",
diff --git a/tests/uitests/src/test/snapshots/images/features.licenses.impl.details_DependenciesDetailsView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.licenses.impl.details_DependenciesDetailsView_Day_0_en.png
new file mode 100644
index 00000000000..05ae7c88958
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.licenses.impl.details_DependenciesDetailsView_Day_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:55663ac9ab6472fe7f302d7f74af536080764a3e581ba50bb8198b8970b35e2c
+size 19164
diff --git a/tests/uitests/src/test/snapshots/images/features.licenses.impl.details_DependenciesDetailsView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.licenses.impl.details_DependenciesDetailsView_Night_0_en.png
new file mode 100644
index 00000000000..a77bb5c4527
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.licenses.impl.details_DependenciesDetailsView_Night_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:32c9a5768233fd07c69612dd839f64a2674b9f6905dc432400b93ab6c8259a34
+size 18933
diff --git a/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_0_en.png
new file mode 100644
index 00000000000..8d199aae33d
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:563d5fb3d177add8f3234d1cd07b8320e3cf36599234181d765e6263f2a8b401
+size 11353
diff --git a/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_1_en.png
new file mode 100644
index 00000000000..3e28ef04c8f
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6c19cd3e9a9e76ad26789ca882e19574ddd8b979f46e2da970b1a31fd6793b81
+size 10659
diff --git a/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_2_en.png
new file mode 100644
index 00000000000..a66e7fcd958
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Day_2_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dcc448310dd9e586df3d8e27b9384e5047f9ebca04c1adf7be4d6d1a6ec88aa7
+size 28735
diff --git a/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_0_en.png
new file mode 100644
index 00000000000..6898dbbe0ae
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9c3e295887c5cb8cf98f24a7de9418f1a28349df90117587404f830e9e44f261
+size 10998
diff --git a/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_1_en.png
new file mode 100644
index 00000000000..4481510fcb0
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cade141584d56fef5ebe38657c3e953914a4e7c7c7167b114327fcae913e50e7
+size 10324
diff --git a/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_2_en.png
new file mode 100644
index 00000000000..9f5c57aaeaa
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.licenses.impl.list_DependencyLicensesListView_Night_2_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2303d25aa0330a27c53d5349b26f86d965fdd87f64c118b6ec8c72a75aa49de7
+size 28165
diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.about_AboutView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.about_AboutView_Day_0_en.png
index 49935c03ae0..122636abc00 100644
--- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.about_AboutView_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.about_AboutView_Day_0_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:15df9c4d192335ee59c8bf1fb4623c5009761ed01e433b05a75bbf29749d6e99
-size 15549
+oid sha256:af0cf33a8f40d66441e9f0e05120c181d2c0fe046cc12c5c1008c905e2a42a46
+size 19774
diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.about_AboutView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.about_AboutView_Day_1_en.png
deleted file mode 100644
index 122636abc00..00000000000
--- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.about_AboutView_Day_1_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:af0cf33a8f40d66441e9f0e05120c181d2c0fe046cc12c5c1008c905e2a42a46
-size 19774
diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.about_AboutView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.about_AboutView_Night_0_en.png
index fd140b454ec..419bd7ec9fa 100644
--- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.about_AboutView_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.about_AboutView_Night_0_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:b0a6db2f0497df327fd9c5461be7e7589b5bf2f65770f2561948ee96a00b4eac
-size 15038
+oid sha256:1c8a5c260e3ed883a808e5c0c1573a322220dd723f06856bad230f55c829a435
+size 19171
diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.about_AboutView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.about_AboutView_Night_1_en.png
deleted file mode 100644
index 419bd7ec9fa..00000000000
--- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.about_AboutView_Night_1_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:1c8a5c260e3ed883a808e5c0c1573a322220dd723f06856bad230f55c829a435
-size 19171