Merge pull request #4660 from vector-im/feature/bma/legals

Legals
This commit is contained in:
Benoit Marty 2021-12-10 16:06:49 +01:00 committed by GitHub
commit 34898f1c81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 621 additions and 124 deletions

1
changelog.d/4638.feature Normal file
View File

@ -0,0 +1 @@
Add a help section in the settings.

1
changelog.d/4660.feature Normal file
View File

@ -0,0 +1 @@
Create a legal screen in the setting to group all the different policies.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<DiscoveryPolicyItem.Holder>() {
@ -40,7 +41,7 @@ abstract class DiscoveryPolicyItem : EpoxyModelWithHolder<DiscoveryPolicyItem.Ho
override fun bind(holder: Holder) {
super.bind(holder)
holder.title.text = name
holder.url.text = url
holder.url.setTextOrHide(url)
holder.view.onClick(clickListener)
}

View File

@ -433,6 +433,6 @@ class DiscoverySettingsController @Inject constructor(
fun onTapUpdateUserConsent(newValue: Boolean)
fun onTapRetryToRetrieveBindings()
fun onPolicyUrlsExpandedStateToggled(newExpandedState: Boolean)
fun onPolicyTapped(policy: IdentityServerPolicy)
fun onPolicyTapped(policy: ServerPolicy)
}
}

View File

@ -167,10 +167,11 @@ class DiscoverySettingsFragment @Inject constructor(
val pidList = state.emailList().orEmpty() + state.phoneNumbersList().orEmpty()
val hasBoundIds = pidList.any { it.isShared() == SharedState.SHARED }
val serverUrl = state.identityServer()?.serverUrl.orEmpty()
val message = if (hasBoundIds) {
getString(R.string.settings_discovery_disconnect_with_bound_pid, state.identityServer(), state.identityServer())
getString(R.string.settings_discovery_disconnect_with_bound_pid, serverUrl, serverUrl)
} else {
getString(R.string.disconnect_identity_server_dialog_content, state.identityServer())
getString(R.string.disconnect_identity_server_dialog_content, serverUrl)
}
MaterialAlertDialogBuilder(requireActivity())
@ -203,7 +204,7 @@ class DiscoverySettingsFragment @Inject constructor(
viewModel.handle(DiscoverySettingsAction.SetPoliciesExpandState(expanded = newExpandedState))
}
override fun onPolicyTapped(policy: IdentityServerPolicy) {
override fun onPolicyTapped(policy: ServerPolicy) {
openUrlInChromeCustomTab(requireContext(), null, policy.url)
}

View File

@ -21,7 +21,7 @@ import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.Uninitialized
data class DiscoverySettingsState(
val identityServer: Async<IdentityServerWithTerms?> = Uninitialized,
val identityServer: Async<ServerAndPolicies?> = Uninitialized,
val emailList: Async<List<PidInfo>> = Uninitialized,
val phoneNumbersList: Async<List<PidInfo>> = Uninitialized,
// Can be true if terms are updated

View File

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

View File

@ -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())
suspend fun Session.fetchIdentityServerWithTerms(userLanguage: String): ServerAndPolicies? {
return identityService().getCurrentIdentityServerUrl()
?.let { identityServerUrl ->
val termsResponse = getTerms(TermsService.ServiceType.IdentityService, identityServerUrl.ensureProtocol())
.serverResponse
.getLocalizedTerms(userLanguage)
buildServerAndPolicies(identityServerUrl, termsResponse, userLanguage)
}
}
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 {
IdentityServerPolicy(name = name, url = url)
ServerPolicy(name = name, url = url)
}
}
IdentityServerWithTerms(identityServerUrl, policyUrls)
}
return ServerAndPolicies(serviceUrl, policyUrls)
}

View File

