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
* 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
/**
* 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
* 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 TermsNotSignedException : IdentityServiceError()
object BulkLookupSha256NotSupported : IdentityServiceError()
object UserConsentNotProvided : IdentityServiceError()
object BindingError : 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 kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import org.matrix.android.sdk.api.extensions.orFalse
import timber.log.Timber
import javax.inject.Inject
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 {
if (!getUserConsent()) {
callback.onFailure(IdentityServiceError.UserConsentNotProvided)
return NoOpCancellable
}
if (threePids.isEmpty()) {
callback.onSuccess(emptyList())
return NoOpCancellable
@ -255,6 +269,9 @@ internal class DefaultIdentityService @Inject constructor(
}
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()) {
callback.onSuccess(emptyMap())
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,
pepper: 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.BindingError -> R.string.identity_server_error_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()
data class ChangeIdentityServer(val url: String) : DiscoverySettingsAction()
data class UpdateUserConsent(val newConsent: Boolean) : DiscoverySettingsAction()
data class RevokeThreePid(val threePid: ThreePid) : DiscoverySettingsAction()
data class ShareThreePid(val threePid: ThreePid) : DiscoverySettingsAction()
data class FinalizeBind3pid(val threePid: ThreePid) : DiscoverySettingsAction()

View File

@ -65,6 +65,7 @@ class DiscoverySettingsController @Inject constructor(
buildIdentityServerSection(data)
val hasIdentityServer = data.identityServer().isNullOrBlank().not()
if (hasIdentityServer && !data.termsNotSigned) {
buildConsentSection(data)
buildEmailsSection(data.emailList)
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) {
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 onTapChangeIdentityServer()
fun onTapDisconnectIdentityServer()
fun onTapUpdateUserConsent(newValue: Boolean)
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() {
viewModel.handle(DiscoverySettingsAction.RetrieveBinding)
}

View File

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

View File

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