Browse Source

Replace OSS licenses plugin with Licensee and some manually done UI.

This should fix both configuration cache and reproducible F-droid builds.

Cleanup and remove gplay/fdroid diff on open source licenses.

Co-authored by @jmartinesp
pull/3381/head
Benoit Marty 2 weeks ago committed by Benoit Marty
parent
commit
965e445d04
  1. 67
      app/build.gradle.kts
  2. 12
      app/src/gplay/AndroidManifest.xml
  3. 37
      app/src/gplay/kotlin/io/element/android/x/licenses/OssOpenSourcesLicensesProvider.kt
  4. 32
      app/src/gplay/res/values-night/colors.xml
  5. 25
      app/src/gplay/res/values-v27/themes.xml
  6. 32
      app/src/gplay/res/values/colors.xml
  7. 23
      app/src/gplay/res/values/styles.xml
  8. 41
      app/src/gplay/res/values/themes.xml
  9. 22
      build.gradle.kts
  10. 28
      features/licenses/api/build.gradle.kts
  11. 10
      features/licenses/api/src/main/kotlin/io/element/android/features/licenses/api/OpenSourceLicensesEntryPoint.kt
  12. 49
      features/licenses/impl/build.gradle.kts
  13. 16
      features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/DefaultOpenSourcesLicensesEntryPoint.kt
  14. 79
      features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/DependenciesFlowNode.kt
  15. 53
      features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/LicensesProvider.kt
  16. 54
      features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/details/DependenciesDetailsNode.kt
  17. 90
      features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/details/DependenciesDetailsView.kt
  18. 58
      features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListNode.kt
  19. 50
      features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListPresenter.kt
  20. 15
      features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListState.kt
  21. 61
      features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListStateProvider.kt
  22. 118
      features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListView.kt
  23. 53
      features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/model/DependencyLicenseItem.kt
  24. 71
      features/licenses/impl/src/test/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListPresenterTest.kt
  25. 29
      features/licenses/impl/src/test/kotlin/io/element/android/features/licenses/impl/list/FakeLicensesProvider.kt
  26. 1
      features/preferences/impl/build.gradle.kts
  27. 15
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt
  28. 8
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutNode.kt
  29. 6
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutPresenter.kt
  30. 1
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutState.kt
  31. 6
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutStateProvider.kt
  32. 2
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutView.kt
  33. 15
      features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/about/AboutPresenterTest.kt
  34. 16
      features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/about/AboutViewTest.kt
  35. 3
      gradle/libs.versions.toml
  36. 55
      plugins/src/main/kotlin/extension/AssetCopyTask.kt
  37. 30
      plugins/src/main/kotlin/extension/Utils.kt
  38. 1
      tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt

67
app/build.gradle.kts

@ -17,15 +17,19 @@
@file:Suppress("UnstableApiUsage") @file:Suppress("UnstableApiUsage")
import com.android.build.api.variant.FilterConfiguration.FilterType.ABI 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.allEnterpriseImpl
import extension.allFeaturesImpl import extension.allFeaturesImpl
import extension.allLibrariesImpl import extension.allLibrariesImpl
import extension.allServicesImpl import extension.allServicesImpl
import extension.gitBranchName
import extension.gitRevision
import extension.koverDependencies import extension.koverDependencies
import extension.locales import extension.locales
import extension.setupKover import extension.setupKover
import java.util.Locale
plugins { plugins {
id("io.element.android-compose-application") id("io.element.android-compose-application")
@ -36,7 +40,8 @@ plugins {
id(libs.plugins.firebaseAppDistribution.get().pluginId) id(libs.plugins.firebaseAppDistribution.get().pluginId)
alias(libs.plugins.knit) alias(libs.plugins.knit)
id("kotlin-parcelize") 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 // To be able to update the firebase.xml files, uncomment and build the project
// id("com.google.gms.google-services") // id("com.google.gms.google-services")
} }
@ -61,9 +66,6 @@ android {
abiFilters += listOf("armeabi-v7a", "x86", "arm64-v8a", "x86_64") 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 // Ref: https://developer.android.com/studio/build/configure-apk-splits.html#configure-abi-split
splits { splits {
// Configures multiple APKs based on ABI. // Configures multiple APKs based on ABI.
@ -215,6 +217,9 @@ androidComponents {
output.versionCode.set((output.versionCode.orNull ?: 0) * 10 + abiCode) output.versionCode.set((output.versionCode.orNull ?: 0) * 10 + abiCode)
} }
} }
val reportingExtension: ReportingExtension = project.extensions.getByType(ReportingExtension::class.java)
configureLicensesTasks(reportingExtension)
} }
// Knit // Knit
@ -259,8 +264,6 @@ dependencies {
// Comment to not include unified push in the project // Comment to not include unified push in the project
implementation(projects.libraries.pushproviders.unifiedpush) implementation(projects.libraries.pushproviders.unifiedpush)
"gplayImplementation"(libs.play.services.oss.licenses)
implementation(libs.appyx.core) implementation(libs.appyx.core)
implementation(libs.androidx.splash) implementation(libs.androidx.splash)
implementation(libs.androidx.core) implementation(libs.androidx.core)
@ -291,3 +294,51 @@ dependencies {
koverDependencies() 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")
}
}
}

