Ask for explicit user consent to send their contact details to the identity server (#2375)

This commit is contained in:
Benoit Marty 2020-11-11 17:12:42 +01:00
parent 99bea8f7c3
commit 6020f423f4
7 changed files with 75 additions and 7 deletions

View file

@ -6,6 +6,7 @@ Features ✨:
Improvements 🙌:
- Open an existing DM instead of creating a new one (#2319)
- Ask for explicit user consent to send their contact details to the identity server (#2375)
Bugfix 🐛:
- Fix issue when restoring draft after sharing (#2287)

View file

@ -21,4 +21,5 @@ import im.vector.app.core.platform.VectorViewModelAction
sealed class ContactsBookAction : VectorViewModelAction {
data class FilterWith(val filter: String) : ContactsBookAction()
data class OnlyBoundContacts(val onlyBoundContacts: Boolean) : ContactsBookAction()
object UserConsentGranted : ContactsBookAction()
}

View file

@ -18,6 +18,7 @@ package im.vector.app.features.contactsbook
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
@ -57,10 +58,26 @@ class ContactsBookFragment @Inject constructor(
sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
setupRecyclerView()
setupFilterView()
setupConsentView()
setupOnlyBoundContactsView()
setupCloseView()
}
private fun setupConsentView() {
phoneBookSearchForMatrixContacts.setOnClickListener {
withState(contactsBookViewModel) { state ->
AlertDialog.Builder(requireActivity())
.setTitle(R.string.identity_server_consent_dialog_title)
.setMessage(getString(R.string.identity_server_consent_dialog_content, state.identityServerUrl ?: ""))
.setPositiveButton(R.string.yes) { _, _ ->
contactsBookViewModel.handle(ContactsBookAction.UserConsentGranted)
}
.setNegativeButton(R.string.no, null)
.show()
}
}
}
private fun setupOnlyBoundContactsView() {
phoneBookOnlyBoundContacts.checkedChanges()
.subscribe {
@ -98,6 +115,7 @@ class ContactsBookFragment @Inject constructor(
}
override fun invalidate() = withState(contactsBookViewModel) { state ->
phoneBookSearchForMatrixContacts.isVisible = state.filteredMappedContacts.isNotEmpty() && state.identityServerUrl != null && !state.userConsent
phoneBookOnlyBoundContacts.isVisible = state.isBoundRetrieved
contactsBookController.setData(state)
}

View file

@ -38,11 +38,10 @@ import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.identity.FoundThreePid
import org.matrix.android.sdk.api.session.identity.IdentityServiceError
import org.matrix.android.sdk.api.session.identity.ThreePid
import timber.log.Timber
private typealias PhoneBookSearch = String
class ContactsBookViewModel @AssistedInject constructor(@Assisted
initialState: ContactsBookViewState,
private val contactsDataSource: ContactsDataSource,
@ -85,7 +84,9 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted
private fun loadContacts() {
setState {
copy(
mappedContacts = Loading()
mappedContacts = Loading(),
identityServerUrl = session.identityService().getCurrentIdentityServerUrl(),
userConsent = session.identityService().getUserConsent()
)
}
@ -109,6 +110,9 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted
}
private fun performLookup(data: List<MappedContact>) {
if (!session.identityService().getUserConsent()) {
return
}
viewModelScope.launch {
val threePids = data.flatMap { contact ->
contact.emails.map { ThreePid.Email(it.email) } +
@ -116,8 +120,14 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted
}
session.identityService().lookUp(threePids, object : MatrixCallback<List<FoundThreePid>> {
override fun onFailure(failure: Throwable) {
// Ignore
Timber.w(failure, "Unable to perform the lookup")
// Should not happen, but just to be sure
if (failure is IdentityServiceError.UserConsentNotProvided) {
setState {
copy(userConsent = false)
}
}
}
override fun onSuccess(data: List<FoundThreePid>) {
@ -171,9 +181,21 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted
when (action) {
is ContactsBookAction.FilterWith -> handleFilterWith(action)
is ContactsBookAction.OnlyBoundContacts -> handleOnlyBoundContacts(action)
ContactsBookAction.UserConsentGranted -> handleUserConsentGranted()
}.exhaustive
}
private fun handleUserConsentGranted() {
session.identityService().setUserConsent(true)
setState {
copy(userConsent = true)
}
// Perform the lookup
performLookup(allContacts)
}
private fun handleOnlyBoundContacts(action: ContactsBookAction.OnlyBoundContacts) {
setState {
copy(

View file

@ -26,10 +26,14 @@ data class ContactsBookViewState(
val mappedContacts: Async<List<MappedContact>> = Loading(),
// Use to filter contacts by display name
val searchTerm: String = "",
// Tru to display only bound contacts with their bound 2pid
// True to display only bound contacts with their bound 2pid
val onlyBoundContacts: Boolean = false,
// All contacts, filtered by searchTerm and onlyBoundContacts
val filteredMappedContacts: List<MappedContact> = emptyList(),
// True when the identity service has return some data
val isBoundRetrieved: Boolean = false
val isBoundRetrieved: Boolean = false,
// The current identity server url if any
val identityServerUrl: String? = null,
// User consent to perform lookup (send emails to the identity server)
val userConsent: Boolean = false
) : MvRxState

View file

@ -93,6 +93,27 @@
app:layout_constraintTop_toBottomOf="@+id/phoneBookFilterContainer"
tools:visibility="visible" />
<com.google.android.material.button.MaterialButton
android:id="@+id/phoneBookSearchForMatrixContacts"
style="@style/VectorButtonStyleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginTop="4dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:text="@string/phone_book_perform_lookup"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/phoneBookFilterContainer"
tools:visibility="visible" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/phoneBookBottomBarrier"
android:layout_width="0dp"
android:layout_height="0dp"
app:barrierDirection="bottom"
app:constraint_referenced_ids="phoneBookSearchForMatrixContacts,phoneBookOnlyBoundContacts" />
<View
android:id="@+id/phoneBookFilterDivider"
android:layout_width="0dp"
@ -101,7 +122,7 @@
android:background="?attr/vctr_list_divider_color"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/phoneBookOnlyBoundContacts" />
app:layout_constraintTop_toBottomOf="@+id/phoneBookBottomBarrier" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/phoneBookRecyclerView"

View file

@ -2602,6 +2602,7 @@
<string name="loading_contact_book">Retrieving your contacts…</string>
<string name="empty_contact_book">Your contact book is empty</string>
<string name="contacts_book_title">Contacts book</string>
<string name="phone_book_perform_lookup">Search for contacts on Matrix</string>
<string name="three_pid_revoke_invite_dialog_title">Revoke invite</string>
<string name="three_pid_revoke_invite_dialog_content">Revoke invite to %1$s?</string>