diff --git a/changelog.d/4638.feature b/changelog.d/4638.feature new file mode 100644 index 0000000000..0f8bd36465 --- /dev/null +++ b/changelog.d/4638.feature @@ -0,0 +1 @@ +Add a help section in the settings. \ No newline at end of file diff --git a/changelog.d/4660.feature b/changelog.d/4660.feature new file mode 100644 index 0000000000..4eca82eaf5 --- /dev/null +++ b/changelog.d/4660.feature @@ -0,0 +1 @@ +Create a legal screen in the setting to group all the different policies. \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/TermsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/TermsService.kt index 10ce0829d0..e64cf1872e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/TermsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/TermsService.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.api.session.terms +import org.matrix.android.sdk.internal.session.terms.TermsResponse + interface TermsService { enum class ServiceType { IntegrationManager, @@ -28,4 +30,10 @@ interface TermsService { baseUrl: String, agreedUrls: List, token: String?) + + /** + * Get the homeserver terms, from the register API. + * Will be updated once https://github.com/matrix-org/matrix-doc/pull/3012 will be implemented. + */ + suspend fun getHomeserverTerms(baseUrl: String): TermsResponse } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt index d40fd8d076..c52c6a404e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt @@ -18,10 +18,13 @@ package org.matrix.android.sdk.internal.session.terms import dagger.Lazy import okhttp3.OkHttpClient +import org.matrix.android.sdk.api.auth.data.LoginFlowTypes +import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.terms.GetTermsResponse import org.matrix.android.sdk.api.session.terms.TermsService +import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate import org.matrix.android.sdk.internal.network.NetworkConstants import org.matrix.android.sdk.internal.network.RetrofitFactory @@ -55,6 +58,27 @@ internal class DefaultTermsService @Inject constructor( return GetTermsResponse(termsResponse, getAlreadyAcceptedTermUrlsFromAccountData()) } + /** + * We use a trick here to get the homeserver T&C, we use the register API + */ + override suspend fun getHomeserverTerms(baseUrl: String): TermsResponse { + return try { + executeRequest(null) { + termsAPI.register(baseUrl + NetworkConstants.URI_API_PREFIX_PATH_R0 + "register") + } + // Return empty result if it succeed, but it should never happen + TermsResponse() + } catch (throwable: Throwable) { + @Suppress("UNCHECKED_CAST") + TermsResponse( + policies = (throwable.toRegistrationFlowResponse() + ?.params + ?.get(LoginFlowTypes.TERMS) as? JsonDict) + ?.get("policies") as? JsonDict + ) + } + } + override suspend fun agreeToTerms(serviceType: TermsService.ServiceType, baseUrl: String, agreedUrls: List, @@ -91,7 +115,7 @@ internal class DefaultTermsService @Inject constructor( private fun buildUrl(baseUrl: String, serviceType: TermsService.ServiceType): String { val servicePath = when (serviceType) { TermsService.ServiceType.IntegrationManager -> NetworkConstants.URI_INTEGRATION_MANAGER_PATH - TermsService.ServiceType.IdentityService -> NetworkConstants.URI_IDENTITY_PATH_V2 + TermsService.ServiceType.IdentityService -> NetworkConstants.URI_IDENTITY_PATH_V2 } return "${baseUrl.ensureTrailingSlash()}$servicePath" } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsAPI.kt index 91d27030de..fb6aff5a9e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsAPI.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.internal.session.terms +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.api.util.emptyJsonDict import org.matrix.android.sdk.internal.network.HttpHeaders import retrofit2.http.Body import retrofit2.http.GET @@ -37,4 +39,12 @@ internal interface TermsAPI { suspend fun agreeToTerms(@Url url: String, @Body params: AcceptTermsBody, @Header(HttpHeaders.Authorization) token: String) + + /** + * API to retrieve the terms for a homeserver. The API /terms does not exist yet, so retrieve the terms from the login flow. + * We do not care about the result (Credentials) + */ + @POST + suspend fun register(@Url url: String, + @Body body: JsonDict = emptyJsonDict) } 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/core/utils/Dialogs.kt b/vector/src/main/java/im/vector/app/core/utils/Dialogs.kt index ad01546782..11b9a693da 100644 --- a/vector/src/main/java/im/vector/app/core/utils/Dialogs.kt +++ b/vector/src/main/java/im/vector/app/core/utils/Dialogs.kt @@ -23,7 +23,7 @@ import android.webkit.WebViewClient import android.widget.TextView import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R -import im.vector.app.features.discovery.IdentityServerWithTerms +import im.vector.app.features.discovery.ServerAndPolicies import me.gujun.android.span.link import me.gujun.android.span.span @@ -45,7 +45,7 @@ fun Context.displayInWebView(url: String) { .show() } -fun Context.showIdentityServerConsentDialog(identityServerWithTerms: IdentityServerWithTerms?, +fun Context.showIdentityServerConsentDialog(identityServerWithTerms: ServerAndPolicies?, consentCallBack: (() -> Unit)) { // Build the message val content = span { diff --git a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewEvents.kt b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewEvents.kt index c7fd13a62c..cb8d137c05 100644 --- a/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/contactsbook/ContactsBookViewEvents.kt @@ -17,9 +17,9 @@ package im.vector.app.features.contactsbook import im.vector.app.core.platform.VectorViewEvents -import im.vector.app.features.discovery.IdentityServerWithTerms +import im.vector.app.features.discovery.ServerAndPolicies sealed class ContactsBookViewEvents : VectorViewEvents { data class Failure(val throwable: Throwable) : ContactsBookViewEvents() - data class OnPoliciesRetrieved(val identityServerWithTerms: IdentityServerWithTerms?) : ContactsBookViewEvents() + data class OnPoliciesRetrieved(val identityServerWithTerms: ServerAndPolicies?) : ContactsBookViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoveryPolicyItem.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoveryPolicyItem.kt index c97a2286ae..4df4146d2f 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoveryPolicyItem.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoveryPolicyItem.kt @@ -24,6 +24,7 @@ import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.onClick +import im.vector.app.core.extensions.setTextOrHide @EpoxyModelClass(layout = R.layout.item_discovery_policy) abstract class DiscoveryPolicyItem : EpoxyModelWithHolder() { @@ -40,7 +41,7 @@ abstract class DiscoveryPolicyItem : EpoxyModelWithHolder = Uninitialized, + val identityServer: Async = Uninitialized, val emailList: Async> = Uninitialized, val phoneNumbersList: Async> = Uninitialized, // Can be true if terms are updated diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt index 4eb3fada28..19f233fe98 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsViewModel.kt @@ -78,7 +78,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor( init { setState { copy( - identityServer = Success(identityService.getCurrentIdentityServerUrl()?.let { IdentityServerWithTerms(it, emptyList()) }), + identityServer = Success(identityService.getCurrentIdentityServerUrl()?.let { ServerAndPolicies(it, emptyList()) }), userConsent = identityService.getUserConsent() ) } @@ -151,7 +151,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor( val data = session.identityService().setNewIdentityServer(action.url) setState { copy( - identityServer = Success(IdentityServerWithTerms(data, emptyList())), + identityServer = Success(ServerAndPolicies(data, emptyList())), userConsent = false ) } @@ -401,7 +401,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor( } } - private suspend fun fetchIdentityServerWithTerms(): IdentityServerWithTerms? { + private suspend fun fetchIdentityServerWithTerms(): ServerAndPolicies? { return session.fetchIdentityServerWithTerms(stringProvider.getString(R.string.resources_language)) } } 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..24d675695b 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 @@ -19,22 +19,35 @@ package im.vector.app.features.discovery import im.vector.app.core.utils.ensureProtocol import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.terms.TermsService +import org.matrix.android.sdk.internal.session.terms.TermsResponse -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) +suspend fun Session.fetchIdentityServerWithTerms(userLanguage: String): ServerAndPolicies? { + return identityService().getCurrentIdentityServerUrl() + ?.let { identityServerUrl -> + val termsResponse = getTerms(TermsService.ServiceType.IdentityService, identityServerUrl.ensureProtocol()) + .serverResponse + buildServerAndPolicies(identityServerUrl, termsResponse, userLanguage) } - } - IdentityServerWithTerms(identityServerUrl, policyUrls) - } +} + +suspend fun Session.fetchHomeserverWithTerms(userLanguage: String): ServerAndPolicies { + val homeserverUrl = sessionParams.homeServerUrl + val terms = getHomeserverTerms(homeserverUrl.ensureProtocol()) + return buildServerAndPolicies(homeserverUrl, terms, userLanguage) +} + +private fun buildServerAndPolicies(serviceUrl: String, + termsResponse: TermsResponse, + userLanguage: String): ServerAndPolicies { + val terms = termsResponse.getLocalizedTerms(userLanguage) + val policyUrls = terms.mapNotNull { + val name = it.localizedName ?: it.policyName + val url = it.localizedUrl + if (name == null || url == null) { + null + } else { + ServerPolicy(name = name, url = url) + } + } + return ServerAndPolicies(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/ServerAndPolicies.kt similarity index 86% rename from vector/src/main/java/im/vector/app/features/discovery/IdentityServerWithTerms.kt rename to vector/src/main/java/im/vector/app/features/discovery/ServerAndPolicies.kt index 36bae0d0c5..17f1b0cff9 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/IdentityServerWithTerms.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/ServerAndPolicies.kt @@ -16,12 +16,12 @@ package im.vector.app.features.discovery -data class IdentityServerWithTerms( +data class ServerAndPolicies( val serverUrl: String, - val policies: List + val policies: List ) -data class IdentityServerPolicy( +data class ServerPolicy( val name: String, val url: String ) diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 47c9af3168..c9464550ac 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -35,6 +35,7 @@ import javax.inject.Inject class VectorPreferences @Inject constructor(private val context: Context) { companion object { + const val SETTINGS_HELP_PREFERENCE_KEY = "SETTINGS_HELP_PREFERENCE_KEY" const val SETTINGS_CHANGE_PASSWORD_PREFERENCE_KEY = "SETTINGS_CHANGE_PASSWORD_PREFERENCE_KEY" const val SETTINGS_VERSION_PREFERENCE_KEY = "SETTINGS_VERSION_PREFERENCE_KEY" const val SETTINGS_SDK_VERSION_PREFERENCE_KEY = "SETTINGS_SDK_VERSION_PREFERENCE_KEY" @@ -42,13 +43,8 @@ class VectorPreferences @Inject constructor(private val context: Context) { const val SETTINGS_LOGGED_IN_PREFERENCE_KEY = "SETTINGS_LOGGED_IN_PREFERENCE_KEY" const val SETTINGS_HOME_SERVER_PREFERENCE_KEY = "SETTINGS_HOME_SERVER_PREFERENCE_KEY" const val SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY = "SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY" - const val SETTINGS_APP_TERM_CONDITIONS_PREFERENCE_KEY = "SETTINGS_APP_TERM_CONDITIONS_PREFERENCE_KEY" - const val SETTINGS_PRIVACY_POLICY_PREFERENCE_KEY = "SETTINGS_PRIVACY_POLICY_PREFERENCE_KEY" const val SETTINGS_DISCOVERY_PREFERENCE_KEY = "SETTINGS_DISCOVERY_PREFERENCE_KEY" - const val SETTINGS_THIRD_PARTY_NOTICES_PREFERENCE_KEY = "SETTINGS_THIRD_PARTY_NOTICES_PREFERENCE_KEY" - const val SETTINGS_OTHER_THIRD_PARTY_NOTICES_PREFERENCE_KEY = "SETTINGS_OTHER_THIRD_PARTY_NOTICES_PREFERENCE_KEY" - const val SETTINGS_COPYRIGHT_PREFERENCE_KEY = "SETTINGS_COPYRIGHT_PREFERENCE_KEY" const val SETTINGS_CLEAR_CACHE_PREFERENCE_KEY = "SETTINGS_CLEAR_CACHE_PREFERENCE_KEY" const val SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY = "SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY" const val SETTINGS_USER_SETTINGS_PREFERENCE_KEY = "SETTINGS_USER_SETTINGS_PREFERENCE_KEY" diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt index 03b7c16274..31d9cf0426 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsHelpAboutFragment.kt @@ -22,11 +22,9 @@ import im.vector.app.R import im.vector.app.core.preference.VectorPreference import im.vector.app.core.utils.FirstThrottler import im.vector.app.core.utils.copyToClipboard -import im.vector.app.core.utils.displayInWebView import im.vector.app.core.utils.openAppSettingsPage import im.vector.app.core.utils.openUrlInChromeCustomTab import im.vector.app.features.version.VersionProvider -import im.vector.app.openOssLicensesMenuActivity import org.matrix.android.sdk.api.Matrix import javax.inject.Inject @@ -40,6 +38,15 @@ class VectorSettingsHelpAboutFragment @Inject constructor( private val firstThrottler = FirstThrottler(1000) override fun bindPref() { + // Help + findPreference(VectorPreferences.SETTINGS_HELP_PREFERENCE_KEY)!! + .onPreferenceClickListener = Preference.OnPreferenceClickListener { + if (firstThrottler.canHandle() is FirstThrottler.CanHandlerResult.Yes) { + openUrlInChromeCustomTab(requireContext(), null, VectorSettingsUrls.HELP) + } + false + } + // preference to start the App info screen, to facilitate App permissions access findPreference(APP_INFO_LINK_PREFERENCE_KEY)!! .onPreferenceClickListener = Preference.OnPreferenceClickListener { @@ -76,44 +83,6 @@ class VectorSettingsHelpAboutFragment @Inject constructor( // olm version findPreference(VectorPreferences.SETTINGS_OLM_VERSION_PREFERENCE_KEY)!! .summary = session.cryptoService().getCryptoVersion(requireContext(), false) - - // copyright - findPreference(VectorPreferences.SETTINGS_COPYRIGHT_PREFERENCE_KEY)!! - .onPreferenceClickListener = Preference.OnPreferenceClickListener { - openUrlInChromeCustomTab(requireContext(), null, VectorSettingsUrls.COPYRIGHT) - false - } - - // terms & conditions - findPreference(VectorPreferences.SETTINGS_APP_TERM_CONDITIONS_PREFERENCE_KEY)!! - .onPreferenceClickListener = Preference.OnPreferenceClickListener { - openUrlInChromeCustomTab(requireContext(), null, VectorSettingsUrls.TAC) - false - } - - // privacy policy - findPreference(VectorPreferences.SETTINGS_PRIVACY_POLICY_PREFERENCE_KEY)!! - .onPreferenceClickListener = Preference.OnPreferenceClickListener { - openUrlInChromeCustomTab(requireContext(), null, VectorSettingsUrls.PRIVACY_POLICY) - false - } - - // third party notice - findPreference(VectorPreferences.SETTINGS_THIRD_PARTY_NOTICES_PREFERENCE_KEY)!! - .onPreferenceClickListener = Preference.OnPreferenceClickListener { - if (firstThrottler.canHandle() is FirstThrottler.CanHandlerResult.Yes) { - activity?.displayInWebView(VectorSettingsUrls.THIRD_PARTY_LICENSES) - } - false - } - - // Note: preference is not visible on F-Droid build - findPreference(VectorPreferences.SETTINGS_OTHER_THIRD_PARTY_NOTICES_PREFERENCE_KEY)!! - .onPreferenceClickListener = Preference.OnPreferenceClickListener { - // See https://developers.google.com/android/guides/opensource - openOssLicensesMenuActivity(requireActivity()) - false - } } companion object { diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsUrls.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsUrls.kt index c5088aac6d..09249c4957 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsUrls.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsUrls.kt @@ -17,7 +17,7 @@ package im.vector.app.features.settings object VectorSettingsUrls { - + const val HELP = "https://element.io/help" const val COPYRIGHT = "https://element.io/copyright" const val TAC = "https://element.io/terms-of-service" const val PRIVACY_POLICY = "https://element.io/privacy" diff --git a/vector/src/main/java/im/vector/app/features/settings/legals/ElementLegals.kt b/vector/src/main/java/im/vector/app/features/settings/legals/ElementLegals.kt new file mode 100644 index 0000000000..de59f36604 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/legals/ElementLegals.kt @@ -0,0 +1,38 @@ +/* + * 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.R +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.discovery.ServerPolicy +import im.vector.app.features.settings.VectorSettingsUrls +import javax.inject.Inject + +class ElementLegals @Inject constructor( + private val stringProvider: StringProvider +) { + /** + * Use ServerPolicy model + */ + fun getData(): List { + return listOf( + ServerPolicy(stringProvider.getString(R.string.settings_copyright), VectorSettingsUrls.COPYRIGHT), + ServerPolicy(stringProvider.getString(R.string.settings_app_term_conditions), VectorSettingsUrls.TAC), + ServerPolicy(stringProvider.getString(R.string.settings_privacy_policy), VectorSettingsUrls.PRIVACY_POLICY) + ) + } +} 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..98e77ae7d5 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/legals/LegalsController.kt @@ -0,0 +1,152 @@ +/* + * 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.content.res.Resources +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.ServerAndPolicies +import im.vector.app.features.discovery.ServerPolicy +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 resources: Resources, + private val elementLegals: ElementLegals, + private val errorFormatter: ErrorFormatter +) : TypedEpoxyController() { + + var listener: Listener? = null + + override fun buildModels(data: LegalsState) { + buildAppSection() + buildHomeserverSection(data) + buildIdentityServerSection(data) + buildThirdPartyNotices() + } + + private fun buildAppSection() { + settingsSectionTitleItem { + id("appTitle") + titleResId(R.string.legals_application_title) + } + + buildPolicies("el", elementLegals.getData()) + } + + private fun buildHomeserverSection(data: LegalsState) { + settingsSectionTitleItem { + id("hsServerTitle") + titleResId(R.string.legals_home_server_title) + } + + buildPolicyAsync("hs", data.homeServer) + } + + private fun buildIdentityServerSection(data: LegalsState) { + if (data.hasIdentityServer) { + settingsSectionTitleItem { + id("idServerTitle") + titleResId(R.string.legals_identity_server_title) + } + + buildPolicyAsync("is", data.identityServer) + } + } + + private fun buildPolicyAsync(tag: String, serverAndPolicies: Async) { + val host = this + + when (serverAndPolicies) { + Uninitialized, + is Loading -> loadingItem { + id("loading_$tag") + } + is Success -> { + val policies = serverAndPolicies()?.policies + if (policies.isNullOrEmpty()) { + settingsInfoItem { + id("emptyPolicy") + helperText(host.stringProvider.getString(R.string.legals_no_policy_provided)) + } + } else { + buildPolicies(tag, policies) + } + } + is Fail -> { + errorWithRetryItem { + id("errorRetry_$tag") + text(host.errorFormatter.toHumanReadable(serverAndPolicies.error)) + listener { host.listener?.onTapRetry() } + } + } + } + } + + private fun buildPolicies(tag: String, policies: List) { + val host = this + + policies.forEach { policy -> + discoveryPolicyItem { + id(tag + policy.url) + name(policy.name) + url(policy.url.takeIf { it.startsWith("http") }) + clickListener { host.listener?.openPolicy(policy) } + } + } + } + + private fun buildThirdPartyNotices() { + val host = this + settingsSectionTitleItem { + id("thirdTitle") + titleResId(R.string.legals_third_party_notices) + } + + discoveryPolicyItem { + id("eltpn1") + name(host.stringProvider.getString(R.string.settings_third_party_notices)) + clickListener { host.listener?.openThirdPartyNotice() } + } + // Only on Gplay + if (resources.getBoolean(R.bool.isGplay)) { + discoveryPolicyItem { + id("eltpn2") + name(host.stringProvider.getString(R.string.settings_other_third_party_notices)) + clickListener { host.listener?.openThirdPartyNoticeGplay() } + } + } + } + + interface Listener { + fun onTapRetry() + fun openPolicy(policy: ServerPolicy) + fun openThirdPartyNotice() + fun openThirdPartyNoticeGplay() + } +} 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..f9b50bdead --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/legals/LegalsFragment.kt @@ -0,0 +1,101 @@ +/* + * 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.platform.VectorBaseFragment +import im.vector.app.core.utils.FirstThrottler +import im.vector.app.core.utils.displayInWebView +import im.vector.app.core.utils.openUrlInChromeCustomTab +import im.vector.app.databinding.FragmentGenericRecyclerBinding +import im.vector.app.features.discovery.ServerPolicy +import im.vector.app.features.settings.VectorSettingsUrls +import im.vector.app.openOssLicensesMenuActivity +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) + private val firstThrottler = FirstThrottler(1000) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + controller.listener = this + views.genericRecyclerView.configureWith(controller) + } + + 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: ServerPolicy) { + openUrl(policy.url) + } + + override fun openThirdPartyNotice() { + openUrl(VectorSettingsUrls.THIRD_PARTY_LICENSES) + } + + private fun openUrl(url: String) { + if (firstThrottler.canHandle() is FirstThrottler.CanHandlerResult.Yes) { + if (url.startsWith("file://")) { + activity?.displayInWebView(url) + } else { + openUrlInChromeCustomTab(requireContext(), null, url) + } + } + } + + override fun openThirdPartyNoticeGplay() { + if (firstThrottler.canHandle() is FirstThrottler.CanHandlerResult.Yes) { + // See https://developers.google.com/android/guides/opensource + openOssLicensesMenuActivity(requireActivity()) + } + } +} 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..ad01f85338 --- /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.ServerAndPolicies + +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/LegalsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/legals/LegalsViewModel.kt new file mode 100644 index 0000000000..9d58535490 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/legals/LegalsViewModel.kt @@ -0,0 +1,92 @@ +/* + * 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.EmptyViewEvents +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/java/im/vector/app/features/userdirectory/UserListViewEvents.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewEvents.kt index ffda0dd505..ef29e8015a 100644 --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewEvents.kt @@ -17,13 +17,13 @@ package im.vector.app.features.userdirectory import im.vector.app.core.platform.VectorViewEvents -import im.vector.app.features.discovery.IdentityServerWithTerms +import im.vector.app.features.discovery.ServerAndPolicies /** * Transient events for invite users to room screen */ sealed class UserListViewEvents : VectorViewEvents { data class Failure(val throwable: Throwable) : UserListViewEvents() - data class OnPoliciesRetrieved(val identityServerWithTerms: IdentityServerWithTerms?) : UserListViewEvents() + data class OnPoliciesRetrieved(val identityServerWithTerms: ServerAndPolicies?) : UserListViewEvents() data class OpenShareMatrixToLink(val link: String) : UserListViewEvents() } diff --git a/vector/src/main/res/drawable/ic_settings_root_legals.xml b/vector/src/main/res/drawable/ic_settings_root_legals.xml new file mode 100644 index 0000000000..8c5049b36e --- /dev/null +++ b/vector/src/main/res/drawable/ic_settings_root_legals.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/layout/item_discovery_policy.xml b/vector/src/main/res/layout/item_discovery_policy.xml index 5d8c7159e9..86333bdee8 100644 --- a/vector/src/main/res/layout/item_discovery_policy.xml +++ b/vector/src/main/res/layout/item_discovery_policy.xml @@ -5,8 +5,20 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?attr/selectableItemBackground" + android:minHeight="80dp" android:padding="16dp"> + + + tools:text="Copyright" /> + android:textColor="?android:textColorLink" + tools:text="https://element.io/copyright" /> Allow integrations Integration manager + ${app_name} policy + Your homeserver policy + Your identity server policy + Third party libraries + This server does not provide any policy. + Integrations are disabled "Enable 'Allow integrations' in Settings to do this." @@ -2270,7 +2276,13 @@ Voice & Video Help & About + Legals + Help + Help and support + Get help with using Element + Versions + System settings Register token @@ -3566,7 +3578,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_help_about.xml b/vector/src/main/res/xml/vector_settings_help_about.xml index b36fba05c2..0388b545b7 100644 --- a/vector/src/main/res/xml/vector_settings_help_about.xml +++ b/vector/src/main/res/xml/vector_settings_help_about.xml @@ -1,47 +1,42 @@ - + - + - + - + - + - + - + - + - + + + + + \ No newline at end of file diff --git a/vector/src/main/res/xml/vector_settings_root.xml b/vector/src/main/res/xml/vector_settings_root.xml index 32e21b3391..520c4595ca 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