12
app/src/gplay/AndroidManifest.xml

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

37
app/src/gplay/kotlin/io/element/android/x/licenses/OssOpenSourcesLicensesProvider.kt

@ -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))
}
}

32
app/src/gplay/res/values-night/colors.xml

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

25
app/src/gplay/res/values-v27/themes.xml

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

32
app/src/gplay/res/values/colors.xml

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

23
app/src/gplay/res/values/styles.xml

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

41
app/src/gplay/res/values/themes.xml

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

22
build.gradle.kts

@ -1,11 +1,7 @@
import com.google.devtools.ksp.gradle.KspTask
import org.apache.tools.ant.taskdefs.optional.ReplaceRegExp
buildscript { buildscript {
dependencies { dependencies {
classpath(libs.kotlin.gradle.plugin) classpath(libs.kotlin.gradle.plugin)
classpath(libs.gms.google.services) classpath(libs.gms.google.services)
classpath(libs.oss.licenses.plugin)
} }
} }
@ -202,24 +198,6 @@ subprojects {
tasks.findByName("recordPaparazziRelease")?.dependsOn(removeOldScreenshotsTask) 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 { subprojects {
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions { kotlinOptions {

28
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)
}

10
features/preferences/api/src/main/kotlin/io/element/android/features/preferences/api/OpenSourceLicensesProvider.kt → features/licenses/api/src/main/kotlin/io/element/android/features/licenses/api/OpenSourceLicensesEntryPoint.kt

@ -14,11 +14,11 @@
* limitations under the License. * 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 { interface OpenSourceLicensesEntryPoint {
val hasOpenSourceLicenses: Boolean fun getNode(node: Node, buildContext: BuildContext): Node
fun navigateToOpenSourceLicenses(activity: Activity)
} }

49
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)
}

16
app/src/fdroid/kotlin/io/element/android/x/licenses/FdroidOpenSourceLicensesProvider.kt → features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/DefaultOpenSourcesLicensesEntryPoint.kt

@ -14,19 +14,19 @@
* limitations under the License. * 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 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 io.element.android.libraries.di.AppScope
import javax.inject.Inject import javax.inject.Inject
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class FdroidOpenSourceLicensesProvider @Inject constructor() : OpenSourceLicensesProvider { class DefaultOpenSourcesLicensesEntryPoint @Inject constructor() : OpenSourceLicensesEntryPoint {
override val hasOpenSourceLicenses: Boolean = false override fun getNode(node: Node, buildContext: BuildContext): Node {
return node.createNode<DependenciesFlowNode>(buildContext)
override fun navigateToOpenSourceLicenses(activity: Activity) {
error("Not supported, please ensure that hasOpenSourcesLicenses is true before calling this method")
} }
} }

79
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)
}
}

53
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() }
}
}
}
}

54
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
)
}
}

90
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 = {}
)
}

58
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,
)
}
}

50
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)
}
}

15
features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/about/FakeOpenSourceLicensesProvider.kt → features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListState.kt

@ -14,13 +14,12 @@
* limitations under the License. * 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.licenses.impl.model.DependencyLicenseItem
import io.element.android.features.preferences.api.OpenSourceLicensesProvider import io.element.android.libraries.architecture.AsyncData
import kotlinx.collections.immutable.ImmutableList
class FakeOpenSourceLicensesProvider( data class DependencyLicensesListState(
override val hasOpenSourceLicenses: Boolean, val licenses: AsyncData<ImmutableList<DependencyLicenseItem>>,
) : OpenSourceLicensesProvider { )
override fun navigateToOpenSourceLicenses(activity: Activity) = Unit
}

61
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,
)

118
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 = {},
)
}

53
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

71
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),
)
}

29
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()
}
}

1
features/preferences/impl/build.gradle.kts

@ -62,6 +62,7 @@ dependencies {
implementation(projects.features.lockscreen.api) implementation(projects.features.lockscreen.api)
implementation(projects.features.analytics.api) implementation(projects.features.analytics.api)
implementation(projects.features.ftue.api) implementation(projects.features.ftue.api)
implementation(projects.features.licenses.api)
implementation(projects.features.logout.api) implementation(projects.features.logout.api)
implementation(projects.features.roomlist.api) implementation(projects.features.roomlist.api)
implementation(projects.services.analytics.api) implementation(projects.services.analytics.api)

15
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.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode 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.lockscreen.api.LockScreenEntryPoint
import io.element.android.features.logout.api.LogoutEntryPoint import io.element.android.features.logout.api.LogoutEntryPoint
import io.element.android.features.preferences.api.PreferencesEntryPoint import io.element.android.features.preferences.api.PreferencesEntryPoint
@ -59,6 +60,7 @@ class PreferencesFlowNode @AssistedInject constructor(
private val lockScreenEntryPoint: LockScreenEntryPoint, private val lockScreenEntryPoint: LockScreenEntryPoint,
private val notificationTroubleShootEntryPoint: NotificationTroubleShootEntryPoint, private val notificationTroubleShootEntryPoint: NotificationTroubleShootEntryPoint,
private val logoutEntryPoint: LogoutEntryPoint, private val logoutEntryPoint: LogoutEntryPoint,
private val openSourceLicensesEntryPoint: OpenSourceLicensesEntryPoint,
) : BaseFlowNode<PreferencesFlowNode.NavTarget>( ) : BaseFlowNode<PreferencesFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
initialElement = plugins.filterIsInstance<PreferencesEntryPoint.Params>().first().initialElement.toNavTarget(), initialElement = plugins.filterIsInstance<PreferencesEntryPoint.Params>().first().initialElement.toNavTarget(),
@ -106,6 +108,9 @@ class PreferencesFlowNode @AssistedInject constructor(
@Parcelize @Parcelize
data object SignOut : NavTarget data object SignOut : NavTarget
@Parcelize
data object OssLicenses : NavTarget
} }
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@ -170,7 +175,12 @@ class PreferencesFlowNode @AssistedInject constructor(
createNode<ConfigureTracingNode>(buildContext) createNode<ConfigureTracingNode>(buildContext)
} }
NavTarget.About -> { NavTarget.About -> {
createNode<AboutNode>(buildContext) val callback = object : AboutNode.Callback {
override fun openOssLicenses() {
backstack.push(NavTarget.OssLicenses)
}
}
createNode<AboutNode>(buildContext, listOf(callback))
} }
NavTarget.AnalyticsSettings -> { NavTarget.AnalyticsSettings -> {
createNode<AnalyticsSettingsNode>(buildContext) createNode<AnalyticsSettingsNode>(buildContext)
@ -232,6 +242,9 @@ class PreferencesFlowNode @AssistedInject constructor(
.callback(callBack) .callback(callBack)
.build() .build()
} }
is NavTarget.OssLicenses -> {
openSourceLicensesEntryPoint.getNode(this, buildContext)
}
} }
} }

8
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 dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.compound.theme.ElementTheme 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.androidutils.browser.openUrlInChromeCustomTab
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
@ -36,8 +35,11 @@ class AboutNode @AssistedInject constructor(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val presenter: AboutPresenter, private val presenter: AboutPresenter,
private val openSourceLicensesProvider: OpenSourceLicensesProvider,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun openOssLicenses()
}
private fun onElementLegalClick( private fun onElementLegalClick(
activity: Activity, activity: Activity,
darkTheme: Boolean, darkTheme: Boolean,
@ -58,7 +60,7 @@ class AboutNode @AssistedInject constructor(
onElementLegalClick(activity, isDark, elementLegal) onElementLegalClick(activity, isDark, elementLegal)
}, },
onOpenSourceLicensesClick = { onOpenSourceLicensesClick = {
openSourceLicensesProvider.navigateToOpenSourceLicenses(activity) plugins.filterIsInstance<Callback>().forEach { it.openOssLicenses() }
}, },
modifier = modifier modifier = modifier
) )

6
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 package io.element.android.features.preferences.impl.about
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import io.element.android.features.preferences.api.OpenSourceLicensesProvider
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import javax.inject.Inject import javax.inject.Inject
class AboutPresenter @Inject constructor( class AboutPresenter @Inject constructor() : Presenter<AboutState> {
private val openSourceLicensesProvider: OpenSourceLicensesProvider,
) : Presenter<AboutState> {
@Composable @Composable
override fun present(): AboutState { override fun present(): AboutState {
return AboutState( return AboutState(
elementLegals = getAllLegals(), elementLegals = getAllLegals(),
hasOpenSourcesLicenses = openSourceLicensesProvider.hasOpenSourceLicenses,
) )
} }
} }