@ -16,12 +16,12 @@
package im.vector.app.features.discovery
data class IdentityServerWithTerms(
data class ServerAndPolicies(
val serverUrl: String,
val policies: List<IdentityServerPolicy>
val policies: List<ServerPolicy>
)
data class IdentityServerPolicy(
data class ServerPolicy(
val name: String,
val url: String
)

View File

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

View File

@ -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<VectorPreference>(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<VectorPreference>(APP_INFO_LINK_PREFERENCE_KEY)!!
.onPreferenceClickListener = Preference.OnPreferenceClickListener {
@ -76,44 +83,6 @@ class VectorSettingsHelpAboutFragment @Inject constructor(
// olm version
findPreference<VectorPreference>(VectorPreferences.SETTINGS_OLM_VERSION_PREFERENCE_KEY)!!
.summary = session.cryptoService().getCryptoVersion(requireContext(), false)
// copyright
findPreference<VectorPreference>(VectorPreferences.SETTINGS_COPYRIGHT_PREFERENCE_KEY)!!
.onPreferenceClickListener = Preference.OnPreferenceClickListener {
openUrlInChromeCustomTab(requireContext(), null, VectorSettingsUrls.COPYRIGHT)
false
}
// terms & conditions
findPreference<VectorPreference>(VectorPreferences.SETTINGS_APP_TERM_CONDITIONS_PREFERENCE_KEY)!!
.onPreferenceClickListener = Preference.OnPreferenceClickListener {
openUrlInChromeCustomTab(requireContext(), null, VectorSettingsUrls.TAC)
false
}
// privacy policy
findPreference<VectorPreference>(VectorPreferences.SETTINGS_PRIVACY_POLICY_PREFERENCE_KEY)!!
.onPreferenceClickListener = Preference.OnPreferenceClickListener {
openUrlInChromeCustomTab(requireContext(), null, VectorSettingsUrls.PRIVACY_POLICY)
false
}
// third party notice
findPreference<VectorPreference>(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<VectorPreference>(VectorPreferences.SETTINGS_OTHER_THIRD_PARTY_NOTICES_PREFERENCE_KEY)!!
.onPreferenceClickListener = Preference.OnPreferenceClickListener {
// See https://developers.google.com/android/guides/opensource
openOssLicensesMenuActivity(requireActivity())
false
}
}
companion object {

View File

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

View File

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

View File

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

View File

@ -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<LegalsState>() {
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<ServerAndPolicies?>) {
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<ServerPolicy>) {
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()
}
}

View File

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

View File

@ -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<ServerAndPolicies?> = Uninitialized,
val hasIdentityServer: Boolean = false,
val identityServer: Async<ServerAndPolicies?> = Uninitialized
) : MavericksState

View File

@ -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<LegalsState, LegalsAction, EmptyViewEvents>(initialState) {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<LegalsViewModel, LegalsState> {
override fun create(initialState: LegalsState): LegalsViewModel
}
companion object : MavericksViewModelFactory<LegalsViewModel, LegalsState> 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)) } }
)
}
}
}
}

View File

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

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="M4,5C4,3.3431 5.3432,2 7,2H17C18.6569,2 20,3.3431 20,5V19C20,20.6569 18.6569,22 17,22H7C5.3432,22 4,20.6569 4,19V5ZM7,14.25C7,13.8358 7.3358,13.5 7.75,13.5H16.25C16.6642,13.5 17,13.8358 17,14.25C17,14.6642 16.6642,15 16.25,15H7.75C7.3358,15 7,14.6642 7,14.25ZM7.75,17C7.3358,17 7,17.3358 7,17.75C7,18.1642 7.3358,18.5 7.75,18.5H12.25C12.6642,18.5 13,18.1642 13,17.75C13,17.3358 12.6642,17 12.25,17H7.75Z" />
</vector>

View File

@ -5,8 +5,20 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:minHeight="80dp"
android:padding="16dp">
<androidx.constraintlayout.helper.widget.Flow
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
app:constraint_referenced_ids="discovery_policy_name,discovery_policy_url"
app:flow_verticalGap="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/discovery_policy_arrow"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/discovery_policy_name"
style="@style/Widget.Vector.TextView.Body"
@ -16,10 +28,7 @@
android:paddingEnd="8dp"
android:textColor="?vctr_content_primary"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@id/discovery_policy_arrow"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Integration manager" />
tools:text="Copyright" />
<TextView
android:id="@+id/discovery_policy_url"
@ -28,11 +37,8 @@
android:layout_height="wrap_content"
android:paddingStart="0dp"
android:paddingEnd="8dp"
android:textColor="?vctr_content_secondary"
app:layout_constraintEnd_toStartOf="@id/discovery_policy_arrow"
app:layout_constraintStart_toStartOf="@id/discovery_policy_name"
app:layout_constraintTop_toBottomOf="@id/discovery_policy_name"
tools:text="Use bots, bridges, widget and sticker packs." />
android:textColor="?android:textColorLink"
tools:text="https://element.io/copyright" />
<!-- Do not use drawableEnd on the TextView because of RTL support -->
<ImageView

