Merge pull request #3381 from element-hq/feature/bma/ossLicense

Replace OSS licenses plugin with Licensee and some manually done UI.
This commit is contained in:
Benoit Marty 2024-09-03 10:40:58 +02:00 committed by GitHub
commit 99f48723d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 1011 additions and 319 deletions

View file

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

View file

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

View file

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

View file

@ -0,0 +1,32 @@
/*
* 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 com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.squareup.anvil.annotations.ContributesBinding
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 DefaultOpenSourcesLicensesEntryPoint @Inject constructor() : OpenSourceLicensesEntryPoint {
override fun getNode(node: Node, buildContext: BuildContext): Node {
return node.createNode<DependenciesFlowNode>(buildContext)
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -18,5 +18,4 @@ package io.element.android.features.preferences.impl.about
data class AboutState(
val elementLegals: List<ElementLegal>,
val hasOpenSourcesLicenses: Boolean,
)

View file

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

View file

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

View file

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

View file

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