mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-02-18 13:00:18 +03:00
Merge pull request #2377 from vector-im/feature/bma/consent
Feature/bma/consent
This commit is contained in:
commit
0022777a4f
25 changed files with 279 additions and 22 deletions
|
@ -6,6 +6,7 @@ Features ✨:
|
||||||
|
|
||||||
Improvements 🙌:
|
Improvements 🙌:
|
||||||
- Open an existing DM instead of creating a new one (#2319)
|
- 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)
|
||||||
- Handle events of type "m.room.server_acl" (#890)
|
- Handle events of type "m.room.server_acl" (#890)
|
||||||
|
|
||||||
Bugfix 🐛:
|
Bugfix 🐛:
|
||||||
|
|
|
@ -92,9 +92,29 @@ 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, and the answer can be stored using [setUserConsent]
|
||||||
|
* 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 for the current identity server, which has been stored using [setUserConsent].
|
||||||
|
* If [setUserConsent] has not been called, the returned value will be false.
|
||||||
|
* Note that if the identity server is changed, the user consent is reset to false.
|
||||||
|
* @return the value stored using [setUserConsent] or false if [setUserConsent] has never been called, or if the identity server
|
||||||
|
* has been changed
|
||||||
|
*/
|
||||||
|
fun getUserConsent(): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the user consent to the provided value. Application MUST 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.
|
||||||
|
* @param newValue true if the user explicitly give their consent, false if the user wants to revoke their consent.
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 emails and phone numbers that the user has already sent
|
||||||
|
// to the home server, and not emails and phone numbers from the contact book of the user
|
||||||
|
|
||||||
if (threePids.isEmpty()) {
|
if (threePids.isEmpty()) {
|
||||||
callback.onSuccess(emptyMap())
|
callback.onSuccess(emptyMap())
|
||||||
return NoOpCancellable
|
return NoOpCancellable
|
||||||
|
|
|
@ -34,6 +34,7 @@ import org.matrix.android.sdk.internal.session.identity.db.IdentityRealmModule
|
||||||
import org.matrix.android.sdk.internal.session.identity.db.RealmIdentityStore
|
import org.matrix.android.sdk.internal.session.identity.db.RealmIdentityStore
|
||||||
import io.realm.RealmConfiguration
|
import io.realm.RealmConfiguration
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
import org.matrix.android.sdk.internal.session.identity.db.RealmIdentityStoreMigration
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
@ -59,6 +60,7 @@ internal abstract class IdentityModule {
|
||||||
@SessionScope
|
@SessionScope
|
||||||
fun providesIdentityRealmConfiguration(realmKeysUtils: RealmKeysUtils,
|
fun providesIdentityRealmConfiguration(realmKeysUtils: RealmKeysUtils,
|
||||||
@SessionFilesDirectory directory: File,
|
@SessionFilesDirectory directory: File,
|
||||||
|
migration: RealmIdentityStoreMigration,
|
||||||
@UserMd5 userMd5: String): RealmConfiguration {
|
@UserMd5 userMd5: String): RealmConfiguration {
|
||||||
return RealmConfiguration.Builder()
|
return RealmConfiguration.Builder()
|
||||||
.directory(directory)
|
.directory(directory)
|
||||||
|
@ -66,6 +68,8 @@ internal abstract class IdentityModule {
|
||||||
.apply {
|
.apply {
|
||||||
realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5))
|
realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5))
|
||||||
}
|
}
|
||||||
|
.schemaVersion(RealmIdentityStoreMigration.IDENTITY_STORE_SCHEMA_VERSION)
|
||||||
|
.migration(migration)
|
||||||
.allowWritesOnUiThread(true)
|
.allowWritesOnUiThread(true)
|
||||||
.modules(IdentityRealmModule())
|
.modules(IdentityRealmModule())
|
||||||
.build()
|
.build()
|
||||||
|
|
|
@ -20,5 +20,6 @@ internal data class IdentityData(
|
||||||
val identityServerUrl: String?,
|
val identityServerUrl: String?,
|
||||||
val token: String?,
|
val token: String?,
|
||||||
val hashLookupPepper: String?,
|
val hashLookupPepper: String?,
|
||||||
val hashLookupAlgorithm: List<String>
|
val hashLookupAlgorithm: List<String>,
|
||||||
|
val userConsent: Boolean
|
||||||
)
|
)
|
||||||
|
|
|
@ -27,6 +27,8 @@ internal interface IdentityStore {
|
||||||
|
|
||||||
fun setToken(token: String?)
|
fun setToken(token: String?)
|
||||||
|
|
||||||
|
fun setUserConsent(consent: Boolean)
|
||||||
|
|
||||||
fun setHashDetails(hashDetailResponse: IdentityHashDetailResponse)
|
fun setHashDetails(hashDetailResponse: IdentityHashDetailResponse)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -23,7 +23,8 @@ internal open class IdentityDataEntity(
|
||||||
var identityServerUrl: String? = null,
|
var identityServerUrl: String? = null,
|
||||||
var token: String? = null,
|
var token: String? = null,
|
||||||
var hashLookupPepper: String? = null,
|
var hashLookupPepper: String? = null,
|
||||||
var hashLookupAlgorithm: RealmList<String> = RealmList()
|
var hashLookupAlgorithm: RealmList<String> = RealmList(),
|
||||||
|
var userConsent: Boolean = false
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
||||||
companion object
|
companion object
|
||||||
|
|
|
@ -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>) {
|
||||||
|
|
|
@ -26,7 +26,8 @@ internal object IdentityMapper {
|
||||||
identityServerUrl = entity.identityServerUrl,
|
identityServerUrl = entity.identityServerUrl,
|
||||||
token = entity.token,
|
token = entity.token,
|
||||||
hashLookupPepper = entity.hashLookupPepper,
|
hashLookupPepper = entity.hashLookupPepper,
|
||||||
hashLookupAlgorithm = entity.hashLookupAlgorithm.toList()
|
hashLookupAlgorithm = entity.hashLookupAlgorithm.toList(),
|
||||||
|
userConsent = entity.userConsent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,14 @@ internal class RealmIdentityStore @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setUserConsent(consent: Boolean) {
|
||||||
|
Realm.getInstance(realmConfiguration).use {
|
||||||
|
it.executeTransaction { realm ->
|
||||||
|
IdentityDataEntity.setUserConsent(realm, consent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun setHashDetails(hashDetailResponse: IdentityHashDetailResponse) {
|
override fun setHashDetails(hashDetailResponse: IdentityHashDetailResponse) {
|
||||||
Realm.getInstance(realmConfiguration).use {
|
Realm.getInstance(realmConfiguration).use {
|
||||||
it.executeTransaction { realm ->
|
it.executeTransaction { realm ->
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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 org.matrix.android.sdk.internal.session.identity.db
|
||||||
|
|
||||||
|
import io.realm.DynamicRealm
|
||||||
|
import io.realm.RealmMigration
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class RealmIdentityStoreMigration @Inject constructor() : RealmMigration {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val IDENTITY_STORE_SCHEMA_VERSION = 1L
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||||
|
Timber.v("Migrating Realm Identity from $oldVersion to $newVersion")
|
||||||
|
|
||||||
|
if (oldVersion <= 0) migrateTo1(realm)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun migrateTo1(realm: DynamicRealm) {
|
||||||
|
Timber.d("Step 0 -> 1")
|
||||||
|
Timber.d("Add field userConsent (Boolean) and set the value to false")
|
||||||
|
|
||||||
|
realm.schema.get("IdentityDataEntity")
|
||||||
|
?.addField(IdentityDataEntityFields.USER_CONSENT, Boolean::class.java)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,4 +21,5 @@ import im.vector.app.core.platform.VectorViewModelAction
|
||||||
sealed class ContactsBookAction : VectorViewModelAction {
|
sealed class ContactsBookAction : VectorViewModelAction {
|
||||||
data class FilterWith(val filter: String) : ContactsBookAction()
|
data class FilterWith(val filter: String) : ContactsBookAction()
|
||||||
data class OnlyBoundContacts(val onlyBoundContacts: Boolean) : ContactsBookAction()
|
data class OnlyBoundContacts(val onlyBoundContacts: Boolean) : ContactsBookAction()
|
||||||
|
object UserConsentGranted : ContactsBookAction()
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,11 +52,10 @@ class ContactsBookController @Inject constructor(
|
||||||
|
|
||||||
override fun buildModels() {
|
override fun buildModels() {
|
||||||
val currentState = state ?: return
|
val currentState = state ?: return
|
||||||
val hasSearch = currentState.searchTerm.isNotEmpty()
|
|
||||||
when (val asyncMappedContacts = currentState.mappedContacts) {
|
when (val asyncMappedContacts = currentState.mappedContacts) {
|
||||||
is Uninitialized -> renderEmptyState(false)
|
is Uninitialized -> renderEmptyState(false)
|
||||||
is Loading -> renderLoading()
|
is Loading -> renderLoading()
|
||||||
is Success -> renderSuccess(currentState.filteredMappedContacts, hasSearch, currentState.onlyBoundContacts)
|
is Success -> renderSuccess(currentState)
|
||||||
is Fail -> renderFailure(asyncMappedContacts.error)
|
is Fail -> renderFailure(asyncMappedContacts.error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,13 +74,13 @@ class ContactsBookController @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderSuccess(mappedContacts: List<MappedContact>,
|
private fun renderSuccess(state: ContactsBookViewState) {
|
||||||
hasSearch: Boolean,
|
val mappedContacts = state.filteredMappedContacts
|
||||||
onlyBoundContacts: Boolean) {
|
|
||||||
if (mappedContacts.isEmpty()) {
|
if (mappedContacts.isEmpty()) {
|
||||||
renderEmptyState(hasSearch)
|
renderEmptyState(state.searchTerm.isNotEmpty())
|
||||||
} else {
|
} else {
|
||||||
renderContacts(mappedContacts, onlyBoundContacts)
|
renderContacts(mappedContacts, state.onlyBoundContacts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ package im.vector.app.features.contactsbook
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.airbnb.mvrx.activityViewModel
|
import com.airbnb.mvrx.activityViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
|
@ -57,10 +58,26 @@ class ContactsBookFragment @Inject constructor(
|
||||||
sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
|
sharedActionViewModel = activityViewModelProvider.get(UserDirectorySharedActionViewModel::class.java)
|
||||||
setupRecyclerView()
|
setupRecyclerView()
|
||||||
setupFilterView()
|
setupFilterView()
|
||||||
|
setupConsentView()
|
||||||
setupOnlyBoundContactsView()
|
setupOnlyBoundContactsView()
|
||||||
setupCloseView()
|
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() {
|
private fun setupOnlyBoundContactsView() {
|
||||||
phoneBookOnlyBoundContacts.checkedChanges()
|
phoneBookOnlyBoundContacts.checkedChanges()
|
||||||
.subscribe {
|
.subscribe {
|
||||||
|
@ -98,6 +115,7 @@ class ContactsBookFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun invalidate() = withState(contactsBookViewModel) { state ->
|
override fun invalidate() = withState(contactsBookViewModel) { state ->
|
||||||
|
phoneBookSearchForMatrixContacts.isVisible = state.filteredMappedContacts.isNotEmpty() && state.identityServerUrl != null && !state.userConsent
|
||||||
phoneBookOnlyBoundContacts.isVisible = state.isBoundRetrieved
|
phoneBookOnlyBoundContacts.isVisible = state.isBoundRetrieved
|
||||||
contactsBookController.setData(state)
|
contactsBookController.setData(state)
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,11 +38,10 @@ import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
import org.matrix.android.sdk.api.session.Session
|
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.FoundThreePid
|
||||||
|
import org.matrix.android.sdk.api.session.identity.IdentityServiceError
|
||||||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
private typealias PhoneBookSearch = String
|
|
||||||
|
|
||||||
class ContactsBookViewModel @AssistedInject constructor(@Assisted
|
class ContactsBookViewModel @AssistedInject constructor(@Assisted
|
||||||
initialState: ContactsBookViewState,
|
initialState: ContactsBookViewState,
|
||||||
private val contactsDataSource: ContactsDataSource,
|
private val contactsDataSource: ContactsDataSource,
|
||||||
|
@ -85,7 +84,9 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted
|
||||||
private fun loadContacts() {
|
private fun loadContacts() {
|
||||||
setState {
|
setState {
|
||||||
copy(
|
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>) {
|
private fun performLookup(data: List<MappedContact>) {
|
||||||
|
if (!session.identityService().getUserConsent()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val threePids = data.flatMap { contact ->
|
val threePids = data.flatMap { contact ->
|
||||||
contact.emails.map { ThreePid.Email(it.email) } +
|
contact.emails.map { ThreePid.Email(it.email) } +
|
||||||
|
@ -116,8 +120,14 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted
|
||||||
}
|
}
|
||||||
session.identityService().lookUp(threePids, object : MatrixCallback<List<FoundThreePid>> {
|
session.identityService().lookUp(threePids, object : MatrixCallback<List<FoundThreePid>> {
|
||||||
override fun onFailure(failure: Throwable) {
|
override fun onFailure(failure: Throwable) {
|
||||||
// Ignore
|
|
||||||
Timber.w(failure, "Unable to perform the lookup")
|
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>) {
|
override fun onSuccess(data: List<FoundThreePid>) {
|
||||||
|
@ -171,9 +181,21 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted
|
||||||
when (action) {
|
when (action) {
|
||||||
is ContactsBookAction.FilterWith -> handleFilterWith(action)
|
is ContactsBookAction.FilterWith -> handleFilterWith(action)
|
||||||
is ContactsBookAction.OnlyBoundContacts -> handleOnlyBoundContacts(action)
|
is ContactsBookAction.OnlyBoundContacts -> handleOnlyBoundContacts(action)
|
||||||
|
ContactsBookAction.UserConsentGranted -> handleUserConsentGranted()
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleUserConsentGranted() {
|
||||||
|
session.identityService().setUserConsent(true)
|
||||||
|
|
||||||
|
setState {
|
||||||
|
copy(userConsent = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the lookup
|
||||||
|
performLookup(allContacts)
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleOnlyBoundContacts(action: ContactsBookAction.OnlyBoundContacts) {
|
private fun handleOnlyBoundContacts(action: ContactsBookAction.OnlyBoundContacts) {
|
||||||
setState {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
|
|
|
@ -26,10 +26,14 @@ data class ContactsBookViewState(
|
||||||
val mappedContacts: Async<List<MappedContact>> = Loading(),
|
val mappedContacts: Async<List<MappedContact>> = Loading(),
|
||||||
// Use to filter contacts by display name
|
// Use to filter contacts by display name
|
||||||
val searchTerm: String = "",
|
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,
|
val onlyBoundContacts: Boolean = false,
|
||||||
// All contacts, filtered by searchTerm and onlyBoundContacts
|
// All contacts, filtered by searchTerm and onlyBoundContacts
|
||||||
val filteredMappedContacts: List<MappedContact> = emptyList(),
|
val filteredMappedContacts: List<MappedContact> = emptyList(),
|
||||||
// True when the identity service has return some data
|
// 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
|
) : MvRxState
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)) }
|
||||||
|
|
|
@ -93,6 +93,27 @@
|
||||||
app:layout_constraintTop_toBottomOf="@+id/phoneBookFilterContainer"
|
app:layout_constraintTop_toBottomOf="@+id/phoneBookFilterContainer"
|
||||||
tools:visibility="visible" />
|
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
|
<View
|
||||||
android:id="@+id/phoneBookFilterDivider"
|
android:id="@+id/phoneBookFilterDivider"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
@ -101,7 +122,7 @@
|
||||||
android:background="?attr/vctr_list_divider_color"
|
android:background="?attr/vctr_list_divider_color"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/phoneBookOnlyBoundContacts" />
|
app:layout_constraintTop_toBottomOf="@+id/phoneBookBottomBarrier" />
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/phoneBookRecyclerView"
|
android:id="@+id/phoneBookRecyclerView"
|
||||||
|
|
|
@ -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>
|
||||||
|
@ -2593,6 +2602,7 @@
|
||||||
<string name="loading_contact_book">Retrieving your contacts…</string>
|
<string name="loading_contact_book">Retrieving your contacts…</string>
|
||||||
<string name="empty_contact_book">Your contact book is empty</string>
|
<string name="empty_contact_book">Your contact book is empty</string>
|
||||||
<string name="contacts_book_title">Contacts book</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_title">Revoke invite</string>
|
||||||
<string name="three_pid_revoke_invite_dialog_content">Revoke invite to %1$s?</string>
|
<string name="three_pid_revoke_invite_dialog_content">Revoke invite to %1$s?</string>
|
||||||
|
|
Loading…
Add table
Reference in a new issue