Merge pull request #180 from vector-im/feature/fix_timeline

Request can now be canceled properly
This commit is contained in:
Benoit Marty 2019-06-17 18:01:46 +02:00 committed by GitHub
commit 7e9275831b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 224 additions and 158 deletions

View file

@ -17,6 +17,7 @@
package im.vector.matrix.android.api.failure package im.vector.matrix.android.api.failure
import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
import java.io.IOException import java.io.IOException
/** /**
@ -35,6 +36,8 @@ sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) {
// When server send an error, but it cannot be interpreted as a MatrixError // When server send an error, but it cannot be interpreted as a MatrixError
data class OtherServerError(val errorBody: String, val httpCode: Int) : Failure(RuntimeException(errorBody)) data class OtherServerError(val errorBody: String, val httpCode: Int) : Failure(RuntimeException(errorBody))
data class RegistrationFlowError(val registrationFlowResponse: RegistrationFlowResponse) : Failure(RuntimeException(registrationFlowResponse.toString()))
data class CryptoError(val error: MXCryptoError) : Failure(RuntimeException(error.toString())) data class CryptoError(val error: MXCryptoError) : Failure(RuntimeException(error.toString()))
abstract class FeatureFailure : Failure() abstract class FeatureFailure : Failure()

View file

@ -36,7 +36,9 @@ interface CryptoService {
fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>)
fun deleteDevice(deviceId: String, accountPassword: String, callback: MatrixCallback<Unit>) fun deleteDevice(deviceId: String, callback: MatrixCallback<Unit>)
fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback<Unit>)
fun getCryptoVersion(context: Context, longFormat: Boolean): String fun getCryptoVersion(context: Context, longFormat: Boolean): String

View file

@ -23,7 +23,7 @@ import com.squareup.moshi.JsonClass
* An interactive authentication flow. * An interactive authentication flow.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class InteractiveAuthenticationFlow( data class InteractiveAuthenticationFlow(
@Json(name = "type") @Json(name = "type")
val type: String? = null, val type: String? = null,

View file

@ -16,7 +16,7 @@
package im.vector.matrix.android.internal.auth.data package im.vector.matrix.android.internal.auth.data
internal object LoginFlowTypes { object LoginFlowTypes {
const val PASSWORD = "m.login.password" const val PASSWORD = "m.login.password"
const val OAUTH2 = "m.login.oauth2" const val OAUTH2 = "m.login.oauth2"
const val EMAIL_CODE = "m.login.email.code" const val EMAIL_CODE = "m.login.email.code"

View file

@ -22,7 +22,7 @@ import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.auth.data.InteractiveAuthenticationFlow import im.vector.matrix.android.internal.auth.data.InteractiveAuthenticationFlow
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class RegistrationFlowResponse( data class RegistrationFlowResponse(
/** /**
* The list of flows. * The list of flows.

View file

@ -55,6 +55,7 @@ import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.*
import im.vector.matrix.android.internal.crypto.tasks.DeleteDeviceTask import im.vector.matrix.android.internal.crypto.tasks.DeleteDeviceTask
import im.vector.matrix.android.internal.crypto.tasks.GetDevicesTask import im.vector.matrix.android.internal.crypto.tasks.GetDevicesTask
import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask
@ -124,6 +125,7 @@ internal class CryptoManager(
private val megolmEncryptionFactory: MXMegolmEncryptionFactory, private val megolmEncryptionFactory: MXMegolmEncryptionFactory,
private val olmEncryptionFactory: MXOlmEncryptionFactory, private val olmEncryptionFactory: MXOlmEncryptionFactory,
private val deleteDeviceTask: DeleteDeviceTask, private val deleteDeviceTask: DeleteDeviceTask,
private val deleteDeviceWithUserPasswordTask: DeleteDeviceWithUserPasswordTask,
// Tasks // Tasks
private val getDevicesTask: GetDevicesTask, private val getDevicesTask: GetDevicesTask,
private val setDeviceNameTask: SetDeviceNameTask, private val setDeviceNameTask: SetDeviceNameTask,
@ -163,9 +165,16 @@ internal class CryptoManager(
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun deleteDevice(deviceId: String, accountPassword: String, callback: MatrixCallback<Unit>) { override fun deleteDevice(deviceId: String, callback: MatrixCallback<Unit>) {
deleteDeviceTask deleteDeviceTask
.configureWith(DeleteDeviceTask.Params(deviceId, accountPassword)) .configureWith(DeleteDeviceTask.Params(deviceId))
.dispatchTo(callback)
.executeBy(taskExecutor)
}
override fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback<Unit>) {
deleteDeviceWithUserPasswordTask
.configureWith(DeleteDeviceWithUserPasswordTask.Params(deviceId, authSession, password))
.dispatchTo(callback) .dispatchTo(callback)
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }

View file

@ -193,6 +193,7 @@ internal class CryptoModule {
megolmEncryptionFactory = get(), megolmEncryptionFactory = get(),
olmEncryptionFactory = get(), olmEncryptionFactory = get(),
deleteDeviceTask = get(), deleteDeviceTask = get(),
deleteDeviceWithUserPasswordTask = get(),
// Tasks // Tasks
getDevicesTask = get(), getDevicesTask = get(),
setDeviceNameTask = get(), setDeviceNameTask = get(),
@ -227,7 +228,10 @@ internal class CryptoModule {
DefaultClaimOneTimeKeysForUsersDevice(get()) as ClaimOneTimeKeysForUsersDeviceTask DefaultClaimOneTimeKeysForUsersDevice(get()) as ClaimOneTimeKeysForUsersDeviceTask
} }
scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
DefaultDeleteDeviceTask(get(), get()) as DeleteDeviceTask DefaultDeleteDeviceTask(get()) as DeleteDeviceTask
}
scope(DefaultSession.SCOPE) {
DefaultDeleteDeviceWithUserPasswordTask(get(), get()) as DeleteDeviceWithUserPasswordTask
} }
scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
DefaultDownloadKeysForUsers(get()) as DownloadKeysForUsersTask DefaultDownloadKeysForUsers(get()) as DownloadKeysForUsersTask

View file

@ -22,7 +22,7 @@ import com.squareup.moshi.JsonClass
* This class provides the authentication data to delete a device * This class provides the authentication data to delete a device
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class DeleteDeviceAuth( internal data class DeleteDeviceAuth(
// device device session id // device device session id
@Json(name = "session") @Json(name = "session")

View file

@ -22,7 +22,7 @@ import com.squareup.moshi.JsonClass
* This class provides the parameter to delete a device * This class provides the parameter to delete a device
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class DeleteDeviceParams( internal data class DeleteDeviceParams(
@Json(name = "auth") @Json(name = "auth")
var deleteDeviceAuth: DeleteDeviceAuth? = null var deleteDeviceAuth: DeleteDeviceAuth? = null
) )

View file

@ -19,27 +19,21 @@ package im.vector.matrix.android.internal.crypto.tasks
import arrow.core.Try import arrow.core.Try
import arrow.core.failure import arrow.core.failure
import arrow.core.recoverWith import arrow.core.recoverWith
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
import im.vector.matrix.android.internal.crypto.api.CryptoApi import im.vector.matrix.android.internal.crypto.api.CryptoApi
import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceAuth
import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import timber.log.Timber
internal interface DeleteDeviceTask : Task<DeleteDeviceTask.Params, Unit> { internal interface DeleteDeviceTask : Task<DeleteDeviceTask.Params, Unit> {
data class Params( data class Params(
val deviceId: String, val deviceId: String
val accountPassword: String
) )
} }
internal class DefaultDeleteDeviceTask(private val cryptoApi: CryptoApi, internal class DefaultDeleteDeviceTask(private val cryptoApi: CryptoApi)
private val credentials: Credentials)
: DeleteDeviceTask { : DeleteDeviceTask {
override suspend fun execute(params: DeleteDeviceTask.Params): Try<Unit> { override suspend fun execute(params: DeleteDeviceTask.Params): Try<Unit> {
@ -47,28 +41,18 @@ internal class DefaultDeleteDeviceTask(private val cryptoApi: CryptoApi,
apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams()) apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams())
}.recoverWith { throwable -> }.recoverWith { throwable ->
if (throwable is Failure.OtherServerError && throwable.httpCode == 401) { if (throwable is Failure.OtherServerError && throwable.httpCode == 401) {
// Replay the request with passing the credentials
// Parse to get a RegistrationFlowResponse // Parse to get a RegistrationFlowResponse
val registrationFlowResponseAdapter = MoshiProvider.providesMoshi().adapter(RegistrationFlowResponse::class.java)
val registrationFlowResponse = try { val registrationFlowResponse = try {
registrationFlowResponseAdapter.fromJson(throwable.errorBody) MoshiProvider.providesMoshi()
.adapter(RegistrationFlowResponse::class.java)
.fromJson(throwable.errorBody)
} catch (e: Exception) { } catch (e: Exception) {
null null
} }
// check if the server response can be casted // check if the server response can be casted
if (registrationFlowResponse?.flows?.isNotEmpty() == true) { if (registrationFlowResponse != null) {
val stages = ArrayList<String>() Failure.RegistrationFlowError(registrationFlowResponse).failure()
// Get all stages
registrationFlowResponse.flows?.forEach {
stages.addAll(it.stages ?: emptyList())
}
Timber.v("## deleteDevice() : supported stages $stages")
deleteDeviceRecursive(registrationFlowResponse.session, params, stages)
} else { } else {
throwable.failure() throwable.failure()
} }
@ -79,43 +63,4 @@ internal class DefaultDeleteDeviceTask(private val cryptoApi: CryptoApi,
} }
} }
} }
private fun deleteDeviceRecursive(authSession: String?,
params: DeleteDeviceTask.Params,
remainingStages: MutableList<String>): Try<Unit> {
// Pick the first stage
val stage = remainingStages.first()
val newParams = DeleteDeviceParams()
.apply {
deleteDeviceAuth = DeleteDeviceAuth()
.apply {
type = stage
session = authSession
user = credentials.userId
password = params.accountPassword
}
}
return executeRequest<Unit> {
apiCall = cryptoApi.deleteDevice(params.deviceId, newParams)
}.recoverWith { throwable ->
if (throwable is Failure.ServerError
&& throwable.httpCode == 401
&& (throwable.error.code == MatrixError.FORBIDDEN || throwable.error.code == MatrixError.UNKNOWN)) {
if (remainingStages.size > 1) {
// Try next stage
val otherStages = remainingStages.subList(1, remainingStages.size)
deleteDeviceRecursive(authSession, params, otherStages)
} else {
// No more stage remaining
throwable.failure()
}
} else {
// Other error
throwable.failure()
}
}
}
} }

View file

@ -0,0 +1,54 @@
/*
* Copyright 2019 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.matrix.android.internal.crypto.tasks
import arrow.core.Try
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.crypto.api.CryptoApi
import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceAuth
import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
internal interface DeleteDeviceWithUserPasswordTask : Task<DeleteDeviceWithUserPasswordTask.Params, Unit> {
data class Params(
val deviceId: String,
val authSession: String?,
val password: String
)
}
internal class DefaultDeleteDeviceWithUserPasswordTask(private val cryptoApi: CryptoApi,
private val credentials: Credentials)
: DeleteDeviceWithUserPasswordTask {
override suspend fun execute(params: DeleteDeviceWithUserPasswordTask.Params): Try<Unit> {
return executeRequest {
apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams()
.apply {
deleteDeviceAuth = DeleteDeviceAuth()
.apply {
type = LoginFlowTypes.PASSWORD
session = params.authSession
user = credentials.userId
password = params.password
}
})
}
}
}

View file

@ -28,34 +28,45 @@ import com.squareup.moshi.Moshi
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.ResponseBody import okhttp3.ResponseBody
import retrofit2.Call import retrofit2.Call
import timber.log.Timber import timber.log.Timber
import java.io.IOException import java.io.IOException
import kotlin.coroutines.resume
internal inline fun <DATA> executeRequest(block: Request<DATA>.() -> Unit) = Request<DATA>().apply(block).execute() internal suspend inline fun <DATA> executeRequest(block: Request<DATA>.() -> Unit) = Request<DATA>().apply(block).execute()
internal class Request<DATA> { internal class Request<DATA> {
private val moshi: Moshi = MoshiProvider.providesMoshi() private val moshi: Moshi = MoshiProvider.providesMoshi()
lateinit var apiCall: Call<DATA> lateinit var apiCall: Call<DATA>
fun execute(): Try<DATA> { suspend fun execute(): Try<DATA> {
return Try { return suspendCancellableCoroutine { continuation ->
val response = apiCall.runAsync(IO.async()).fix().unsafeRunSync() continuation.invokeOnCancellation {
if (response.isSuccessful) { Timber.v("Request is canceled")
response.body() ?: throw IllegalStateException("The request returned a null body") apiCall.cancel()
} else {
throw manageFailure(response.errorBody(), response.code())
} }
}.recoverWith { val result = Try {
when (it) { val response = apiCall.runAsync(IO.async()).fix().unsafeRunSync()
is IOException -> Failure.NetworkConnection(it) if (response.isSuccessful) {
is Failure.ServerError, response.body()
is Failure.OtherServerError -> it ?: throw IllegalStateException("The request returned a null body")
else -> Failure.Unknown(it) } else {
}.failure() throw manageFailure(response.errorBody(), response.code())
}
}.recoverWith {
when (it) {
is IOException -> Failure.NetworkConnection(it)
is Failure.ServerError,
is Failure.OtherServerError -> it
else -> Failure.Unknown(it)
}.failure()
}
continuation.resume(result)
} }
} }
private fun manageFailure(errorBody: ResponseBody?, httpCode: Int): Throwable { private fun manageFailure(errorBody: ResponseBody?, httpCode: Int): Throwable {

View file

@ -290,8 +290,12 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
cryptoService.setDeviceName(deviceId, deviceName, callback) cryptoService.setDeviceName(deviceId, deviceName, callback)
} }
override fun deleteDevice(deviceId: String, accountPassword: String, callback: MatrixCallback<Unit>) { override fun deleteDevice(deviceId: String, callback: MatrixCallback<Unit>) {
cryptoService.deleteDevice(deviceId, accountPassword, callback) cryptoService.deleteDevice(deviceId, callback)
}
override fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback<Unit>) {
cryptoService.deleteDeviceWithUserPassword(deviceId, authSession, password, callback)
} }
override fun getCryptoVersion(context: Context, longFormat: Boolean): String { override fun getCryptoVersion(context: Context, longFormat: Boolean): String {

View file

@ -21,13 +21,13 @@ import arrow.core.fix
import arrow.instances.`try`.monad.monad import arrow.instances.`try`.monad.monad
import arrow.typeclasses.binding import arrow.typeclasses.binding
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.database.model.GroupSummaryEntity import im.vector.matrix.android.internal.database.model.GroupSummaryEntity
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.group.model.GroupRooms import im.vector.matrix.android.internal.session.group.model.GroupRooms
import im.vector.matrix.android.internal.session.group.model.GroupSummaryResponse import im.vector.matrix.android.internal.session.group.model.GroupSummaryResponse
import im.vector.matrix.android.internal.session.group.model.GroupUsers import im.vector.matrix.android.internal.session.group.model.GroupUsers
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.tryTransactionSync import im.vector.matrix.android.internal.util.tryTransactionSync
import io.realm.kotlin.createObject import io.realm.kotlin.createObject
@ -45,21 +45,17 @@ internal class DefaultGetGroupDataTask(
override suspend fun execute(params: GetGroupDataTask.Params): Try<Unit> { override suspend fun execute(params: GetGroupDataTask.Params): Try<Unit> {
val groupId = params.groupId val groupId = params.groupId
val groupSummary = executeRequest<GroupSummaryResponse> {
apiCall = groupAPI.getSummary(groupId)
}
val groupRooms = executeRequest<GroupRooms> {
apiCall = groupAPI.getRooms(groupId)
}
val groupUsers = executeRequest<GroupUsers> {
apiCall = groupAPI.getUsers(groupId)
}
return Try.monad().binding { return Try.monad().binding {
insertInDb(groupSummary.bind(), groupRooms.bind(), groupUsers.bind(), groupId).bind()
val groupSummary = executeRequest<GroupSummaryResponse> {
apiCall = groupAPI.getSummary(groupId)
}.bind()
val groupRooms = executeRequest<GroupRooms> {
apiCall = groupAPI.getRooms(groupId)
}.bind()
val groupUsers = executeRequest<GroupUsers> {
apiCall = groupAPI.getUsers(groupId)
}.bind()
insertInDb(groupSummary, groupRooms, groupUsers, groupId).bind()
}.fix() }.fix()
} }
@ -71,12 +67,13 @@ internal class DefaultGetGroupDataTask(
return monarchy return monarchy
.tryTransactionSync { realm -> .tryTransactionSync { realm ->
val groupSummaryEntity = GroupSummaryEntity.where(realm, groupId).findFirst() val groupSummaryEntity = GroupSummaryEntity.where(realm, groupId).findFirst()
?: realm.createObject(groupId) ?: realm.createObject(groupId)
groupSummaryEntity.avatarUrl = groupSummary.profile?.avatarUrl ?: "" groupSummaryEntity.avatarUrl = groupSummary.profile?.avatarUrl ?: ""
val name = groupSummary.profile?.name val name = groupSummary.profile?.name
groupSummaryEntity.displayName = if (name.isNullOrEmpty()) groupId else name groupSummaryEntity.displayName = if (name.isNullOrEmpty()) groupId else name
groupSummaryEntity.shortDescription = groupSummary.profile?.shortDescription ?: "" groupSummaryEntity.shortDescription = groupSummary.profile?.shortDescription
?: ""
val roomIds = groupRooms.rooms.map { it.roomId } val roomIds = groupRooms.rooms.map { it.roomId }
groupSummaryEntity.roomIds.clear() groupSummaryEntity.roomIds.clear()

View file

@ -16,6 +16,7 @@
package im.vector.matrix.android.internal.session.room.relation package im.vector.matrix.android.internal.session.room.relation
import android.content.Context import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
@ -32,7 +33,7 @@ import im.vector.matrix.android.internal.util.WorkerParamsFactory
import org.koin.standalone.inject import org.koin.standalone.inject
class SendRelationWorker(context: Context, params: WorkerParameters) class SendRelationWorker(context: Context, params: WorkerParameters)
: Worker(context, params), MatrixKoinComponent { : CoroutineWorker(context, params), MatrixKoinComponent {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
@ -44,7 +45,7 @@ class SendRelationWorker(context: Context, params: WorkerParameters)
private val roomAPI by inject<RoomAPI>() private val roomAPI by inject<RoomAPI>()
override fun doWork(): Result { override suspend fun doWork(): Result {
val params = WorkerParamsFactory.fromData<Params>(inputData) val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.failure() ?: return Result.failure()

View file

@ -16,6 +16,7 @@
package im.vector.matrix.android.internal.session.room.send package im.vector.matrix.android.internal.session.room.send
import android.content.Context import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
@ -27,7 +28,7 @@ import im.vector.matrix.android.internal.util.WorkerParamsFactory
import org.koin.standalone.inject import org.koin.standalone.inject
internal class RedactEventWorker(context: Context, params: WorkerParameters) internal class RedactEventWorker(context: Context, params: WorkerParameters)
: Worker(context, params), MatrixKoinComponent { : CoroutineWorker(context, params), MatrixKoinComponent {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
@ -39,7 +40,7 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters)
private val roomAPI by inject<RoomAPI>() private val roomAPI by inject<RoomAPI>()
override fun doWork(): Result { override suspend fun doWork(): Result {
val params = WorkerParamsFactory.fromData<Params>(inputData) val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.failure() ?: return Result.failure()

View file

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.session.room.send package im.vector.matrix.android.internal.session.room.send
import android.content.Context import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
@ -30,7 +31,7 @@ import im.vector.matrix.android.internal.util.WorkerParamsFactory
import org.koin.standalone.inject import org.koin.standalone.inject
internal class SendEventWorker(context: Context, params: WorkerParameters) internal class SendEventWorker(context: Context, params: WorkerParameters)
: Worker(context, params), MatrixKoinComponent { : CoroutineWorker(context, params), MatrixKoinComponent {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
@ -42,7 +43,7 @@ internal class SendEventWorker(context: Context, params: WorkerParameters)
private val roomAPI by inject<RoomAPI>() private val roomAPI by inject<RoomAPI>()
private val localEchoUpdater by inject<LocalEchoUpdater>() private val localEchoUpdater by inject<LocalEchoUpdater>()
override fun doWork(): Result { override suspend fun doWork(): Result {
val params = WorkerParamsFactory.fromData<Params>(inputData) val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.success() ?: return Result.success()

View file

@ -46,7 +46,9 @@ import com.google.android.material.textfield.TextInputLayout
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.extensions.getFingerprintHumanReadable import im.vector.matrix.android.api.extensions.getFingerprintHumanReadable
import im.vector.matrix.android.api.extensions.sortByLastSeen import im.vector.matrix.android.api.extensions.sortByLastSeen
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.riotredesign.R import im.vector.riotredesign.R
@ -2413,7 +2415,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
// disable the deletion for our own device // disable the deletion for our own device
if (!TextUtils.equals(mSession.getMyDevice()?.deviceId, aDeviceInfo.deviceId)) { if (!TextUtils.equals(mSession.getMyDevice()?.deviceId, aDeviceInfo.deviceId)) {
builder.setNegativeButton(R.string.delete) { _, _ -> displayDeviceDeletionDialog(aDeviceInfo) } builder.setNegativeButton(R.string.delete) { _, _ -> deleteDevice(aDeviceInfo) }
} }
builder.setNeutralButton(R.string.cancel, null) builder.setNeutralButton(R.string.cancel, null)
@ -2486,11 +2488,17 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
/** /**
* Try to delete a device. * Try to delete a device.
* *
* @param deviceId the device id * @param deviceInfo the device to delete
*/ */
private fun deleteDevice(deviceId: String) { private fun deleteDevice(deviceInfo: DeviceInfo) {
val deviceId = deviceInfo.deviceId
if (deviceId == null) {
Timber.e("## displayDeviceDeletionDialog(): sanity check failure")
return
}
displayLoadingView() displayLoadingView()
mSession.deleteDevice(deviceId, mAccountPassword, object : MatrixCallback<Unit> { mSession.deleteDevice(deviceId, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
hideLoadingView() hideLoadingView()
// force settings update // force settings update
@ -2498,59 +2506,85 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
mAccountPassword = "" var isPasswordRequestFound = false
onCommonDone(failure.localizedMessage)
if (failure is Failure.RegistrationFlowError) {
// We only support LoginFlowTypes.PASSWORD
// Check if we can provide the user password
failure.registrationFlowResponse.flows?.forEach { interactiveAuthenticationFlow ->
isPasswordRequestFound = isPasswordRequestFound || interactiveAuthenticationFlow.stages?.any { it == LoginFlowTypes.PASSWORD } == true
}
if (isPasswordRequestFound) {
maybeShowDeleteDeviceWithPasswordDialog(deviceId, failure.registrationFlowResponse.session)
}
}
if (!isPasswordRequestFound) {
// LoginFlowTypes.PASSWORD not supported, and this is the only one RiotX supports so far...
onCommonDone(failure.localizedMessage)
}
} }
}) })
} }
/** /**
* Display a delete confirmation dialog to remove a device.<br></br> * Show a dialog to ask for user password, or use a previously entered password.
* The user is invited to enter his password to confirm the deletion.
*
* @param aDeviceInfoToDelete device info
*/ */
private fun displayDeviceDeletionDialog(aDeviceInfoToDelete: DeviceInfo) { private fun maybeShowDeleteDeviceWithPasswordDialog(deviceId: String, authSession: String?) {
if (aDeviceInfoToDelete.deviceId != null) { if (!TextUtils.isEmpty(mAccountPassword)) {
if (!TextUtils.isEmpty(mAccountPassword)) { deleteDeviceWithPassword(deviceId, authSession, mAccountPassword)
deleteDevice(aDeviceInfoToDelete.deviceId!!)
} else {
activity?.let {
val inflater = it.layoutInflater
val layout = inflater.inflate(R.layout.dialog_device_delete, null)
val passwordEditText = layout.findViewById<EditText>(R.id.delete_password)
AlertDialog.Builder(it)
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(R.string.devices_delete_dialog_title)
.setView(layout)
.setPositiveButton(R.string.devices_delete_submit_button_label, DialogInterface.OnClickListener { _, _ ->
if (TextUtils.isEmpty(passwordEditText.toString())) {
it.toast(R.string.error_empty_field_your_password)
return@OnClickListener
}
mAccountPassword = passwordEditText.text.toString()
deleteDevice(aDeviceInfoToDelete.deviceId!!)
})
.setNegativeButton(R.string.cancel) { _, _ ->
hideLoadingView()
}
.setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event ->
if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
dialog.cancel()
hideLoadingView()
return@OnKeyListener true
}
false
})
.show()
}
}
} else { } else {
Timber.e("## displayDeviceDeletionDialog(): sanity check failure") activity?.let {
val inflater = it.layoutInflater
val layout = inflater.inflate(R.layout.dialog_device_delete, null)
val passwordEditText = layout.findViewById<EditText>(R.id.delete_password)
AlertDialog.Builder(it)
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(R.string.devices_delete_dialog_title)
.setView(layout)
.setPositiveButton(R.string.devices_delete_submit_button_label, DialogInterface.OnClickListener { _, _ ->
if (TextUtils.isEmpty(passwordEditText.toString())) {
it.toast(R.string.error_empty_field_your_password)
return@OnClickListener
}
mAccountPassword = passwordEditText.text.toString()
deleteDeviceWithPassword(deviceId, authSession, mAccountPassword)
})
.setNegativeButton(R.string.cancel) { _, _ ->
hideLoadingView()
}
.setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event ->
if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
dialog.cancel()
hideLoadingView()
return@OnKeyListener true
}
false
})
.show()
}
} }
} }
private fun deleteDeviceWithPassword(deviceId: String, authSession: String?, accountPassword: String) {
mSession.deleteDeviceWithUserPassword(deviceId, authSession, accountPassword, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
hideLoadingView()
// force settings update
refreshDevicesList()
}
override fun onFailure(failure: Throwable) {
// Password is maybe not good
onCommonDone(failure.localizedMessage)
mAccountPassword = ""
}
})
}
/** /**
* Manage the e2e keys export. * Manage the e2e keys export.
*/ */