1
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( data class AboutState(
val elementLegals: List<ElementLegal>, val elementLegals: List<ElementLegal>,
val hasOpenSourcesLicenses: Boolean,
) )

6
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> override val values: Sequence<AboutState>
get() = sequenceOf( get() = sequenceOf(
anAboutState(), anAboutState(),
anAboutState(hasOpenSourcesLicenses = true),
) )
} }
fun anAboutState( fun anAboutState(
hasOpenSourcesLicenses: Boolean = false, elementLegals: List<ElementLegal> = getAllLegals(),
) = AboutState( ) = AboutState(
elementLegals = getAllLegals(), elementLegals = elementLegals,
hasOpenSourcesLicenses = hasOpenSourcesLicenses,
) )

2
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutView.kt

@ -45,14 +45,12 @@ fun AboutView(
onClick = { onElementLegalClick(elementLegal) } onClick = { onElementLegalClick(elementLegal) }
) )
} }
if (state.hasOpenSourcesLicenses) {
PreferenceText( PreferenceText(
title = stringResource(id = CommonStrings.common_open_source_licenses), title = stringResource(id = CommonStrings.common_open_source_licenses),
onClick = onOpenSourceLicensesClick, onClick = onOpenSourceLicensesClick,
) )
} }
} }
}
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable

15
features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/about/AboutPresenterTest.kt

@ -31,25 +31,12 @@ class AboutPresenterTest {
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val presenter = AboutPresenter(FakeOpenSourceLicensesProvider(hasOpenSourceLicenses = true)) val presenter = AboutPresenter()
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.elementLegals).isEqualTo(getAllLegals()) 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()
} }
} }
} }

16
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.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalled
@ -61,21 +60,10 @@ class AboutViewTest {
} }
@Test @Test
fun `if open source licenses are not available, the entry is not displayed`() { fun `clicking on the open source licenses invokes the expected callback`() {
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`() {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setAboutView( rule.setAboutView(
anAboutState( anAboutState(),
hasOpenSourcesLicenses = true,
),
onOpenSourceLicensesClick = callback, onOpenSourceLicensesClick = callback,
) )
rule.clickOn(CommonStrings.common_open_source_licenses) rule.clickOn(CommonStrings.common_open_source_licenses)

3
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" google_firebase_bom = "com.google.firebase:firebase-bom:33.2.0"
firebase_appdistribution_gradle = { module = "com.google.firebase:firebase-appdistribution-gradle", version.ref = "firebaseAppDistribution" } 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" } 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
androidx_core = { module = "androidx.core:core", version.ref = "core" } 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" opusencoder = "io.element.android:opusencoder:1.1.0"
kotlinpoet = "com.squareup:kotlinpoet:1.18.1" kotlinpoet = "com.squareup:kotlinpoet:1.18.1"
zxing_cpp = "io.github.zxing-cpp:android:2.2.0" 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 # Analytics
posthog = "com.posthog:posthog-android:3.6.0" 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" } firebaseAppDistribution = { id = "com.google.firebase.appdistribution", version.ref = "firebaseAppDistribution" }
knit = { id = "org.jetbrains.kotlinx.knit", version = "0.5.0" } knit = { id = "org.jetbrains.kotlinx.knit", version = "0.5.0" }
sonarqube = "org.sonarqube:5.1.0.4882" sonarqube = "org.sonarqube:5.1.0.4882"
licensee = "app.cash.licensee:1.11.0"

55
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,
)
}
}

30
plugins/src/main/kotlin/extension/Utils.kt

@ -17,13 +17,35 @@
package extension package extension
import org.gradle.api.Project 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.ByteArrayOutputStream
import java.io.IOException 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 outputStream = ByteArrayOutputStream()
val errorStream = ByteArrayOutputStream() val errorStream = ByteArrayOutputStream()
project.exec { exec {
commandLine = cmd.split(" ") commandLine = cmd.split(" ")
standardOutput = outputStream standardOutput = outputStream
errorOutput = errorStream errorOutput = errorStream
@ -34,7 +56,3 @@ private fun Project.runCommand(cmd: String): String {
} }
return String(outputStream.toByteArray()).trim() 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")

1
tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt

@ -108,6 +108,7 @@ class KonsistClassNameTest {
"Accompanist", "Accompanist",
"AES", "AES",
"Android", "Android",
"Asset",
"Database", "Database",
"DBov", "DBov",
"Default", "Default",

Loading…
Cancel
Save