Merge branch 'develop' into feature/bca/rust_flavor
This commit is contained in:
commit
3db82e629b
|
@ -0,0 +1 @@
|
||||||
|
Fix issue of Scan QR code button sometimes not showing when it should be available
|
|
@ -0,0 +1 @@
|
||||||
|
Handle account data removal
|
|
@ -0,0 +1 @@
|
||||||
|
Delete unused client information from account data
|
|
@ -0,0 +1 @@
|
||||||
|
[Push Notifications] When push notification for threaded message is clicked, thread timeline will be opened instead of room's main timeline
|
|
@ -125,12 +125,6 @@ interface AuthenticationService {
|
||||||
deviceId: String? = null
|
deviceId: String? = null
|
||||||
): Session
|
): Session
|
||||||
|
|
||||||
/**
|
|
||||||
* @param homeServerConnectionConfig the information about the homeserver and other configuration
|
|
||||||
* Return true if qr code login is supported by the server, false otherwise.
|
|
||||||
*/
|
|
||||||
suspend fun isQrLoginSupported(homeServerConnectionConfig: HomeServerConnectionConfig): Boolean
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authenticate using m.login.token method during sign in with QR code.
|
* Authenticate using m.login.token method during sign in with QR code.
|
||||||
* @param homeServerConnectionConfig the information about the homeserver and other configuration
|
* @param homeServerConnectionConfig the information about the homeserver and other configuration
|
||||||
|
|
|
@ -22,5 +22,6 @@ data class LoginFlowResult(
|
||||||
val isLoginAndRegistrationSupported: Boolean,
|
val isLoginAndRegistrationSupported: Boolean,
|
||||||
val homeServerUrl: String,
|
val homeServerUrl: String,
|
||||||
val isOutdatedHomeserver: Boolean,
|
val isOutdatedHomeserver: Boolean,
|
||||||
val isLogoutDevicesSupported: Boolean
|
val isLogoutDevicesSupported: Boolean,
|
||||||
|
val isLoginWithQrSupported: Boolean,
|
||||||
)
|
)
|
||||||
|
|
|
@ -63,4 +63,17 @@ interface SessionAccountDataService {
|
||||||
* Update the account data with the provided type and the provided account data content.
|
* Update the account data with the provided type and the provided account data content.
|
||||||
*/
|
*/
|
||||||
suspend fun updateUserAccountData(type: String, content: Content)
|
suspend fun updateUserAccountData(type: String, content: Content)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve user account data list whose type starts with the given type.
|
||||||
|
* @param type the type or the starting part of a type
|
||||||
|
* @return list of account data whose type starts with the given type
|
||||||
|
*/
|
||||||
|
fun getUserAccountDataEventsStartWith(type: String): List<UserAccountDataEvent>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes user account data of the given type.
|
||||||
|
* @param type the type to delete from user account data
|
||||||
|
*/
|
||||||
|
suspend fun deleteUserAccountData(type: String)
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,6 @@ import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
|
||||||
import org.matrix.android.sdk.api.auth.login.LoginWizard
|
import org.matrix.android.sdk.api.auth.login.LoginWizard
|
||||||
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
|
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
|
||||||
import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
|
import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
|
||||||
import org.matrix.android.sdk.api.failure.Failure
|
import org.matrix.android.sdk.api.failure.Failure
|
||||||
import org.matrix.android.sdk.api.failure.MatrixIdFailure
|
import org.matrix.android.sdk.api.failure.MatrixIdFailure
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
@ -299,7 +298,8 @@ internal class DefaultAuthenticationService @Inject constructor(
|
||||||
isLoginAndRegistrationSupported = versions.isLoginAndRegistrationSupportedBySdk(),
|
isLoginAndRegistrationSupported = versions.isLoginAndRegistrationSupportedBySdk(),
|
||||||
homeServerUrl = homeServerUrl,
|
homeServerUrl = homeServerUrl,
|
||||||
isOutdatedHomeserver = !versions.isSupportedBySdk(),
|
isOutdatedHomeserver = !versions.isSupportedBySdk(),
|
||||||
isLogoutDevicesSupported = versions.doesServerSupportLogoutDevices()
|
isLogoutDevicesSupported = versions.doesServerSupportLogoutDevices(),
|
||||||
|
isLoginWithQrSupported = versions.doesServerSupportQrCodeLogin(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -408,20 +408,6 @@ internal class DefaultAuthenticationService @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun isQrLoginSupported(homeServerConnectionConfig: HomeServerConnectionConfig): Boolean {
|
|
||||||
val authAPI = buildAuthAPI(homeServerConnectionConfig)
|
|
||||||
val versions = runCatching {
|
|
||||||
executeRequest(null) {
|
|
||||||
authAPI.versions()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return if (versions.isSuccess) {
|
|
||||||
versions.getOrNull()?.doesServerSupportQrCodeLogin().orFalse()
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun loginUsingQrLoginToken(
|
override suspend fun loginUsingQrLoginToken(
|
||||||
homeServerConnectionConfig: HomeServerConnectionConfig,
|
homeServerConnectionConfig: HomeServerConnectionConfig,
|
||||||
loginToken: String,
|
loginToken: String,
|
||||||
|
|
|
@ -21,6 +21,7 @@ import io.realm.RealmQuery
|
||||||
import io.realm.kotlin.where
|
import io.realm.kotlin.where
|
||||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||||
import org.matrix.android.sdk.internal.database.model.EventEntity
|
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntityFields
|
||||||
import org.matrix.android.sdk.internal.database.model.RoomEntity
|
import org.matrix.android.sdk.internal.database.model.RoomEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.RoomEntityFields
|
import org.matrix.android.sdk.internal.database.model.RoomEntityFields
|
||||||
|
|
||||||
|
@ -44,3 +45,11 @@ internal fun RoomEntity.Companion.where(realm: Realm, membership: Membership? =
|
||||||
internal fun RoomEntity.fastContains(eventId: String): Boolean {
|
internal fun RoomEntity.fastContains(eventId: String): Boolean {
|
||||||
return EventEntity.where(realm, eventId = eventId).findFirst() != null
|
return EventEntity.where(realm, eventId = eventId).findFirst() != null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun RoomEntity.removeAccountData(type: String) {
|
||||||
|
accountData
|
||||||
|
.where()
|
||||||
|
.equalTo(RoomAccountDataEntityFields.TYPE, type)
|
||||||
|
.findFirst()
|
||||||
|
?.deleteFromRealm()
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 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.database.query
|
||||||
|
|
||||||
|
import io.realm.Realm
|
||||||
|
import io.realm.kotlin.where
|
||||||
|
import org.matrix.android.sdk.internal.database.model.UserAccountDataEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.UserAccountDataEntityFields
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an account_data event.
|
||||||
|
*/
|
||||||
|
internal fun UserAccountDataEntity.Companion.delete(realm: Realm, type: String) {
|
||||||
|
realm
|
||||||
|
.where<UserAccountDataEntity>()
|
||||||
|
.equalTo(UserAccountDataEntityFields.TYPE, type)
|
||||||
|
.findFirst()
|
||||||
|
?.deleteFromRealm()
|
||||||
|
}
|
|
@ -427,6 +427,19 @@ internal interface RoomAPI {
|
||||||
@Body content: JsonDict
|
@Body content: JsonDict
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an account_data event from the room.
|
||||||
|
* @param userId the user id
|
||||||
|
* @param roomId the room id
|
||||||
|
* @param type the type
|
||||||
|
*/
|
||||||
|
@DELETE(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc3391/user/{userId}/rooms/{roomId}/account_data/{type}")
|
||||||
|
suspend fun deleteRoomAccountData(
|
||||||
|
@Path("userId") userId: String,
|
||||||
|
@Path("roomId") roomId: String,
|
||||||
|
@Path("type") type: String
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upgrades the given room to a particular room version.
|
* Upgrades the given room to a particular room version.
|
||||||
* Errors:
|
* Errors:
|
||||||
|
|
|
@ -45,6 +45,7 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.UserAccountDataEntity
|
import org.matrix.android.sdk.internal.database.model.UserAccountDataEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.UserAccountDataEntityFields
|
import org.matrix.android.sdk.internal.database.model.UserAccountDataEntityFields
|
||||||
import org.matrix.android.sdk.internal.database.model.deleteOnCascade
|
import org.matrix.android.sdk.internal.database.model.deleteOnCascade
|
||||||
|
import org.matrix.android.sdk.internal.database.query.delete
|
||||||
import org.matrix.android.sdk.internal.database.query.findAllFrom
|
import org.matrix.android.sdk.internal.database.query.findAllFrom
|
||||||
import org.matrix.android.sdk.internal.database.query.getDirectRooms
|
import org.matrix.android.sdk.internal.database.query.getDirectRooms
|
||||||
import org.matrix.android.sdk.internal.database.query.getOrCreate
|
import org.matrix.android.sdk.internal.database.query.getOrCreate
|
||||||
|
@ -94,7 +95,7 @@ internal class UserAccountDataSyncHandler @Inject constructor(
|
||||||
|
|
||||||
// If we get some direct chat invites, we synchronize the user account data including those.
|
// If we get some direct chat invites, we synchronize the user account data including those.
|
||||||
suspend fun synchronizeWithServerIfNeeded(invites: Map<String, InvitedRoomSync>) {
|
suspend fun synchronizeWithServerIfNeeded(invites: Map<String, InvitedRoomSync>) {
|
||||||
if (invites.isNullOrEmpty()) return
|
if (invites.isEmpty()) return
|
||||||
val directChats = directChatsHelper.getLocalDirectMessages().toMutable()
|
val directChats = directChatsHelper.getLocalDirectMessages().toMutable()
|
||||||
var hasUpdate = false
|
var hasUpdate = false
|
||||||
monarchy.doWithRealm { realm ->
|
monarchy.doWithRealm { realm ->
|
||||||
|
@ -252,9 +253,17 @@ internal class UserAccountDataSyncHandler @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleGenericAccountData(realm: Realm, type: String, content: Content?) {
|
fun handleGenericAccountData(realm: Realm, type: String, content: Content?) {
|
||||||
|
if (content.isNullOrEmpty()) {
|
||||||
|
// This is a response for a deleted account data according to
|
||||||
|
// https://github.com/ShadowJonathan/matrix-doc/blob/account-data-delete/proposals/3391-account-data-delete.md#sync
|
||||||
|
UserAccountDataEntity.delete(realm, type)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val existing = realm.where<UserAccountDataEntity>()
|
val existing = realm.where<UserAccountDataEntity>()
|
||||||
.equalTo(UserAccountDataEntityFields.TYPE, type)
|
.equalTo(UserAccountDataEntityFields.TYPE, type)
|
||||||
.findFirst()
|
.findFirst()
|
||||||
|
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
// Update current value
|
// Update current value
|
||||||
existing.contentStr = ContentMapper.map(content)
|
existing.contentStr = ContentMapper.map(content)
|
||||||
|
|
|
@ -27,6 +27,7 @@ import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntityFields
|
import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntityFields
|
||||||
import org.matrix.android.sdk.internal.database.model.RoomEntity
|
import org.matrix.android.sdk.internal.database.model.RoomEntity
|
||||||
import org.matrix.android.sdk.internal.database.query.getOrCreate
|
import org.matrix.android.sdk.internal.database.query.getOrCreate
|
||||||
|
import org.matrix.android.sdk.internal.database.query.removeAccountData
|
||||||
import org.matrix.android.sdk.internal.session.room.read.FullyReadContent
|
import org.matrix.android.sdk.internal.session.room.read.FullyReadContent
|
||||||
import org.matrix.android.sdk.internal.session.sync.handler.room.RoomFullyReadHandler
|
import org.matrix.android.sdk.internal.session.sync.handler.room.RoomFullyReadHandler
|
||||||
import org.matrix.android.sdk.internal.session.sync.handler.room.RoomTagHandler
|
import org.matrix.android.sdk.internal.session.sync.handler.room.RoomTagHandler
|
||||||
|
@ -56,6 +57,13 @@ internal class RoomSyncAccountDataHandler @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleGeneric(roomEntity: RoomEntity, content: JsonDict?, eventType: String) {
|
private fun handleGeneric(roomEntity: RoomEntity, content: JsonDict?, eventType: String) {
|
||||||
|
if (content.isNullOrEmpty()) {
|
||||||
|
// This is a response for a deleted account data according to
|
||||||
|
// https://github.com/ShadowJonathan/matrix-doc/blob/account-data-delete/proposals/3391-account-data-delete.md#sync
|
||||||
|
roomEntity.removeAccountData(eventType)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val existing = roomEntity.accountData.where().equalTo(RoomAccountDataEntityFields.TYPE, eventType).findFirst()
|
val existing = roomEntity.accountData.where().equalTo(RoomAccountDataEntityFields.TYPE, eventType).findFirst()
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
existing.contentStr = ContentMapper.map(content)
|
existing.contentStr = ContentMapper.map(content)
|
||||||
|
|
|
@ -18,13 +18,14 @@ package org.matrix.android.sdk.internal.session.user.accountdata
|
||||||
|
|
||||||
import org.matrix.android.sdk.internal.network.NetworkConstants
|
import org.matrix.android.sdk.internal.network.NetworkConstants
|
||||||
import retrofit2.http.Body
|
import retrofit2.http.Body
|
||||||
|
import retrofit2.http.DELETE
|
||||||
import retrofit2.http.PUT
|
import retrofit2.http.PUT
|
||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
|
|
||||||
internal interface AccountDataAPI {
|
internal interface AccountDataAPI {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set some account_data for the client.
|
* Set some account_data for the user.
|
||||||
*
|
*
|
||||||
* @param userId the user id
|
* @param userId the user id
|
||||||
* @param type the type
|
* @param type the type
|
||||||
|
@ -36,4 +37,16 @@ internal interface AccountDataAPI {
|
||||||
@Path("type") type: String,
|
@Path("type") type: String,
|
||||||
@Body params: Any
|
@Body params: Any
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an account_data for the user.
|
||||||
|
*
|
||||||
|
* @param userId the user id
|
||||||
|
* @param type the type
|
||||||
|
*/
|
||||||
|
@DELETE(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc3391/user/{userId}/account_data/{type}")
|
||||||
|
suspend fun deleteAccountData(
|
||||||
|
@Path("userId") userId: String,
|
||||||
|
@Path("type") type: String
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,4 +42,7 @@ internal abstract class AccountDataModule {
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindUpdateBreadcrumbsTask(task: DefaultUpdateBreadcrumbsTask): UpdateBreadcrumbsTask
|
abstract fun bindUpdateBreadcrumbsTask(task: DefaultUpdateBreadcrumbsTask): UpdateBreadcrumbsTask
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindDeleteUserAccountDataTask(task: DefaultDeleteUserAccountDataTask): DeleteUserAccountDataTask
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,10 +34,11 @@ import javax.inject.Inject
|
||||||
internal class DefaultSessionAccountDataService @Inject constructor(
|
internal class DefaultSessionAccountDataService @Inject constructor(
|
||||||
@SessionDatabase private val monarchy: Monarchy,
|
@SessionDatabase private val monarchy: Monarchy,
|
||||||
private val updateUserAccountDataTask: UpdateUserAccountDataTask,
|
private val updateUserAccountDataTask: UpdateUserAccountDataTask,
|
||||||
|
private val deleteUserAccountDataTask: DeleteUserAccountDataTask,
|
||||||
private val userAccountDataSyncHandler: UserAccountDataSyncHandler,
|
private val userAccountDataSyncHandler: UserAccountDataSyncHandler,
|
||||||
private val userAccountDataDataSource: UserAccountDataDataSource,
|
private val userAccountDataDataSource: UserAccountDataDataSource,
|
||||||
private val roomAccountDataDataSource: RoomAccountDataDataSource,
|
private val roomAccountDataDataSource: RoomAccountDataDataSource,
|
||||||
private val taskExecutor: TaskExecutor
|
private val taskExecutor: TaskExecutor,
|
||||||
) : SessionAccountDataService {
|
) : SessionAccountDataService {
|
||||||
|
|
||||||
override fun getUserAccountDataEvent(type: String): UserAccountDataEvent? {
|
override fun getUserAccountDataEvent(type: String): UserAccountDataEvent? {
|
||||||
|
@ -78,4 +79,12 @@ internal class DefaultSessionAccountDataService @Inject constructor(
|
||||||
userAccountDataSyncHandler.handleGenericAccountData(realm, type, content)
|
userAccountDataSyncHandler.handleGenericAccountData(realm, type, content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getUserAccountDataEventsStartWith(type: String): List<UserAccountDataEvent> {
|
||||||
|
return userAccountDataDataSource.getAccountDataEventsStartWith(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteUserAccountData(type: String) {
|
||||||
|
deleteUserAccountDataTask.execute(DeleteUserAccountDataTask.Params(type))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 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.user.accountdata
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.internal.di.UserId
|
||||||
|
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
||||||
|
import org.matrix.android.sdk.internal.network.executeRequest
|
||||||
|
import org.matrix.android.sdk.internal.task.Task
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal interface DeleteUserAccountDataTask : Task<DeleteUserAccountDataTask.Params, Unit> {
|
||||||
|
|
||||||
|
data class Params(
|
||||||
|
val type: String,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DefaultDeleteUserAccountDataTask @Inject constructor(
|
||||||
|
private val accountDataApi: AccountDataAPI,
|
||||||
|
@UserId private val userId: String,
|
||||||
|
private val globalErrorReceiver: GlobalErrorReceiver,
|
||||||
|
) : DeleteUserAccountDataTask {
|
||||||
|
|
||||||
|
override suspend fun execute(params: DeleteUserAccountDataTask.Params) {
|
||||||
|
return executeRequest(globalErrorReceiver) {
|
||||||
|
accountDataApi.deleteAccountData(userId, params.type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -60,6 +60,16 @@ internal class UserAccountDataDataSource @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getAccountDataEventsStartWith(type: String): List<UserAccountDataEvent> {
|
||||||
|
return realmSessionProvider.withRealm { realm ->
|
||||||
|
realm
|
||||||
|
.where(UserAccountDataEntity::class.java)
|
||||||
|
.beginsWith(UserAccountDataEntityFields.TYPE, type)
|
||||||
|
.findAll()
|
||||||
|
.map(accountDataMapper::map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun accountDataEventsQuery(realm: Realm, types: Set<String>): RealmQuery<UserAccountDataEntity> {
|
private fun accountDataEventsQuery(realm: Realm, types: Set<String>): RealmQuery<UserAccountDataEntity> {
|
||||||
val query = realm.where(UserAccountDataEntity::class.java)
|
val query = realm.where(UserAccountDataEntity::class.java)
|
||||||
if (types.isNotEmpty()) {
|
if (types.isNotEmpty()) {
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 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.user.accountdata
|
||||||
|
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
import org.matrix.android.sdk.test.fakes.FakeAccountDataApi
|
||||||
|
import org.matrix.android.sdk.test.fakes.FakeGlobalErrorReceiver
|
||||||
|
|
||||||
|
private const val A_TYPE = "a-type"
|
||||||
|
private const val A_USER_ID = "a-user-id"
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
class DefaultDeleteUserAccountDataTaskTest {
|
||||||
|
|
||||||
|
private val fakeGlobalErrorReceiver = FakeGlobalErrorReceiver()
|
||||||
|
private val fakeAccountDataApi = FakeAccountDataApi()
|
||||||
|
|
||||||
|
private val deleteUserAccountDataTask = DefaultDeleteUserAccountDataTask(
|
||||||
|
accountDataApi = fakeAccountDataApi.instance,
|
||||||
|
userId = A_USER_ID,
|
||||||
|
globalErrorReceiver = fakeGlobalErrorReceiver
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given parameters when executing the task then api is called`() = runTest {
|
||||||
|
// Given
|
||||||
|
val params = DeleteUserAccountDataTask.Params(type = A_TYPE)
|
||||||
|
fakeAccountDataApi.givenParamsToDeleteAccountData(A_USER_ID, A_TYPE)
|
||||||
|
|
||||||
|
// When
|
||||||
|
deleteUserAccountDataTask.execute(params)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
coVerify { fakeAccountDataApi.instance.deleteAccountData(A_USER_ID, A_TYPE) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 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.test.fakes
|
||||||
|
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.just
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.runs
|
||||||
|
import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataAPI
|
||||||
|
|
||||||
|
internal class FakeAccountDataApi {
|
||||||
|
|
||||||
|
val instance: AccountDataAPI = mockk()
|
||||||
|
|
||||||
|
fun givenParamsToDeleteAccountData(userId: String, type: String) {
|
||||||
|
coEvery { instance.deleteAccountData(userId, type) } just runs
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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.core.session.clientinfo
|
||||||
|
|
||||||
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class DeleteUnusedClientInformationUseCase @Inject constructor(
|
||||||
|
private val activeSessionHolder: ActiveSessionHolder,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun execute(deviceInfoList: List<DeviceInfo>): Result<Unit> = runCatching {
|
||||||
|
// A defensive approach against local storage reports an empty device list (although it is not a seen situation).
|
||||||
|
if (deviceInfoList.isEmpty()) return Result.success(Unit)
|
||||||
|
|
||||||
|
val expectedClientInfoKeyList = deviceInfoList.map { MATRIX_CLIENT_INFO_KEY_PREFIX + it.deviceId }
|
||||||
|
activeSessionHolder
|
||||||
|
.getSafeActiveSession()
|
||||||
|
?.accountDataService()
|
||||||
|
?.getUserAccountDataEventsStartWith(MATRIX_CLIENT_INFO_KEY_PREFIX)
|
||||||
|
?.map { it.type }
|
||||||
|
?.subtract(expectedClientInfoKeyList.toSet())
|
||||||
|
?.forEach { userAccountDataKeyToDelete ->
|
||||||
|
activeSessionHolder.getSafeActiveSession()?.accountDataService()?.deleteUserAccountData(userAccountDataKeyToDelete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,10 +31,10 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||||
import im.vector.app.core.platform.EmptyViewEvents
|
import im.vector.app.core.platform.EmptyViewEvents
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
import im.vector.app.core.platform.VectorViewModelAction
|
import im.vector.app.core.platform.VectorViewModelAction
|
||||||
|
import im.vector.app.core.session.clientinfo.DeleteUnusedClientInformationUseCase
|
||||||
import im.vector.app.core.time.Clock
|
import im.vector.app.core.time.Clock
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.flow
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.sample
|
import kotlinx.coroutines.flow.sample
|
||||||
|
@ -67,6 +67,7 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(
|
||||||
private val setUnverifiedSessionsAlertShownUseCase: SetUnverifiedSessionsAlertShownUseCase,
|
private val setUnverifiedSessionsAlertShownUseCase: SetUnverifiedSessionsAlertShownUseCase,
|
||||||
private val isNewLoginAlertShownUseCase: IsNewLoginAlertShownUseCase,
|
private val isNewLoginAlertShownUseCase: IsNewLoginAlertShownUseCase,
|
||||||
private val setNewLoginAlertShownUseCase: SetNewLoginAlertShownUseCase,
|
private val setNewLoginAlertShownUseCase: SetNewLoginAlertShownUseCase,
|
||||||
|
private val deleteUnusedClientInformationUseCase: DeleteUnusedClientInformationUseCase,
|
||||||
) : VectorViewModel<UnknownDevicesState, UnknownDeviceDetectorSharedViewModel.Action, EmptyViewEvents>(initialState) {
|
) : VectorViewModel<UnknownDevicesState, UnknownDeviceDetectorSharedViewModel.Action, EmptyViewEvents>(initialState) {
|
||||||
|
|
||||||
sealed class Action : VectorViewModelAction {
|
sealed class Action : VectorViewModelAction {
|
||||||
|
@ -103,6 +104,9 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(
|
||||||
) { cryptoList, infoList, pInfo ->
|
) { cryptoList, infoList, pInfo ->
|
||||||
// Timber.v("## Detector trigger ${cryptoList.map { "${it.deviceId} ${it.trustLevel}" }}")
|
// Timber.v("## Detector trigger ${cryptoList.map { "${it.deviceId} ${it.trustLevel}" }}")
|
||||||
// Timber.v("## Detector trigger canCrossSign ${pInfo.get().selfSigned != null}")
|
// Timber.v("## Detector trigger canCrossSign ${pInfo.get().selfSigned != null}")
|
||||||
|
|
||||||
|
deleteUnusedClientInformation(infoList)
|
||||||
|
|
||||||
infoList
|
infoList
|
||||||
.filter { info ->
|
.filter { info ->
|
||||||
// filter out verified sessions or those which do not support encryption (i.e. without crypto info)
|
// filter out verified sessions or those which do not support encryption (i.e. without crypto info)
|
||||||
|
@ -146,6 +150,12 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun deleteUnusedClientInformation(deviceFullInfoList: List<DeviceInfo>) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
deleteUnusedClientInformationUseCase.execute(deviceFullInfoList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun handle(action: Action) {
|
override fun handle(action: Action) {
|
||||||
when (action) {
|
when (action) {
|
||||||
is Action.IgnoreDevice -> {
|
is Action.IgnoreDevice -> {
|
||||||
|
|
|
@ -26,6 +26,7 @@ import im.vector.app.core.extensions.addFragmentToBackstack
|
||||||
import im.vector.app.core.extensions.replaceFragment
|
import im.vector.app.core.extensions.replaceFragment
|
||||||
import im.vector.app.core.platform.VectorBaseActivity
|
import im.vector.app.core.platform.VectorBaseActivity
|
||||||
import im.vector.app.databinding.ActivityThreadsBinding
|
import im.vector.app.databinding.ActivityThreadsBinding
|
||||||
|
import im.vector.app.features.MainActivity
|
||||||
import im.vector.app.features.analytics.extensions.toAnalyticsInteraction
|
import im.vector.app.features.analytics.extensions.toAnalyticsInteraction
|
||||||
import im.vector.app.features.analytics.plan.Interaction
|
import im.vector.app.features.analytics.plan.Interaction
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
import im.vector.app.features.home.AvatarRenderer
|
||||||
|
@ -143,13 +144,20 @@ class ThreadsActivity : VectorBaseActivity<ActivityThreadsBinding>() {
|
||||||
context: Context,
|
context: Context,
|
||||||
threadTimelineArgs: ThreadTimelineArgs?,
|
threadTimelineArgs: ThreadTimelineArgs?,
|
||||||
threadListArgs: ThreadListArgs?,
|
threadListArgs: ThreadListArgs?,
|
||||||
eventIdToNavigate: String? = null
|
eventIdToNavigate: String? = null,
|
||||||
|
firstStartMainActivity: Boolean = false
|
||||||
): Intent {
|
): Intent {
|
||||||
return Intent(context, ThreadsActivity::class.java).apply {
|
val intent = Intent(context, ThreadsActivity::class.java).apply {
|
||||||
putExtra(THREAD_TIMELINE_ARGS, threadTimelineArgs)
|
putExtra(THREAD_TIMELINE_ARGS, threadTimelineArgs)
|
||||||
putExtra(THREAD_EVENT_ID_TO_NAVIGATE, eventIdToNavigate)
|
putExtra(THREAD_EVENT_ID_TO_NAVIGATE, eventIdToNavigate)
|
||||||
putExtra(THREAD_LIST_ARGS, threadListArgs)
|
putExtra(THREAD_LIST_ARGS, threadListArgs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return if (firstStartMainActivity) {
|
||||||
|
MainActivity.getIntentWithNextIntent(context, intent)
|
||||||
|
} else {
|
||||||
|
intent
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -60,6 +60,8 @@ import im.vector.app.features.displayname.getBestName
|
||||||
import im.vector.app.features.home.HomeActivity
|
import im.vector.app.features.home.HomeActivity
|
||||||
import im.vector.app.features.home.room.detail.RoomDetailActivity
|
import im.vector.app.features.home.room.detail.RoomDetailActivity
|
||||||
import im.vector.app.features.home.room.detail.arguments.TimelineArgs
|
import im.vector.app.features.home.room.detail.arguments.TimelineArgs
|
||||||
|
import im.vector.app.features.home.room.threads.ThreadsActivity
|
||||||
|
import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
import im.vector.app.features.settings.troubleshoot.TestNotificationReceiver
|
import im.vector.app.features.settings.troubleshoot.TestNotificationReceiver
|
||||||
import im.vector.app.features.themes.ThemeUtils
|
import im.vector.app.features.themes.ThemeUtils
|
||||||
|
@ -574,6 +576,7 @@ class NotificationUtils @Inject constructor(
|
||||||
fun buildMessagesListNotification(
|
fun buildMessagesListNotification(
|
||||||
messageStyle: NotificationCompat.MessagingStyle,
|
messageStyle: NotificationCompat.MessagingStyle,
|
||||||
roomInfo: RoomEventGroupInfo,
|
roomInfo: RoomEventGroupInfo,
|
||||||
|
threadId: String?,
|
||||||
largeIcon: Bitmap?,
|
largeIcon: Bitmap?,
|
||||||
lastMessageTimestamp: Long,
|
lastMessageTimestamp: Long,
|
||||||
senderDisplayNameForReplyCompat: String?,
|
senderDisplayNameForReplyCompat: String?,
|
||||||
|
@ -581,7 +584,11 @@ class NotificationUtils @Inject constructor(
|
||||||
): Notification {
|
): Notification {
|
||||||
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
|
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
|
||||||
// Build the pending intent for when the notification is clicked
|
// Build the pending intent for when the notification is clicked
|
||||||
val openRoomIntent = buildOpenRoomIntent(roomInfo.roomId)
|
val openIntent = when {
|
||||||
|
threadId != null && vectorPreferences.areThreadMessagesEnabled() -> buildOpenThreadIntent(roomInfo, threadId)
|
||||||
|
else -> buildOpenRoomIntent(roomInfo.roomId)
|
||||||
|
}
|
||||||
|
|
||||||
val smallIcon = R.drawable.ic_notification
|
val smallIcon = R.drawable.ic_notification
|
||||||
|
|
||||||
val channelID = if (roomInfo.shouldBing) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID
|
val channelID = if (roomInfo.shouldBing) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID
|
||||||
|
@ -666,8 +673,8 @@ class NotificationUtils @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (openRoomIntent != null) {
|
if (openIntent != null) {
|
||||||
setContentIntent(openRoomIntent)
|
setContentIntent(openIntent)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (largeIcon != null) {
|
if (largeIcon != null) {
|
||||||
|
@ -826,6 +833,45 @@ class NotificationUtils @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildOpenThreadIntent(roomInfo: RoomEventGroupInfo, threadId: String?): PendingIntent? {
|
||||||
|
val threadTimelineArgs = ThreadTimelineArgs(
|
||||||
|
startsThread = false,
|
||||||
|
roomId = roomInfo.roomId,
|
||||||
|
rootThreadEventId = threadId,
|
||||||
|
showKeyboard = false,
|
||||||
|
displayName = roomInfo.roomDisplayName,
|
||||||
|
avatarUrl = null,
|
||||||
|
roomEncryptionTrustLevel = null,
|
||||||
|
)
|
||||||
|
val threadIntentTap = ThreadsActivity.newIntent(
|
||||||
|
context = context,
|
||||||
|
threadTimelineArgs = threadTimelineArgs,
|
||||||
|
threadListArgs = null,
|
||||||
|
firstStartMainActivity = true,
|
||||||
|
)
|
||||||
|
threadIntentTap.action = actionIds.tapToView
|
||||||
|
// pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that
|
||||||
|
threadIntentTap.data = createIgnoredUri("openThread?$threadId")
|
||||||
|
|
||||||
|
val roomIntent = RoomDetailActivity.newIntent(
|
||||||
|
context = context,
|
||||||
|
timelineArgs = TimelineArgs(
|
||||||
|
roomId = roomInfo.roomId,
|
||||||
|
switchToParentSpace = true
|
||||||
|
),
|
||||||
|
firstStartMainActivity = false
|
||||||
|
)
|
||||||
|
// Recreate the back stack
|
||||||
|
return TaskStackBuilder.create(context)
|
||||||
|
.addNextIntentWithParentStack(HomeActivity.newIntent(context, firstStartMainActivity = false))
|
||||||
|
.addNextIntentWithParentStack(roomIntent)
|
||||||
|
.addNextIntent(threadIntentTap)
|
||||||
|
.getPendingIntent(
|
||||||
|
clock.epochMillis().toInt(),
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun buildOpenHomePendingIntentForSummary(): PendingIntent {
|
private fun buildOpenHomePendingIntentForSummary(): PendingIntent {
|
||||||
val intent = HomeActivity.newIntent(context, firstStartMainActivity = false, clearNotification = true)
|
val intent = HomeActivity.newIntent(context, firstStartMainActivity = false, clearNotification = true)
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||||
|
|
|
@ -33,14 +33,14 @@ class RoomGroupMessageCreator @Inject constructor(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun createRoomMessage(events: List<NotifiableMessageEvent>, roomId: String, userDisplayName: String, userAvatarUrl: String?): RoomNotification.Message {
|
fun createRoomMessage(events: List<NotifiableMessageEvent>, roomId: String, userDisplayName: String, userAvatarUrl: String?): RoomNotification.Message {
|
||||||
val firstKnownRoomEvent = events[0]
|
val lastKnownRoomEvent = events.last()
|
||||||
val roomName = firstKnownRoomEvent.roomName ?: firstKnownRoomEvent.senderName ?: ""
|
val roomName = lastKnownRoomEvent.roomName ?: lastKnownRoomEvent.senderName ?: ""
|
||||||
val roomIsGroup = !firstKnownRoomEvent.roomIsDirect
|
val roomIsGroup = !lastKnownRoomEvent.roomIsDirect
|
||||||
val style = NotificationCompat.MessagingStyle(
|
val style = NotificationCompat.MessagingStyle(
|
||||||
Person.Builder()
|
Person.Builder()
|
||||||
.setName(userDisplayName)
|
.setName(userDisplayName)
|
||||||
.setIcon(bitmapLoader.getUserIcon(userAvatarUrl))
|
.setIcon(bitmapLoader.getUserIcon(userAvatarUrl))
|
||||||
.setKey(firstKnownRoomEvent.matrixID)
|
.setKey(lastKnownRoomEvent.matrixID)
|
||||||
.build()
|
.build()
|
||||||
).also {
|
).also {
|
||||||
it.conversationTitle = roomName.takeIf { roomIsGroup }
|
it.conversationTitle = roomName.takeIf { roomIsGroup }
|
||||||
|
@ -75,6 +75,7 @@ class RoomGroupMessageCreator @Inject constructor(
|
||||||
it.customSound = events.last().soundName
|
it.customSound = events.last().soundName
|
||||||
it.isUpdated = events.last().isUpdated
|
it.isUpdated = events.last().isUpdated
|
||||||
},
|
},
|
||||||
|
threadId = lastKnownRoomEvent.threadId,
|
||||||
largeIcon = largeBitmap,
|
largeIcon = largeBitmap,
|
||||||
lastMessageTimestamp,
|
lastMessageTimestamp,
|
||||||
userDisplayName,
|
userDisplayName,
|
||||||
|
|
|
@ -118,7 +118,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun checkQrCodeLoginCapability(config: HomeServerConnectionConfig) {
|
private fun checkQrCodeLoginCapability() {
|
||||||
if (!vectorFeatures.isQrCodeLoginEnabled()) {
|
if (!vectorFeatures.isQrCodeLoginEnabled()) {
|
||||||
setState {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
|
@ -133,11 +133,9 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// check if selected server supports MSC3882 first
|
|
||||||
val canLoginWithQrCode = authenticationService.isQrLoginSupported(config)
|
|
||||||
setState {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
canLoginWithQrCode = canLoginWithQrCode
|
canLoginWithQrCode = selectedHomeserver.isLoginWithQrSupported
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -705,7 +703,10 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
// This is invalid
|
// This is invalid
|
||||||
_viewEvents.post(OnboardingViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig")))
|
_viewEvents.post(OnboardingViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig")))
|
||||||
} else {
|
} else {
|
||||||
startAuthenticationFlow(action, homeServerConnectionConfig, serverTypeOverride, postAction)
|
startAuthenticationFlow(action, homeServerConnectionConfig, serverTypeOverride, suspend {
|
||||||
|
checkQrCodeLoginCapability()
|
||||||
|
postAction()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -764,8 +765,6 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||||
_viewEvents.post(OnboardingViewEvents.OutdatedHomeserver)
|
_viewEvents.post(OnboardingViewEvents.OutdatedHomeserver)
|
||||||
}
|
}
|
||||||
|
|
||||||
checkQrCodeLoginCapability(config)
|
|
||||||
|
|
||||||
when (trigger) {
|
when (trigger) {
|
||||||
is OnboardingAction.HomeServerChange.SelectHomeServer -> {
|
is OnboardingAction.HomeServerChange.SelectHomeServer -> {
|
||||||
onHomeServerSelected(config, serverTypeOverride, authResult)
|
onHomeServerSelected(config, serverTypeOverride, authResult)
|
||||||
|
|
|
@ -76,6 +76,7 @@ data class SelectedHomeserverState(
|
||||||
val preferredLoginMode: LoginMode = LoginMode.Unknown,
|
val preferredLoginMode: LoginMode = LoginMode.Unknown,
|
||||||
val supportedLoginTypes: List<String> = emptyList(),
|
val supportedLoginTypes: List<String> = emptyList(),
|
||||||
val isLogoutDevicesSupported: Boolean = false,
|
val isLogoutDevicesSupported: Boolean = false,
|
||||||
|
val isLoginWithQrSupported: Boolean = false,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
|
|
@ -47,7 +47,8 @@ class StartAuthenticationFlowUseCase @Inject constructor(
|
||||||
upstreamUrl = authFlow.homeServerUrl,
|
upstreamUrl = authFlow.homeServerUrl,
|
||||||
preferredLoginMode = preferredLoginMode,
|
preferredLoginMode = preferredLoginMode,
|
||||||
supportedLoginTypes = authFlow.supportedLoginTypes,
|
supportedLoginTypes = authFlow.supportedLoginTypes,
|
||||||
isLogoutDevicesSupported = authFlow.isLogoutDevicesSupported
|
isLogoutDevicesSupported = authFlow.isLogoutDevicesSupported,
|
||||||
|
isLoginWithQrSupported = authFlow.isLoginWithQrSupported,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun LoginFlowResult.findPreferredLoginMode() = when {
|
private fun LoginFlowResult.findPreferredLoginMode() = when {
|
||||||
|
|
|
@ -112,6 +112,7 @@ class DevicesViewModel @AssistedInject constructor(
|
||||||
val deviceFullInfoList = async.invoke()
|
val deviceFullInfoList = async.invoke()
|
||||||
val unverifiedSessionsCount = deviceFullInfoList.count { !it.cryptoDeviceInfo?.trustLevel?.isCrossSigningVerified().orFalse() }
|
val unverifiedSessionsCount = deviceFullInfoList.count { !it.cryptoDeviceInfo?.trustLevel?.isCrossSigningVerified().orFalse() }
|
||||||
val inactiveSessionsCount = deviceFullInfoList.count { it.isInactive }
|
val inactiveSessionsCount = deviceFullInfoList.count { it.isInactive }
|
||||||
|
|
||||||
copy(
|
copy(
|
||||||
devices = async,
|
devices = async,
|
||||||
unverifiedSessionsCount = unverifiedSessionsCount,
|
unverifiedSessionsCount = unverifiedSessionsCount,
|
||||||
|
|
|
@ -0,0 +1,136 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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.core.session.clientinfo
|
||||||
|
|
||||||
|
import im.vector.app.test.fakes.FakeActiveSessionHolder
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
|
||||||
|
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
|
||||||
|
|
||||||
|
private const val A_CURRENT_DEVICE_ID = "current-device-id"
|
||||||
|
private const val A_DEVICE_ID_1 = "a-device-id-1"
|
||||||
|
private const val A_DEVICE_ID_2 = "a-device-id-2"
|
||||||
|
private const val A_DEVICE_ID_3 = "a-device-id-3"
|
||||||
|
private const val A_DEVICE_ID_4 = "a-device-id-4"
|
||||||
|
|
||||||
|
private val A_DEVICE_INFO_1 = DeviceInfo(deviceId = A_DEVICE_ID_1)
|
||||||
|
private val A_DEVICE_INFO_2 = DeviceInfo(deviceId = A_DEVICE_ID_2)
|
||||||
|
|
||||||
|
class DeleteUnusedClientInformationUseCaseTest {
|
||||||
|
|
||||||
|
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
|
||||||
|
|
||||||
|
private val deleteUnusedClientInformationUseCase = DeleteUnusedClientInformationUseCase(
|
||||||
|
activeSessionHolder = fakeActiveSessionHolder.instance,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
fakeActiveSessionHolder.fakeSession.givenSessionId(A_CURRENT_DEVICE_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a device list that account data has all of them and extra devices then use case deletes the unused ones`() = runTest {
|
||||||
|
// Given
|
||||||
|
val devices = listOf(A_DEVICE_INFO_1, A_DEVICE_INFO_2)
|
||||||
|
val userAccountDataEventList = listOf(
|
||||||
|
UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_1, mapOf("key" to "value")),
|
||||||
|
UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_2, mapOf("key" to "value")),
|
||||||
|
UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_3, mapOf("key" to "value")),
|
||||||
|
UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_4, mapOf("key" to "value")),
|
||||||
|
)
|
||||||
|
fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.givenGetUserAccountDataEventsStartWith(
|
||||||
|
type = MATRIX_CLIENT_INFO_KEY_PREFIX,
|
||||||
|
userAccountDataEventList = userAccountDataEventList,
|
||||||
|
)
|
||||||
|
|
||||||
|
// When
|
||||||
|
deleteUnusedClientInformationUseCase.execute(devices)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
coVerify { fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.deleteUserAccountData(MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_3) }
|
||||||
|
coVerify { fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.deleteUserAccountData(MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_4) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a device list that account data has exactly all of them then use case does nothing`() = runTest {
|
||||||
|
// Given
|
||||||
|
val devices = listOf(A_DEVICE_INFO_1, A_DEVICE_INFO_2)
|
||||||
|
val userAccountDataEventList = listOf(
|
||||||
|
UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_1, mapOf("key" to "value")),
|
||||||
|
UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_2, mapOf("key" to "value")),
|
||||||
|
)
|
||||||
|
fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.givenGetUserAccountDataEventsStartWith(
|
||||||
|
type = MATRIX_CLIENT_INFO_KEY_PREFIX,
|
||||||
|
userAccountDataEventList = userAccountDataEventList,
|
||||||
|
)
|
||||||
|
|
||||||
|
// When
|
||||||
|
deleteUnusedClientInformationUseCase.execute(devices)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
coVerify(exactly = 0) {
|
||||||
|
fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.deleteUserAccountData(any())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a device list that account data has missing some of them then use case does nothing`() = runTest {
|
||||||
|
// Given
|
||||||
|
val devices = listOf(A_DEVICE_INFO_1, A_DEVICE_INFO_2)
|
||||||
|
val userAccountDataEventList = listOf(
|
||||||
|
UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_1, mapOf("key" to "value")),
|
||||||
|
)
|
||||||
|
fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.givenGetUserAccountDataEventsStartWith(
|
||||||
|
type = MATRIX_CLIENT_INFO_KEY_PREFIX,
|
||||||
|
userAccountDataEventList = userAccountDataEventList,
|
||||||
|
)
|
||||||
|
|
||||||
|
// When
|
||||||
|
deleteUnusedClientInformationUseCase.execute(devices)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
coVerify(exactly = 0) {
|
||||||
|
fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.deleteUserAccountData(any())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given an empty device list that account data has some devices then use case does nothing`() = runTest {
|
||||||
|
// Given
|
||||||
|
val devices = emptyList<DeviceInfo>()
|
||||||
|
val userAccountDataEventList = listOf(
|
||||||
|
UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_1, mapOf("key" to "value")),
|
||||||
|
UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_2, mapOf("key" to "value")),
|
||||||
|
)
|
||||||
|
fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.givenGetUserAccountDataEventsStartWith(
|
||||||
|
type = MATRIX_CLIENT_INFO_KEY_PREFIX,
|
||||||
|
userAccountDataEventList = userAccountDataEventList,
|
||||||
|
)
|
||||||
|
|
||||||
|
// When
|
||||||
|
deleteUnusedClientInformationUseCase.execute(devices)
|
||||||
|
|
||||||
|
// Then
|
||||||
|
coVerify(exactly = 0) {
|
||||||
|
fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.deleteUserAccountData(any())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -83,6 +83,7 @@ private val A_HOMESERVER_CONFIG = HomeServerConnectionConfig(FakeUri().instance)
|
||||||
private val SELECTED_HOMESERVER_STATE = SelectedHomeserverState(preferredLoginMode = LoginMode.Password, userFacingUrl = A_HOMESERVER_URL)
|
private val SELECTED_HOMESERVER_STATE = SelectedHomeserverState(preferredLoginMode = LoginMode.Password, userFacingUrl = A_HOMESERVER_URL)
|
||||||
private val SELECTED_HOMESERVER_STATE_SUPPORTED_LOGOUT_DEVICES = SelectedHomeserverState(isLogoutDevicesSupported = true)
|
private val SELECTED_HOMESERVER_STATE_SUPPORTED_LOGOUT_DEVICES = SelectedHomeserverState(isLogoutDevicesSupported = true)
|
||||||
private val DEFAULT_SELECTED_HOMESERVER_STATE = SELECTED_HOMESERVER_STATE.copy(userFacingUrl = A_DEFAULT_HOMESERVER_URL)
|
private val DEFAULT_SELECTED_HOMESERVER_STATE = SELECTED_HOMESERVER_STATE.copy(userFacingUrl = A_DEFAULT_HOMESERVER_URL)
|
||||||
|
private val DEFAULT_SELECTED_HOMESERVER_STATE_WITH_QR_SUPPORTED = DEFAULT_SELECTED_HOMESERVER_STATE.copy(isLoginWithQrSupported = true)
|
||||||
private const val AN_EMAIL = "hello@example.com"
|
private const val AN_EMAIL = "hello@example.com"
|
||||||
private const val A_PASSWORD = "a-password"
|
private const val A_PASSWORD = "a-password"
|
||||||
private const val A_USERNAME = "hello-world"
|
private const val A_USERNAME = "hello-world"
|
||||||
|
@ -164,7 +165,7 @@ class OnboardingViewModelTest {
|
||||||
fun `given combined login enabled, when handling sign in splash action, then emits OpenCombinedLogin with default homeserver qrCode supported`() = runTest {
|
fun `given combined login enabled, when handling sign in splash action, then emits OpenCombinedLogin with default homeserver qrCode supported`() = runTest {
|
||||||
val test = viewModel.test()
|
val test = viewModel.test()
|
||||||
fakeVectorFeatures.givenCombinedLoginEnabled()
|
fakeVectorFeatures.givenCombinedLoginEnabled()
|
||||||
givenCanSuccessfullyUpdateHomeserver(A_DEFAULT_HOMESERVER_URL, DEFAULT_SELECTED_HOMESERVER_STATE, canLoginWithQrCode = true)
|
givenCanSuccessfullyUpdateHomeserver(A_DEFAULT_HOMESERVER_URL, DEFAULT_SELECTED_HOMESERVER_STATE_WITH_QR_SUPPORTED)
|
||||||
|
|
||||||
viewModel.handle(OnboardingAction.SplashAction.OnIAlreadyHaveAnAccount(OnboardingFlow.SignIn))
|
viewModel.handle(OnboardingAction.SplashAction.OnIAlreadyHaveAnAccount(OnboardingFlow.SignIn))
|
||||||
|
|
||||||
|
@ -173,9 +174,9 @@ class OnboardingViewModelTest {
|
||||||
initialState,
|
initialState,
|
||||||
{ copy(onboardingFlow = OnboardingFlow.SignIn) },
|
{ copy(onboardingFlow = OnboardingFlow.SignIn) },
|
||||||
{ copy(isLoading = true) },
|
{ copy(isLoading = true) },
|
||||||
{ copy(canLoginWithQrCode = true) },
|
{ copy(selectedHomeserver = DEFAULT_SELECTED_HOMESERVER_STATE_WITH_QR_SUPPORTED) },
|
||||||
{ copy(selectedHomeserver = DEFAULT_SELECTED_HOMESERVER_STATE) },
|
|
||||||
{ copy(signMode = SignMode.SignIn) },
|
{ copy(signMode = SignMode.SignIn) },
|
||||||
|
{ copy(canLoginWithQrCode = true) },
|
||||||
{ copy(isLoading = false) }
|
{ copy(isLoading = false) }
|
||||||
)
|
)
|
||||||
.assertEvents(OnboardingViewEvents.OpenCombinedLogin)
|
.assertEvents(OnboardingViewEvents.OpenCombinedLogin)
|
||||||
|
@ -1174,13 +1175,11 @@ class OnboardingViewModelTest {
|
||||||
resultingState: SelectedHomeserverState,
|
resultingState: SelectedHomeserverState,
|
||||||
config: HomeServerConnectionConfig = A_HOMESERVER_CONFIG,
|
config: HomeServerConnectionConfig = A_HOMESERVER_CONFIG,
|
||||||
fingerprint: Fingerprint? = null,
|
fingerprint: Fingerprint? = null,
|
||||||
canLoginWithQrCode: Boolean = false,
|
|
||||||
) {
|
) {
|
||||||
fakeHomeServerConnectionConfigFactory.givenConfigFor(homeserverUrl, fingerprint, config)
|
fakeHomeServerConnectionConfigFactory.givenConfigFor(homeserverUrl, fingerprint, config)
|
||||||
fakeStartAuthenticationFlowUseCase.givenResult(config, StartAuthenticationResult(isHomeserverOutdated = false, resultingState))
|
fakeStartAuthenticationFlowUseCase.givenResult(config, StartAuthenticationResult(isHomeserverOutdated = false, resultingState))
|
||||||
givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.StartRegistration)
|
givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.StartRegistration)
|
||||||
fakeHomeServerHistoryService.expectUrlToBeAdded(config.homeServerUri.toString())
|
fakeHomeServerHistoryService.expectUrlToBeAdded(config.homeServerUri.toString())
|
||||||
fakeAuthenticationService.givenIsQrLoginSupported(config, canLoginWithQrCode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun givenUpdatingHomeserverErrors(homeserverUrl: String, resultingState: SelectedHomeserverState, error: Throwable) {
|
private fun givenUpdatingHomeserverErrors(homeserverUrl: String, resultingState: SelectedHomeserverState, error: Throwable) {
|
||||||
|
@ -1188,7 +1187,6 @@ class OnboardingViewModelTest {
|
||||||
fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(isHomeserverOutdated = false, resultingState))
|
fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(isHomeserverOutdated = false, resultingState))
|
||||||
givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.Error(error))
|
givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.Error(error))
|
||||||
fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString())
|
fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString())
|
||||||
fakeAuthenticationService.givenIsQrLoginSupported(A_HOMESERVER_CONFIG, false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun givenUserNameIsAvailable(userName: String) {
|
private fun givenUserNameIsAvailable(userName: String) {
|
||||||
|
|
|
@ -140,7 +140,8 @@ class StartAuthenticationFlowUseCaseTest {
|
||||||
isLoginAndRegistrationSupported = true,
|
isLoginAndRegistrationSupported = true,
|
||||||
homeServerUrl = A_DECLARED_HOMESERVER_URL,
|
homeServerUrl = A_DECLARED_HOMESERVER_URL,
|
||||||
isOutdatedHomeserver = false,
|
isOutdatedHomeserver = false,
|
||||||
isLogoutDevicesSupported = false
|
isLogoutDevicesSupported = false,
|
||||||
|
isLoginWithQrSupported = false
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun expectedResult(
|
private fun expectedResult(
|
||||||
|
|
|
@ -58,10 +58,6 @@ class FakeAuthenticationService : AuthenticationService by mockk() {
|
||||||
coEvery { getWellKnownData(matrixId, config) } returns result
|
coEvery { getWellKnownData(matrixId, config) } returns result
|
||||||
}
|
}
|
||||||
|
|
||||||
fun givenIsQrLoginSupported(config: HomeServerConnectionConfig, result: Boolean) {
|
|
||||||
coEvery { isQrLoginSupported(config) } returns result
|
|
||||||
}
|
|
||||||
|
|
||||||
fun givenWellKnownThrows(matrixId: String, config: HomeServerConnectionConfig?, cause: Throwable) {
|
fun givenWellKnownThrows(matrixId: String, config: HomeServerConnectionConfig?, cause: Throwable) {
|
||||||
coEvery { getWellKnownData(matrixId, config) } throws cause
|
coEvery { getWellKnownData(matrixId, config) } throws cause
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,4 +54,8 @@ class FakeSessionAccountDataService : SessionAccountDataService by mockk(relaxed
|
||||||
fun verifyUpdateUserAccountDataEventSucceeds(type: String, content: Content, inverse: Boolean = false) {
|
fun verifyUpdateUserAccountDataEventSucceeds(type: String, content: Content, inverse: Boolean = false) {
|
||||||
coVerify(inverse = inverse) { updateUserAccountData(type, content) }
|
coVerify(inverse = inverse) { updateUserAccountData(type, content) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun givenGetUserAccountDataEventsStartWith(type: String, userAccountDataEventList: List<UserAccountDataEvent>) {
|
||||||
|
every { getUserAccountDataEventsStartWith(type) } returns userAccountDataEventList
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue