From d49a0dde6ec79e1782d4a667b78101d64082ee0d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 8 Dec 2021 18:55:43 +0100 Subject: [PATCH] Legals: Add the screen (WIP) --- .../im/vector/app/core/di/FragmentModule.kt | 6 + .../app/core/di/MavericksViewModelModule.kt | 6 + .../app/features/discovery/Extensions.kt | 37 ++++-- .../discovery/IdentityServerWithTerms.kt | 1 + .../features/settings/legals/LegalsAction.kt | 23 ++++ .../settings/legals/LegalsController.kt | 118 ++++++++++++++++++ .../settings/legals/LegalsFragment.kt | 84 +++++++++++++ .../features/settings/legals/LegalsState.kt | 28 +++++ .../settings/legals/LegalsViewEvents.kt | 23 ++++ .../settings/legals/LegalsViewModel.kt | 91 ++++++++++++++ vector/src/main/res/values/strings.xml | 7 +- .../src/main/res/xml/vector_settings_root.xml | 5 + 12 files changed, 415 insertions(+), 14 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/settings/legals/LegalsAction.kt create mode 100644 vector/src/main/java/im/vector/app/features/settings/legals/LegalsController.kt create mode 100644 vector/src/main/java/im/vector/app/features/settings/legals/LegalsFragment.kt create mode 100644 vector/src/main/java/im/vector/app/features/settings/legals/LegalsState.kt create mode 100644 vector/src/main/java/im/vector/app/features/settings/legals/LegalsViewEvents.kt create mode 100644 vector/src/main/java/im/vector/app/features/settings/legals/LegalsViewModel.kt diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index b8d00fac5a..43bb505a5e 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -133,6 +133,7 @@ import im.vector.app.features.settings.devtools.KeyRequestsFragment import im.vector.app.features.settings.devtools.OutgoingKeyRequestListFragment import im.vector.app.features.settings.homeserver.HomeserverSettingsFragment import im.vector.app.features.settings.ignored.VectorSettingsIgnoredUsersFragment +import im.vector.app.features.settings.legals.LegalsFragment import im.vector.app.features.settings.locale.LocalePickerFragment import im.vector.app.features.settings.notifications.VectorSettingsAdvancedNotificationPreferenceFragment import im.vector.app.features.settings.notifications.VectorSettingsNotificationPreferenceFragment @@ -699,6 +700,11 @@ interface FragmentModule { @FragmentKey(DiscoverySettingsFragment::class) fun bindDiscoverySettingsFragment(fragment: DiscoverySettingsFragment): Fragment + @Binds + @IntoMap + @FragmentKey(LegalsFragment::class) + fun bindLegalsFragment(fragment: LegalsFragment): Fragment + @Binds @IntoMap @FragmentKey(ReviewTermsFragment::class) diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt index cac694e84e..37721ca9f9 100644 --- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt @@ -85,6 +85,7 @@ import im.vector.app.features.settings.devtools.KeyRequestListViewModel import im.vector.app.features.settings.devtools.KeyRequestViewModel import im.vector.app.features.settings.homeserver.HomeserverSettingsViewModel import im.vector.app.features.settings.ignored.IgnoredUsersViewModel +import im.vector.app.features.settings.legals.LegalsViewModel import im.vector.app.features.settings.locale.LocalePickerViewModel import im.vector.app.features.settings.push.PushGatewaysViewModel import im.vector.app.features.settings.threepids.ThreePidsSettingsViewModel @@ -504,6 +505,11 @@ interface MavericksViewModelModule { @MavericksViewModelKey(DiscoverySettingsViewModel::class) fun discoverySettingsViewModelFactory(factory: DiscoverySettingsViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + @Binds + @IntoMap + @MavericksViewModelKey(LegalsViewModel::class) + fun legalsViewModelFactory(factory: LegalsViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + @Binds @IntoMap @MavericksViewModelKey(RoomDetailViewModel::class) diff --git a/vector/src/main/java/im/vector/app/features/discovery/Extensions.kt b/vector/src/main/java/im/vector/app/features/discovery/Extensions.kt index bf6bd89938..119e544414 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/Extensions.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/Extensions.kt @@ -23,18 +23,29 @@ import org.matrix.android.sdk.api.session.terms.TermsService suspend fun Session.fetchIdentityServerWithTerms(userLanguage: String): IdentityServerWithTerms? { val identityServerUrl = identityService().getCurrentIdentityServerUrl() return identityServerUrl?.let { - val terms = getTerms(TermsService.ServiceType.IdentityService, identityServerUrl.ensureProtocol()) - .serverResponse - .getLocalizedTerms(userLanguage) - val policyUrls = terms.mapNotNull { - val name = it.localizedName ?: it.policyName - val url = it.localizedUrl - if (name == null || url == null) { - null - } else { - IdentityServerPolicy(name = name, url = url) - } - } - IdentityServerWithTerms(identityServerUrl, policyUrls) + fetchTerms(it, TermsService.ServiceType.IdentityService, userLanguage) } } + +suspend fun Session.fetchHomeserverWithTerms(userLanguage: String): IdentityServerWithTerms { + val homeserverUrl = sessionParams.homeServerUrl + return fetchTerms(homeserverUrl, TermsService.ServiceType.IdentityService, userLanguage) +} + +private suspend fun Session.fetchTerms(serviceUrl: String, + serviceType: TermsService.ServiceType, + userLanguage: String): IdentityServerWithTerms { + val terms = getTerms(serviceType, serviceUrl.ensureProtocol()) + .serverResponse + .getLocalizedTerms(userLanguage) + val policyUrls = terms.mapNotNull { + val name = it.localizedName ?: it.policyName + val url = it.localizedUrl + if (name == null || url == null) { + null + } else { + IdentityServerPolicy(name = name, url = url) + } + } + return IdentityServerWithTerms(serviceUrl, policyUrls) +} diff --git a/vector/src/main/java/im/vector/app/features/discovery/IdentityServerWithTerms.kt b/vector/src/main/java/im/vector/app/features/discovery/IdentityServerWithTerms.kt index 36bae0d0c5..0454de70d7 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/IdentityServerWithTerms.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/IdentityServerWithTerms.kt @@ -16,6 +16,7 @@ package im.vector.app.features.discovery +// TODO Rename for more generic name data class IdentityServerWithTerms( val serverUrl: String, val policies: List diff --git a/vector/src/main/java/im/vector/app/features/settings/legals/LegalsAction.kt b/vector/src/main/java/im/vector/app/features/settings/legals/LegalsAction.kt new file mode 100644 index 0000000000..424c6bb78b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/legals/LegalsAction.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2021 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. + */ + +package im.vector.app.features.settings.legals + +import im.vector.app.core.platform.VectorViewModelAction + +sealed interface LegalsAction : VectorViewModelAction { + object Refresh : LegalsAction +} diff --git a/vector/src/main/java/im/vector/app/features/settings/legals/LegalsController.kt b/vector/src/main/java/im/vector/app/features/settings/legals/LegalsController.kt new file mode 100644 index 0000000000..29ad27b4a9 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/legals/LegalsController.kt @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2021 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. + */ +package im.vector.app.features.settings.legals + +import com.airbnb.epoxy.TypedEpoxyController +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import im.vector.app.R +import im.vector.app.core.epoxy.errorWithRetryItem +import im.vector.app.core.epoxy.loadingItem +import im.vector.app.core.error.ErrorFormatter +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.discovery.IdentityServerPolicy +import im.vector.app.features.discovery.IdentityServerWithTerms +import im.vector.app.features.discovery.discoveryPolicyItem +import im.vector.app.features.discovery.settingsInfoItem +import im.vector.app.features.discovery.settingsSectionTitleItem +import javax.inject.Inject + +class LegalsController @Inject constructor( + private val stringProvider: StringProvider, + private val errorFormatter: ErrorFormatter +) : TypedEpoxyController() { + + var listener: Listener? = null + + override fun buildModels(data: LegalsState) { + buildAppSection() + buildHomeserverSection(data) + buildIdentityServerSection(data) + } + + private fun buildAppSection() { + settingsSectionTitleItem { + id("appTitle") + titleResId(R.string.legals_application_title) + } + + // TODO + } + + private fun buildHomeserverSection(data: LegalsState) { + settingsSectionTitleItem { + id("hsServerTitle") + titleResId(R.string.legals_home_server_title) + } + + buildPolicy("hs", data.homeServer) + } + + private fun buildIdentityServerSection(data: LegalsState) { + if (data.hasIdentityServer) { + settingsSectionTitleItem { + id("idServerTitle") + titleResId(R.string.legals_identity_server_title) + } + + buildPolicy("is", data.identityServer) + } + } + + private fun buildPolicy(tag: String, serverWithTerms: Async) { + val host = this + + when (serverWithTerms) { + Uninitialized, + is Loading -> loadingItem { + id("loading_$tag") + } + is Success -> { + val policies = serverWithTerms()?.policies + if (policies.isNullOrEmpty()) { + settingsInfoItem { + id("emptyPolicy") + helperText(host.stringProvider.getString(R.string.legals_no_policy_provided)) + } + } else { + policies.forEach { policy -> + discoveryPolicyItem { + id(policy.url) + name(policy.name) + url(policy.url) + clickListener { host.listener?.openPolicy(policy) } + } + } + } + } + is Fail -> { + errorWithRetryItem { + id("errorRetry_$tag") + text(host.errorFormatter.toHumanReadable(serverWithTerms.error)) + listener { host.listener?.onTapRetry() } + } + } + } + } + + interface Listener { + fun onTapRetry() + fun openPolicy(policy: IdentityServerPolicy) + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/legals/LegalsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/legals/LegalsFragment.kt new file mode 100644 index 0000000000..9aa7633e84 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/legals/LegalsFragment.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2021 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. + */ +package im.vector.app.features.settings.legals + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.app.R +import im.vector.app.core.extensions.cleanup +import im.vector.app.core.extensions.configureWith +import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.utils.openUrlInChromeCustomTab +import im.vector.app.databinding.FragmentGenericRecyclerBinding +import im.vector.app.features.discovery.IdentityServerPolicy +import javax.inject.Inject + +class LegalsFragment @Inject constructor( + private val controller: LegalsController +) : VectorBaseFragment(), + LegalsController.Listener { + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentGenericRecyclerBinding { + return FragmentGenericRecyclerBinding.inflate(inflater, container, false) + } + + private val viewModel by fragmentViewModel(LegalsViewModel::class) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + controller.listener = this + views.genericRecyclerView.configureWith(controller) + + viewModel.observeViewEvents { + when (it) { + is LegalsViewEvents.Failure -> { + displayErrorDialog(it.throwable) + } + }.exhaustive + } + } + + override fun onDestroyView() { + views.genericRecyclerView.cleanup() + controller.listener = null + super.onDestroyView() + } + + override fun invalidate() = withState(viewModel) { state -> + controller.setData(state) + } + + override fun onResume() { + super.onResume() + (activity as? AppCompatActivity)?.supportActionBar?.setTitle(R.string.preference_root_legals) + viewModel.handle(LegalsAction.Refresh) + } + + override fun onTapRetry() { + viewModel.handle(LegalsAction.Refresh) + } + + override fun openPolicy(policy: IdentityServerPolicy) { + openUrlInChromeCustomTab(requireContext(), null, policy.url) + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/legals/LegalsState.kt b/vector/src/main/java/im/vector/app/features/settings/legals/LegalsState.kt new file mode 100644 index 0000000000..fea9a06c11 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/legals/LegalsState.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2021 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. + */ + +package im.vector.app.features.settings.legals + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.Uninitialized +import im.vector.app.features.discovery.IdentityServerWithTerms + +data class LegalsState( + val homeServer: Async = Uninitialized, + val hasIdentityServer: Boolean = false, + val identityServer: Async = Uninitialized +) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/settings/legals/LegalsViewEvents.kt b/vector/src/main/java/im/vector/app/features/settings/legals/LegalsViewEvents.kt new file mode 100644 index 0000000000..40741a4d62 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/legals/LegalsViewEvents.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2021 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. + */ + +package im.vector.app.features.settings.legals + +import im.vector.app.core.platform.VectorViewEvents + +sealed interface LegalsViewEvents : VectorViewEvents { + data class Failure(val throwable: Throwable) : LegalsViewEvents +} diff --git a/vector/src/main/java/im/vector/app/features/settings/legals/LegalsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/legals/LegalsViewModel.kt new file mode 100644 index 0000000000..7947bcc570 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/legals/LegalsViewModel.kt @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2021 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. + */ +package im.vector.app.features.settings.legals + +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.MavericksViewModelFactory +import com.airbnb.mvrx.Success +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.R +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.discovery.fetchHomeserverWithTerms +import im.vector.app.features.discovery.fetchIdentityServerWithTerms +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.session.Session + +class LegalsViewModel @AssistedInject constructor( + @Assisted initialState: LegalsState, + private val session: Session, + private val stringProvider: StringProvider +) : VectorViewModel(initialState) { + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: LegalsState): LegalsViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + + override fun handle(action: LegalsAction) { + when (action) { + LegalsAction.Refresh -> loadData() + }.exhaustive + } + + private fun loadData() = withState { state -> + loadHomeserver(state) + val url = session.identityService().getCurrentIdentityServerUrl() + if (url.isNullOrEmpty()) { + setState { copy(hasIdentityServer = false) } + } else { + setState { copy(hasIdentityServer = true) } + loadIdentityServer(state) + } + } + + private fun loadHomeserver(state: LegalsState) { + if (state.homeServer !is Success) { + setState { copy(homeServer = Loading()) } + viewModelScope.launch { + runCatching { session.fetchHomeserverWithTerms(stringProvider.getString(R.string.resources_language)) } + .fold( + onSuccess = { setState { copy(homeServer = Success(it)) } }, + onFailure = { setState { copy(homeServer = Fail(it)) } } + ) + } + } + } + + private fun loadIdentityServer(state: LegalsState) { + if (state.identityServer !is Success) { + setState { copy(identityServer = Loading()) } + viewModelScope.launch { + runCatching { session.fetchIdentityServerWithTerms(stringProvider.getString(R.string.resources_language)) } + .fold( + onSuccess = { setState { copy(identityServer = Success(it)) } }, + onFailure = { setState { copy(identityServer = Fail(it)) } } + ) + } + } + } +} diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index ddb731a5e0..e23cc02235 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1397,6 +1397,11 @@ Allow integrations Integration manager + ${app_name} policy + Your homeserver policy + Your identity server policy + This server does not provide any policy. + Integrations are disabled "Enable 'Allow integrations' in Settings to do this." @@ -2270,6 +2275,7 @@ Voice & Video Help & About + Legals Register token @@ -3566,7 +3572,6 @@ Manage rooms and spaces - Show all rooms in Home All rooms you’re in will be shown in Home. diff --git a/vector/src/main/res/xml/vector_settings_root.xml b/vector/src/main/res/xml/vector_settings_root.xml index 32e21b3391..712c1e7a4c 100644 --- a/vector/src/main/res/xml/vector_settings_root.xml +++ b/vector/src/main/res/xml/vector_settings_root.xml @@ -53,4 +53,9 @@ android:title="@string/preference_root_help_about" app:fragment="im.vector.app.features.settings.VectorSettingsHelpAboutFragment" /> + + \ No newline at end of file