View File

@ -1397,6 +1397,12 @@
<string name="settings_integration_allow">Allow integrations</string>
<string name="settings_integration_manager">Integration manager</string>
<string name="template_legals_application_title">${app_name} policy</string>
<string name="legals_home_server_title">Your homeserver policy</string>
<string name="legals_identity_server_title">Your identity server policy</string>
<string name="legals_third_party_notices">Third party libraries</string>
<string name="legals_no_policy_provided">This server does not provide any policy.</string>
<string name="disabled_integration_dialog_title">Integrations are disabled</string>
<string name="disabled_integration_dialog_content">"Enable 'Allow integrations' in Settings to do this."</string>
@ -2270,7 +2276,13 @@
<string name="preference_voice_and_video">Voice &amp; Video</string>
<string name="preference_root_help_about">Help &amp; About</string>
<string name="preference_root_legals">Legals</string>
<string name="preference_help">Help</string>
<string name="preference_help_title">Help and support</string>
<string name="preference_help_summary">Get help with using Element</string>
<string name="preference_versions">Versions</string>
<string name="preference_system_settings">System settings</string>
<string name="settings_troubleshoot_test_token_registration_quick_fix">Register token</string>
@ -3566,7 +3578,6 @@
<string name="space_manage_rooms_and_spaces">Manage rooms and spaces</string>
<string name="preference_show_all_rooms_in_home">Show all rooms in Home</string>
<string name="all_rooms_youre_in_will_be_shown_in_home">All rooms youre in will be shown in Home.</string>

View File

@ -1,12 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<im.vector.app.core.preference.VectorPreferenceCategory android:title="@string/preference_help">
<im.vector.app.core.preference.VectorPreference
android:key="APP_INFO_LINK_PREFERENCE_KEY"
android:summary="@string/settings_app_info_link_summary"
android:title="@string/settings_app_info_link_title" />
android:key="SETTINGS_HELP_PREFERENCE_KEY"
android:summary="@string/preference_help_summary"
android:title="@string/preference_help_title" />
</im.vector.app.core.preference.VectorPreferenceCategory>
<im.vector.app.core.preference.VectorPreferenceCategory android:title="@string/preference_versions">
<im.vector.app.core.preference.VectorPreference
android:key="SETTINGS_VERSION_PREFERENCE_KEY"
@ -23,25 +28,15 @@
android:title="@string/settings_olm_version"
tools:summary="7.8.9" />
<im.vector.app.core.preference.VectorPreference
android:key="SETTINGS_COPYRIGHT_PREFERENCE_KEY"
android:title="@string/settings_copyright" />
</im.vector.app.core.preference.VectorPreferenceCategory>
<im.vector.app.core.preference.VectorPreferenceCategory android:title="@string/preference_system_settings">
<im.vector.app.core.preference.VectorPreference
android:key="SETTINGS_APP_TERM_CONDITIONS_PREFERENCE_KEY"
android:title="@string/settings_app_term_conditions" />
android:key="APP_INFO_LINK_PREFERENCE_KEY"
android:summary="@string/settings_app_info_link_summary"
android:title="@string/settings_app_info_link_title" />
<im.vector.app.core.preference.VectorPreference
android:key="SETTINGS_PRIVACY_POLICY_PREFERENCE_KEY"
android:title="@string/settings_privacy_policy" />
<im.vector.app.core.preference.VectorPreference
android:key="SETTINGS_THIRD_PARTY_NOTICES_PREFERENCE_KEY"
android:title="@string/settings_third_party_notices" />
<im.vector.app.core.preference.VectorPreference
android:key="SETTINGS_OTHER_THIRD_PARTY_NOTICES_PREFERENCE_KEY"
android:title="@string/settings_other_third_party_notices"
app:isPreferenceVisible="@bool/isGplay" />
</im.vector.app.core.preference.VectorPreferenceCategory>
</androidx.preference.PreferenceScreen>

View File

@ -53,4 +53,9 @@
android:title="@string/preference_root_help_about"
app:fragment="im.vector.app.features.settings.VectorSettingsHelpAboutFragment" />
<im.vector.app.core.preference.VectorPreference
android:icon="@drawable/ic_settings_root_legals"
android:title="@string/preference_root_legals"
app:fragment="im.vector.app.features.settings.legals.LegalsFragment" />
</androidx.preference.PreferenceScreen>