Add userConsent UI to the Discovery screen

This commit is contained in:
Benoit Marty 2020-11-11 16:07:18 +01:00
parent ccf5d759a4
commit d1e2d06538
11 changed files with 130 additions and 5 deletions

View File

@ -92,9 +92,24 @@ interface IdentityService {
/** /**
* Search MatrixId of users providing email and phone numbers * Search MatrixId of users providing email and phone numbers
* Note the the user consent has to be set to true, or it will throw a UserConsentNotProvided failure
* Application has to explicitly ask for the user consent.
* Please see https://support.google.com/googleplay/android-developer/answer/9888076?hl=en for more details.
*/ */
fun lookUp(threePids: List<ThreePid>, callback: MatrixCallback<List<FoundThreePid>>): Cancelable fun lookUp(threePids: List<ThreePid>, callback: MatrixCallback<List<FoundThreePid>>): Cancelable
/**
* Return the current user consent
*/
fun getUserConsent(): Boolean
/**
* Set the user consent. Application may have explicitly ask for the user consent to send their private data
* (email and phone numbers) to the identity server.
* Please see https://support.google.com/googleplay/android-developer/answer/9888076?hl=en for more details.
*/
fun setUserConsent(newValue: Boolean)
/** /**
* Get the status of the current user's threePid * Get the status of the current user's threePid
* A lookup will be performed, but also pending binding state will be restored * A lookup will be performed, but also pending binding state will be restored

View File

@ -24,6 +24,7 @@ sealed class IdentityServiceError : Failure.FeatureFailure() {
object NoIdentityServerConfigured : IdentityServiceError() object NoIdentityServerConfigured : IdentityServiceError()
object TermsNotSignedException : IdentityServiceError() object TermsNotSignedException : IdentityServiceError()
object BulkLookupSha256NotSupported : IdentityServiceError() object BulkLookupSha256NotSupported : IdentityServiceError()
object UserConsentNotProvided : IdentityServiceError()
object BindingError : IdentityServiceError() object BindingError : IdentityServiceError()
object NoCurrentBindingError : IdentityServiceError() object NoCurrentBindingError : IdentityServiceError()
} }

View File

@ -55,6 +55,7 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import org.matrix.android.sdk.internal.util.ensureProtocol import org.matrix.android.sdk.internal.util.ensureProtocol
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.matrix.android.sdk.api.extensions.orFalse
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
@ -243,7 +244,20 @@ internal class DefaultIdentityService @Inject constructor(
)) ))
} }
override fun getUserConsent(): Boolean {
return identityStore.getIdentityData()?.userConsent.orFalse()
}
override fun setUserConsent(newValue: Boolean) {
identityStore.setUserConsent(newValue)
}
override fun lookUp(threePids: List<ThreePid>, callback: MatrixCallback<List<FoundThreePid>>): Cancelable { override fun lookUp(threePids: List<ThreePid>, callback: MatrixCallback<List<FoundThreePid>>): Cancelable {
if (!getUserConsent()) {
callback.onFailure(IdentityServiceError.UserConsentNotProvided)
return NoOpCancellable
}
if (threePids.isEmpty()) { if (threePids.isEmpty()) {
callback.onSuccess(emptyList()) callback.onSuccess(emptyList())
return NoOpCancellable return NoOpCancellable
@ -255,6 +269,9 @@ internal class DefaultIdentityService @Inject constructor(
} }
override fun getShareStatus(threePids: List<ThreePid>, callback: MatrixCallback<Map<ThreePid, SharedState>>): Cancelable { override fun getShareStatus(threePids: List<ThreePid>, callback: MatrixCallback<Map<ThreePid, SharedState>>): Cancelable {
// Note: we do not require user consent here, because it is used for email and phone numbers that the user has already sent
// to the home server. Identity server is another service though...
if (threePids.isEmpty()) { if (threePids.isEmpty()) {
callback.onSuccess(emptyMap()) callback.onSuccess(emptyMap())
return NoOpCancellable return NoOpCancellable

View File

@ -52,6 +52,13 @@ internal fun IdentityDataEntity.Companion.setToken(realm: Realm,
} }
} }
internal fun IdentityDataEntity.Companion.setUserConsent(realm: Realm,
newConsent: Boolean) {
get(realm)?.apply {
userConsent = newConsent
}
}
internal fun IdentityDataEntity.Companion.setHashDetails(realm: Realm, internal fun IdentityDataEntity.Companion.setHashDetails(realm: Realm,
pepper: String, pepper: String,
algorithms: List<String>) { algorithms: List<String>) {

View File

@ -136,6 +136,7 @@ class DefaultErrorFormatter @Inject constructor(
IdentityServiceError.BulkLookupSha256NotSupported -> R.string.identity_server_error_bulk_sha256_not_supported IdentityServiceError.BulkLookupSha256NotSupported -> R.string.identity_server_error_bulk_sha256_not_supported
IdentityServiceError.BindingError -> R.string.identity_server_error_binding_error IdentityServiceError.BindingError -> R.string.identity_server_error_binding_error
IdentityServiceError.NoCurrentBindingError -> R.string.identity_server_error_no_current_binding_error IdentityServiceError.NoCurrentBindingError -> R.string.identity_server_error_no_current_binding_error
IdentityServiceError.UserConsentNotProvided -> R.string.identity_server_user_consent_not_provided
}) })
} }
} }

View File

@ -25,6 +25,7 @@ sealed class DiscoverySettingsAction : VectorViewModelAction {
object DisconnectIdentityServer : DiscoverySettingsAction() object DisconnectIdentityServer : DiscoverySettingsAction()
data class ChangeIdentityServer(val url: String) : DiscoverySettingsAction() data class ChangeIdentityServer(val url: String) : DiscoverySettingsAction()
data class UpdateUserConsent(val newConsent: Boolean) : DiscoverySettingsAction()
data class RevokeThreePid(val threePid: ThreePid) : DiscoverySettingsAction() data class RevokeThreePid(val threePid: ThreePid) : DiscoverySettingsAction()
data class ShareThreePid(val threePid: ThreePid) : DiscoverySettingsAction() data class ShareThreePid(val threePid: ThreePid) : DiscoverySettingsAction()
data class FinalizeBind3pid(val threePid: ThreePid) : DiscoverySettingsAction() data class FinalizeBind3pid(val threePid: ThreePid) : DiscoverySettingsAction()

View File

@ -65,6 +65,7 @@ class DiscoverySettingsController @Inject constructor(
buildIdentityServerSection(data) buildIdentityServerSection(data)
val hasIdentityServer = data.identityServer().isNullOrBlank().not() val hasIdentityServer = data.identityServer().isNullOrBlank().not()
if (hasIdentityServer && !data.termsNotSigned) { if (hasIdentityServer && !data.termsNotSigned) {
buildConsentSection(data)
buildEmailsSection(data.emailList) buildEmailsSection(data.emailList)
buildMsisdnSection(data.phoneNumbersList) buildMsisdnSection(data.phoneNumbersList)
} }
@ -72,6 +73,38 @@ class DiscoverySettingsController @Inject constructor(
} }
} }
private fun buildConsentSection(data: DiscoverySettingsState) {
settingsSectionTitleItem {
id("idConsentTitle")
titleResId(R.string.settings_discovery_consent_title)
}
if (data.userConsent) {
settingsInfoItem {
id("idConsentInfo")
helperTextResId(R.string.settings_discovery_consent_notice_on)
}
settingsButtonItem {
id("idConsentButton")
colorProvider(colorProvider)
buttonTitleId(R.string.settings_discovery_consent_action_revoke)
buttonStyle(ButtonStyle.DESTRUCTIVE)
buttonClickListener { listener?.onTapUpdateUserConsent(false) }
}
} else {
settingsInfoItem {
id("idConsentInfo")
helperTextResId(R.string.settings_discovery_consent_notice_off)
}
settingsButtonItem {
id("idConsentButton")
colorProvider(colorProvider)
buttonTitleId(R.string.settings_discovery_consent_action_give_consent)
buttonClickListener { listener?.onTapUpdateUserConsent(true) }
}
}
}
private fun buildIdentityServerSection(data: DiscoverySettingsState) { private fun buildIdentityServerSection(data: DiscoverySettingsState) {
val identityServer = data.identityServer() ?: stringProvider.getString(R.string.none) val identityServer = data.identityServer() ?: stringProvider.getString(R.string.none)
@ -359,6 +392,7 @@ class DiscoverySettingsController @Inject constructor(
fun sendMsisdnVerificationCode(threePid: ThreePid.Msisdn, code: String) fun sendMsisdnVerificationCode(threePid: ThreePid.Msisdn, code: String)
fun onTapChangeIdentityServer() fun onTapChangeIdentityServer()
fun onTapDisconnectIdentityServer() fun onTapDisconnectIdentityServer()
fun onTapUpdateUserConsent(newValue: Boolean)
fun onTapRetryToRetrieveBindings() fun onTapRetryToRetrieveBindings()
} }
} }

View File

@ -170,6 +170,23 @@ class DiscoverySettingsFragment @Inject constructor(
} }
} }
override fun onTapUpdateUserConsent(newValue: Boolean) {
if (newValue) {
withState(viewModel) { state ->
AlertDialog.Builder(requireActivity())
.setTitle(R.string.identity_server_consent_dialog_title)
.setMessage(getString(R.string.identity_server_consent_dialog_content, state.identityServer.invoke()))
.setPositiveButton(R.string.yes) { _, _ ->
viewModel.handle(DiscoverySettingsAction.UpdateUserConsent(true))
}
.setNegativeButton(R.string.no, null)
.show()
}
} else {
viewModel.handle(DiscoverySettingsAction.UpdateUserConsent(false))
}
}
override fun onTapRetryToRetrieveBindings() { override fun onTapRetryToRetrieveBindings() {
viewModel.handle(DiscoverySettingsAction.RetrieveBinding) viewModel.handle(DiscoverySettingsAction.RetrieveBinding)
} }

View File

@ -25,5 +25,6 @@ data class DiscoverySettingsState(
val emailList: Async<List<PidInfo>> = Uninitialized, val emailList: Async<List<PidInfo>> = Uninitialized,
val phoneNumbersList: Async<List<PidInfo>> = Uninitialized, val phoneNumbersList: Async<List<PidInfo>> = Uninitialized,
// Can be true if terms are updated // Can be true if terms are updated
val termsNotSigned: Boolean = false val termsNotSigned: Boolean = false,
val userConsent: Boolean = false
) : MvRxState ) : MvRxState

View File

@ -63,7 +63,10 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
val identityServerUrl = identityService.getCurrentIdentityServerUrl() val identityServerUrl = identityService.getCurrentIdentityServerUrl()
val currentIS = state.identityServer() val currentIS = state.identityServer()
setState { setState {
copy(identityServer = Success(identityServerUrl)) copy(
identityServer = Success(identityServerUrl),
userConsent = false
)
} }
if (currentIS != identityServerUrl) retrieveBinding() if (currentIS != identityServerUrl) retrieveBinding()
} }
@ -71,7 +74,10 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
init { init {
setState { setState {
copy(identityServer = Success(identityService.getCurrentIdentityServerUrl())) copy(
identityServer = Success(identityService.getCurrentIdentityServerUrl()),
userConsent = identityService.getUserConsent()
)
} }
startListenToIdentityManager() startListenToIdentityManager()
observeThreePids() observeThreePids()
@ -97,6 +103,7 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
DiscoverySettingsAction.RetrieveBinding -> retrieveBinding() DiscoverySettingsAction.RetrieveBinding -> retrieveBinding()
DiscoverySettingsAction.DisconnectIdentityServer -> disconnectIdentityServer() DiscoverySettingsAction.DisconnectIdentityServer -> disconnectIdentityServer()
is DiscoverySettingsAction.ChangeIdentityServer -> changeIdentityServer(action) is DiscoverySettingsAction.ChangeIdentityServer -> changeIdentityServer(action)
is DiscoverySettingsAction.UpdateUserConsent -> handleUpdateUserConsent(action)
is DiscoverySettingsAction.RevokeThreePid -> revokeThreePid(action) is DiscoverySettingsAction.RevokeThreePid -> revokeThreePid(action)
is DiscoverySettingsAction.ShareThreePid -> shareThreePid(action) is DiscoverySettingsAction.ShareThreePid -> shareThreePid(action)
is DiscoverySettingsAction.FinalizeBind3pid -> finalizeBind3pid(action, true) is DiscoverySettingsAction.FinalizeBind3pid -> finalizeBind3pid(action, true)
@ -105,13 +112,23 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
}.exhaustive }.exhaustive
} }
private fun handleUpdateUserConsent(action: DiscoverySettingsAction.UpdateUserConsent) {
identityService.setUserConsent(action.newConsent)
setState { copy(userConsent = action.newConsent) }
}
private fun disconnectIdentityServer() { private fun disconnectIdentityServer() {
setState { copy(identityServer = Loading()) } setState { copy(identityServer = Loading()) }
viewModelScope.launch { viewModelScope.launch {
try { try {
awaitCallback<Unit> { session.identityService().disconnect(it) } awaitCallback<Unit> { session.identityService().disconnect(it) }
setState { copy(identityServer = Success(null)) } setState {
copy(
identityServer = Success(null),
userConsent = false
)
}
} catch (failure: Throwable) { } catch (failure: Throwable) {
setState { copy(identityServer = Fail(failure)) } setState { copy(identityServer = Fail(failure)) }
} }
@ -126,7 +143,12 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
val data = awaitCallback<String?> { val data = awaitCallback<String?> {
session.identityService().setNewIdentityServer(action.url, it) session.identityService().setNewIdentityServer(action.url, it)
} }
setState { copy(identityServer = Success(data)) } setState {
copy(
identityServer = Success(data),
userConsent = false
)
}
retrieveBinding() retrieveBinding()
} catch (failure: Throwable) { } catch (failure: Throwable) {
setState { copy(identityServer = Fail(failure)) } setState { copy(identityServer = Fail(failure)) }

View File

@ -1793,6 +1793,14 @@
<string name="settings_discovery_confirm_mail">We sent you a confirm email to %s, check your email and click on the confirmation link</string> <string name="settings_discovery_confirm_mail">We sent you a confirm email to %s, check your email and click on the confirmation link</string>
<string name="settings_discovery_confirm_mail_not_clicked">We sent you a confirm email to %s, please first check your email and click on the confirmation link</string> <string name="settings_discovery_confirm_mail_not_clicked">We sent you a confirm email to %s, please first check your email and click on the confirmation link</string>
<string name="settings_discovery_mail_pending">Pending</string> <string name="settings_discovery_mail_pending">Pending</string>
<string name="settings_discovery_consent_title">Send emails and phone numbers</string>
<string name="settings_discovery_consent_notice_on">You have given your consent to send emails and phone numbers to this identity server to discover other users from your contacts.</string>
<string name="settings_discovery_consent_notice_off">You have not given your consent to send emails and phone numbers to this identity server to discover other users from your contacts.</string>
<string name="settings_discovery_consent_action_revoke">Revoke my consent</string>
<string name="settings_discovery_consent_action_give_consent">Give consent</string>
<string name="identity_server_consent_dialog_title">Send emails and phone numbers</string>
<string name="identity_server_consent_dialog_content">In order to discover existing contacts you know, do you accept to send your contact data (phone numbers and/or emails) to the configured Identity Server (%1$s)?\n\nFor more privacy, the sent data will be hashed before being sent.</string>
<string name="settings_discovery_enter_identity_server">Enter an identity server URL</string> <string name="settings_discovery_enter_identity_server">Enter an identity server URL</string>
<string name="settings_discovery_bad_identity_server">Could not connect to identity server</string> <string name="settings_discovery_bad_identity_server">Could not connect to identity server</string>
@ -2527,6 +2535,7 @@
<string name="identity_server_error_bulk_sha256_not_supported">For your privacy, Element only supports sending hashed user emails and phone number.</string> <string name="identity_server_error_bulk_sha256_not_supported">For your privacy, Element only supports sending hashed user emails and phone number.</string>
<string name="identity_server_error_binding_error">The association has failed.</string> <string name="identity_server_error_binding_error">The association has failed.</string>
<string name="identity_server_error_no_current_binding_error">The is no current association with this identifier.</string> <string name="identity_server_error_no_current_binding_error">The is no current association with this identifier.</string>
<string name="identity_server_user_consent_not_provided">The user consent has not been provided.</string>
<string name="identity_server_set_default_notice">Your homeserver (%1$s) proposes to use %2$s for your identity server</string> <string name="identity_server_set_default_notice">Your homeserver (%1$s) proposes to use %2$s for your identity server</string>
<string name="identity_server_set_default_submit">Use %1$s</string> <string name="identity_server_set_default_submit">Use %1$s</string>