Merge pull request #3445 from vector-im/feature/bma/withContext
Migration to coroutines
This commit is contained in:
commit
2864101e2a
@ -42,7 +42,6 @@ import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent
|
|||||||
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse
|
import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
|
import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
|
||||||
import kotlin.jvm.Throws
|
|
||||||
|
|
||||||
interface CryptoService {
|
interface CryptoService {
|
||||||
|
|
||||||
@ -82,9 +81,11 @@ interface CryptoService {
|
|||||||
|
|
||||||
fun getDeviceTrackingStatus(userId: String): Int
|
fun getDeviceTrackingStatus(userId: String): Int
|
||||||
|
|
||||||
fun importRoomKeys(roomKeysAsArray: ByteArray, password: String, progressListener: ProgressListener?, callback: MatrixCallback<ImportRoomKeysResult>)
|
suspend fun importRoomKeys(roomKeysAsArray: ByteArray,
|
||||||
|
password: String,
|
||||||
|
progressListener: ProgressListener?): ImportRoomKeysResult
|
||||||
|
|
||||||
fun exportRoomKeys(password: String, callback: MatrixCallback<ByteArray>)
|
suspend fun exportRoomKeys(password: String): ByteArray
|
||||||
|
|
||||||
fun setRoomBlacklistUnverifiedDevices(roomId: String)
|
fun setRoomBlacklistUnverifiedDevices(roomId: String)
|
||||||
|
|
||||||
|
@ -928,14 +928,10 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
* Export the crypto keys
|
* Export the crypto keys
|
||||||
*
|
*
|
||||||
* @param password the password
|
* @param password the password
|
||||||
* @param callback the exported keys
|
* @return the exported keys
|
||||||
*/
|
*/
|
||||||
override fun exportRoomKeys(password: String, callback: MatrixCallback<ByteArray>) {
|
override suspend fun exportRoomKeys(password: String): ByteArray {
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
return exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT)
|
||||||
runCatching {
|
|
||||||
exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT)
|
|
||||||
}.foldToCallback(callback)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -963,42 +959,37 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
* @param roomKeysAsArray the room keys as array.
|
* @param roomKeysAsArray the room keys as array.
|
||||||
* @param password the password
|
* @param password the password
|
||||||
* @param progressListener the progress listener
|
* @param progressListener the progress listener
|
||||||
* @param callback the asynchronous callback.
|
* @return the result ImportRoomKeysResult
|
||||||
*/
|
*/
|
||||||
override fun importRoomKeys(roomKeysAsArray: ByteArray,
|
override suspend fun importRoomKeys(roomKeysAsArray: ByteArray,
|
||||||
password: String,
|
password: String,
|
||||||
progressListener: ProgressListener?,
|
progressListener: ProgressListener?): ImportRoomKeysResult {
|
||||||
callback: MatrixCallback<ImportRoomKeysResult>) {
|
return withContext(coroutineDispatchers.crypto) {
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
Timber.v("## CRYPTO | importRoomKeys starts")
|
||||||
runCatching {
|
|
||||||
withContext(coroutineDispatchers.crypto) {
|
|
||||||
Timber.v("## CRYPTO | importRoomKeys starts")
|
|
||||||
|
|
||||||
val t0 = System.currentTimeMillis()
|
val t0 = System.currentTimeMillis()
|
||||||
val roomKeys = MXMegolmExportEncryption.decryptMegolmKeyFile(roomKeysAsArray, password)
|
val roomKeys = MXMegolmExportEncryption.decryptMegolmKeyFile(roomKeysAsArray, password)
|
||||||
val t1 = System.currentTimeMillis()
|
val t1 = System.currentTimeMillis()
|
||||||
|
|
||||||
Timber.v("## CRYPTO | importRoomKeys : decryptMegolmKeyFile done in ${t1 - t0} ms")
|
Timber.v("## CRYPTO | importRoomKeys : decryptMegolmKeyFile done in ${t1 - t0} ms")
|
||||||
|
|
||||||
val importedSessions = MoshiProvider.providesMoshi()
|
val importedSessions = MoshiProvider.providesMoshi()
|
||||||
.adapter<List<MegolmSessionData>>(Types.newParameterizedType(List::class.java, MegolmSessionData::class.java))
|
.adapter<List<MegolmSessionData>>(Types.newParameterizedType(List::class.java, MegolmSessionData::class.java))
|
||||||
.fromJson(roomKeys)
|
.fromJson(roomKeys)
|
||||||
|
|
||||||
val t2 = System.currentTimeMillis()
|
val t2 = System.currentTimeMillis()
|
||||||
|
|
||||||
Timber.v("## CRYPTO | importRoomKeys : JSON parsing ${t2 - t1} ms")
|
Timber.v("## CRYPTO | importRoomKeys : JSON parsing ${t2 - t1} ms")
|
||||||
|
|
||||||
if (importedSessions == null) {
|
if (importedSessions == null) {
|
||||||
throw Exception("Error")
|
throw Exception("Error")
|
||||||
}
|
}
|
||||||
|
|
||||||
megolmSessionDataImporter.handle(
|
megolmSessionDataImporter.handle(
|
||||||
megolmSessionsData = importedSessions,
|
megolmSessionsData = importedSessions,
|
||||||
fromBackup = false,
|
fromBackup = false,
|
||||||
progressListener = progressListener
|
progressListener = progressListener
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}.foldToCallback(callback)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +24,6 @@ import io.realm.RealmQuery
|
|||||||
import io.realm.RealmResults
|
import io.realm.RealmResults
|
||||||
import io.realm.Sort
|
import io.realm.Sort
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
import org.matrix.android.sdk.api.NoOpMatrixCallback
|
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
@ -168,9 +167,7 @@ internal class DefaultTimeline(
|
|||||||
timelineEvents.addChangeListener(eventsChangeListener)
|
timelineEvents.addChangeListener(eventsChangeListener)
|
||||||
handleInitialLoad()
|
handleInitialLoad()
|
||||||
loadRoomMembersTask
|
loadRoomMembersTask
|
||||||
.configureWith(LoadRoomMembersTask.Params(roomId)) {
|
.configureWith(LoadRoomMembersTask.Params(roomId))
|
||||||
this.callback = NoOpMatrixCallback()
|
|
||||||
}
|
|
||||||
.executeBy(taskExecutor)
|
.executeBy(taskExecutor)
|
||||||
|
|
||||||
// Ensure ReadReceipt from init sync are loaded
|
// Ensure ReadReceipt from init sync are loaded
|
||||||
|
@ -18,13 +18,13 @@ package org.matrix.android.sdk.internal.session.room.typing
|
|||||||
|
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import dagger.assisted.AssistedInject
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.session.room.typing.TypingService
|
import org.matrix.android.sdk.api.session.room.typing.TypingService
|
||||||
import org.matrix.android.sdk.api.util.Cancelable
|
|
||||||
import org.matrix.android.sdk.internal.task.TaskExecutor
|
|
||||||
import org.matrix.android.sdk.internal.task.configureWith
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -35,7 +35,6 @@ import timber.log.Timber
|
|||||||
*/
|
*/
|
||||||
internal class DefaultTypingService @AssistedInject constructor(
|
internal class DefaultTypingService @AssistedInject constructor(
|
||||||
@Assisted private val roomId: String,
|
@Assisted private val roomId: String,
|
||||||
private val taskExecutor: TaskExecutor,
|
|
||||||
private val sendTypingTask: SendTypingTask
|
private val sendTypingTask: SendTypingTask
|
||||||
) : TypingService {
|
) : TypingService {
|
||||||
|
|
||||||
@ -44,8 +43,8 @@ internal class DefaultTypingService @AssistedInject constructor(
|
|||||||
fun create(roomId: String): DefaultTypingService
|
fun create(roomId: String): DefaultTypingService
|
||||||
}
|
}
|
||||||
|
|
||||||
private var currentTask: Cancelable? = null
|
private val coroutineScope = CoroutineScope(Job())
|
||||||
private var currentAutoStopTask: Cancelable? = null
|
private var currentTask: Job? = null
|
||||||
|
|
||||||
// What the homeserver knows
|
// What the homeserver knows
|
||||||
private var userIsTyping = false
|
private var userIsTyping = false
|
||||||
@ -53,26 +52,24 @@ internal class DefaultTypingService @AssistedInject constructor(
|
|||||||
// Last time the user is typing event has been sent
|
// Last time the user is typing event has been sent
|
||||||
private var lastRequestTimestamp: Long = 0
|
private var lastRequestTimestamp: Long = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify to the server that the user is typing and schedule the auto typing off
|
||||||
|
*/
|
||||||
override fun userIsTyping() {
|
override fun userIsTyping() {
|
||||||
scheduleAutoStop()
|
|
||||||
|
|
||||||
val now = SystemClock.elapsedRealtime()
|
val now = SystemClock.elapsedRealtime()
|
||||||
|
|
||||||
if (userIsTyping && now < lastRequestTimestamp + MIN_DELAY_BETWEEN_TWO_USER_IS_TYPING_REQUESTS_MILLIS) {
|
|
||||||
Timber.d("Typing: Skip start request")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.d("Typing: Send start request")
|
|
||||||
userIsTyping = true
|
|
||||||
lastRequestTimestamp = now
|
|
||||||
|
|
||||||
currentTask?.cancel()
|
currentTask?.cancel()
|
||||||
|
currentTask = coroutineScope.launch {
|
||||||
val params = SendTypingTask.Params(roomId, true)
|
if (userIsTyping && now < lastRequestTimestamp + MIN_DELAY_BETWEEN_TWO_USER_IS_TYPING_REQUESTS_MILLIS) {
|
||||||
currentTask = sendTypingTask
|
Timber.d("Typing: Skip start request")
|
||||||
.configureWith(params)
|
} else {
|
||||||
.executeBy(taskExecutor)
|
Timber.d("Typing: Send start request")
|
||||||
|
lastRequestTimestamp = now
|
||||||
|
sendRequest(true)
|
||||||
|
}
|
||||||
|
delay(MIN_DELAY_TO_SEND_STOP_TYPING_REQUEST_WHEN_NO_USER_ACTIVITY_MILLIS)
|
||||||
|
Timber.d("Typing: auto stop")
|
||||||
|
sendRequest(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun userStopsTyping() {
|
override fun userStopsTyping() {
|
||||||
@ -82,35 +79,22 @@ internal class DefaultTypingService @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Timber.d("Typing: Send stop request")
|
Timber.d("Typing: Send stop request")
|
||||||
userIsTyping = false
|
|
||||||
lastRequestTimestamp = 0
|
lastRequestTimestamp = 0
|
||||||
|
|
||||||
currentAutoStopTask?.cancel()
|
|
||||||
currentTask?.cancel()
|
currentTask?.cancel()
|
||||||
|
currentTask = coroutineScope.launch {
|
||||||
val params = SendTypingTask.Params(roomId, false)
|
sendRequest(false)
|
||||||
currentTask = sendTypingTask
|
}
|
||||||
.configureWith(params)
|
|
||||||
.executeBy(taskExecutor)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun scheduleAutoStop() {
|
private suspend fun sendRequest(isTyping: Boolean) {
|
||||||
Timber.d("Typing: Schedule auto stop")
|
try {
|
||||||
currentAutoStopTask?.cancel()
|
sendTypingTask.execute(SendTypingTask.Params(roomId, isTyping))
|
||||||
|
userIsTyping = isTyping
|
||||||
val params = SendTypingTask.Params(
|
} catch (failure: Throwable) {
|
||||||
roomId,
|
// Ignore network error, etc...
|
||||||
false,
|
Timber.w(failure, "Unable to send typing request")
|
||||||
delay = MIN_DELAY_TO_SEND_STOP_TYPING_REQUEST_WHEN_NO_USER_ACTIVITY_MILLIS)
|
}
|
||||||
currentAutoStopTask = sendTypingTask
|
|
||||||
.configureWith(params) {
|
|
||||||
callback = object : MatrixCallback<Unit> {
|
|
||||||
override fun onSuccess(data: Unit) {
|
|
||||||
userIsTyping = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.executeBy(taskExecutor)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -17,11 +17,10 @@
|
|||||||
package org.matrix.android.sdk.internal.session.room.typing
|
package org.matrix.android.sdk.internal.session.room.typing
|
||||||
|
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
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.network.executeRequest
|
||||||
import org.matrix.android.sdk.internal.session.room.RoomAPI
|
import org.matrix.android.sdk.internal.session.room.RoomAPI
|
||||||
import org.matrix.android.sdk.internal.task.Task
|
import org.matrix.android.sdk.internal.task.Task
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal interface SendTypingTask : Task<SendTypingTask.Params, Unit> {
|
internal interface SendTypingTask : Task<SendTypingTask.Params, Unit> {
|
||||||
@ -29,9 +28,7 @@ internal interface SendTypingTask : Task<SendTypingTask.Params, Unit> {
|
|||||||
data class Params(
|
data class Params(
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val isTyping: Boolean,
|
val isTyping: Boolean,
|
||||||
val typingTimeoutMillis: Int? = 30_000,
|
val typingTimeoutMillis: Int? = 30_000
|
||||||
// Optional delay before sending the request to the homeserver
|
|
||||||
val delay: Long? = null
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,8 +39,6 @@ internal class DefaultSendTypingTask @Inject constructor(
|
|||||||
) : SendTypingTask {
|
) : SendTypingTask {
|
||||||
|
|
||||||
override suspend fun execute(params: SendTypingTask.Params) {
|
override suspend fun execute(params: SendTypingTask.Params) {
|
||||||
delay(params.delay ?: -1)
|
|
||||||
|
|
||||||
executeRequest(globalErrorReceiver) {
|
executeRequest(globalErrorReceiver) {
|
||||||
roomAPI.sendTypingState(
|
roomAPI.sendTypingState(
|
||||||
params.roomId,
|
params.roomId,
|
||||||
|
1
newsfragment/2449.feature
Normal file
1
newsfragment/2449.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Migrate DefaultTypingService, KeysImporter and KeysExporter to coroutines
|
@ -32,6 +32,7 @@ import im.vector.app.features.call.conference.VectorJitsiActivity
|
|||||||
import im.vector.app.features.call.transfer.CallTransferActivity
|
import im.vector.app.features.call.transfer.CallTransferActivity
|
||||||
import im.vector.app.features.createdirect.CreateDirectRoomActivity
|
import im.vector.app.features.createdirect.CreateDirectRoomActivity
|
||||||
import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
||||||
|
import im.vector.app.features.crypto.keysbackup.setup.KeysBackupSetupActivity
|
||||||
import im.vector.app.features.crypto.quads.SharedSecureStorageActivity
|
import im.vector.app.features.crypto.quads.SharedSecureStorageActivity
|
||||||
import im.vector.app.features.crypto.recover.BootstrapBottomSheet
|
import im.vector.app.features.crypto.recover.BootstrapBottomSheet
|
||||||
import im.vector.app.features.crypto.verification.VerificationBottomSheet
|
import im.vector.app.features.crypto.verification.VerificationBottomSheet
|
||||||
@ -138,6 +139,7 @@ interface ScreenComponent {
|
|||||||
fun inject(activity: LinkHandlerActivity)
|
fun inject(activity: LinkHandlerActivity)
|
||||||
fun inject(activity: MainActivity)
|
fun inject(activity: MainActivity)
|
||||||
fun inject(activity: RoomDirectoryActivity)
|
fun inject(activity: RoomDirectoryActivity)
|
||||||
|
fun inject(activity: KeysBackupSetupActivity)
|
||||||
fun inject(activity: BugReportActivity)
|
fun inject(activity: BugReportActivity)
|
||||||
fun inject(activity: FilteredRoomsActivity)
|
fun inject(activity: FilteredRoomsActivity)
|
||||||
fun inject(activity: CreateRoomActivity)
|
fun inject(activity: CreateRoomActivity)
|
||||||
|
@ -18,35 +18,24 @@ package im.vector.app.features.crypto.keys
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import im.vector.app.features.session.coroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
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.internal.extensions.foldToCallback
|
import javax.inject.Inject
|
||||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
|
||||||
|
|
||||||
class KeysExporter(private val session: Session) {
|
|
||||||
|
|
||||||
|
class KeysExporter @Inject constructor(
|
||||||
|
private val session: Session,
|
||||||
|
private val context: Context
|
||||||
|
) {
|
||||||
/**
|
/**
|
||||||
* Export keys and return the file path with the callback
|
* Export keys and write them to the provided uri
|
||||||
*/
|
*/
|
||||||
fun export(context: Context, password: String, uri: Uri, callback: MatrixCallback<Boolean>) {
|
suspend fun export(password: String, uri: Uri) {
|
||||||
session.coroutineScope.launch(Dispatchers.Main) {
|
return withContext(Dispatchers.IO) {
|
||||||
runCatching {
|
val data = session.cryptoService().exportRoomKeys(password)
|
||||||
withContext(Dispatchers.IO) {
|
context.contentResolver.openOutputStream(uri)
|
||||||
val data = awaitCallback<ByteArray> { session.cryptoService().exportRoomKeys(password, it) }
|
?.use { it.write(data) }
|
||||||
val os = context.contentResolver?.openOutputStream(uri)
|
?: throw IllegalStateException("Unable to open file for writting")
|
||||||
if (os == null) {
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
os.write(data)
|
|
||||||
os.flush()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.foldToCallback(callback)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,49 +20,27 @@ import android.content.Context
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import im.vector.app.core.intent.getMimeTypeFromUri
|
import im.vector.app.core.intent.getMimeTypeFromUri
|
||||||
import im.vector.app.core.resources.openResource
|
import im.vector.app.core.resources.openResource
|
||||||
import im.vector.app.features.session.coroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
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.internal.crypto.model.ImportRoomKeysResult
|
import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult
|
||||||
import org.matrix.android.sdk.internal.extensions.foldToCallback
|
import javax.inject.Inject
|
||||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
class KeysImporter(private val session: Session) {
|
|
||||||
|
|
||||||
|
class KeysImporter @Inject constructor(
|
||||||
|
private val context: Context,
|
||||||
|
private val session: Session
|
||||||
|
) {
|
||||||
/**
|
/**
|
||||||
* Import keys from provided Uri
|
* Import keys from provided Uri
|
||||||
*/
|
*/
|
||||||
fun import(context: Context,
|
suspend fun import(uri: Uri,
|
||||||
uri: Uri,
|
mimetype: String?,
|
||||||
mimetype: String?,
|
password: String): ImportRoomKeysResult {
|
||||||
password: String,
|
return withContext(Dispatchers.IO) {
|
||||||
callback: MatrixCallback<ImportRoomKeysResult>) {
|
val resource = openResource(context, uri, mimetype ?: getMimeTypeFromUri(context, uri))
|
||||||
session.coroutineScope.launch(Dispatchers.Main) {
|
val stream = resource?.mContentStream ?: throw Exception("Error")
|
||||||
runCatching {
|
val data = stream.use { it.readBytes() }
|
||||||
withContext(Dispatchers.IO) {
|
session.cryptoService().importRoomKeys(data, password, null)
|
||||||
val resource = openResource(context, uri, mimetype ?: getMimeTypeFromUri(context, uri))
|
|
||||||
|
|
||||||
if (resource?.mContentStream == null) {
|
|
||||||
throw Exception("Error")
|
|
||||||
}
|
|
||||||
|
|
||||||
val data: ByteArray
|
|
||||||
try {
|
|
||||||
data = resource.mContentStream!!.use { it.readBytes() }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "## importKeys()")
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
|
|
||||||
awaitCallback<ImportRoomKeysResult> {
|
|
||||||
session.cryptoService().importRoomKeys(data, password, null, it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.foldToCallback(callback)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,10 +18,13 @@ package im.vector.app.features.crypto.keysbackup.setup
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.di.ScreenComponent
|
||||||
import im.vector.app.core.dialogs.ExportKeysDialog
|
import im.vector.app.core.dialogs.ExportKeysDialog
|
||||||
import im.vector.app.core.extensions.observeEvent
|
import im.vector.app.core.extensions.observeEvent
|
||||||
import im.vector.app.core.extensions.queryExportKeys
|
import im.vector.app.core.extensions.queryExportKeys
|
||||||
@ -30,7 +33,8 @@ import im.vector.app.core.extensions.replaceFragment
|
|||||||
import im.vector.app.core.platform.SimpleFragmentActivity
|
import im.vector.app.core.platform.SimpleFragmentActivity
|
||||||
import im.vector.app.core.utils.toast
|
import im.vector.app.core.utils.toast
|
||||||
import im.vector.app.features.crypto.keys.KeysExporter
|
import im.vector.app.features.crypto.keys.KeysExporter
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class KeysBackupSetupActivity : SimpleFragmentActivity() {
|
class KeysBackupSetupActivity : SimpleFragmentActivity() {
|
||||||
|
|
||||||
@ -38,6 +42,13 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
|
|||||||
|
|
||||||
private lateinit var viewModel: KeysBackupSetupSharedViewModel
|
private lateinit var viewModel: KeysBackupSetupSharedViewModel
|
||||||
|
|
||||||
|
@Inject lateinit var keysExporter: KeysExporter
|
||||||
|
|
||||||
|
override fun injectWith(injector: ScreenComponent) {
|
||||||
|
super.injectWith(injector)
|
||||||
|
injector.inject(this)
|
||||||
|
}
|
||||||
|
|
||||||
override fun initUiAndData() {
|
override fun initUiAndData() {
|
||||||
super.initUiAndData()
|
super.initUiAndData()
|
||||||
if (isFirstCreation()) {
|
if (isFirstCreation()) {
|
||||||
@ -132,30 +143,7 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
|
|||||||
ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener {
|
ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener {
|
||||||
override fun onPassphrase(passphrase: String) {
|
override fun onPassphrase(passphrase: String) {
|
||||||
showWaitingView()
|
showWaitingView()
|
||||||
|
export(passphrase, uri)
|
||||||
KeysExporter(session)
|
|
||||||
.export(this@KeysBackupSetupActivity,
|
|
||||||
passphrase,
|
|
||||||
uri,
|
|
||||||
object : MatrixCallback<Boolean> {
|
|
||||||
override fun onSuccess(data: Boolean) {
|
|
||||||
if (data) {
|
|
||||||
toast(getString(R.string.encryption_exported_successfully))
|
|
||||||
Intent().apply {
|
|
||||||
putExtra(MANUAL_EXPORT, true)
|
|
||||||
}.let {
|
|
||||||
setResult(Activity.RESULT_OK, it)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hideWaitingView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
|
||||||
toast(failure.localizedMessage ?: getString(R.string.unexpected_error))
|
|
||||||
hideWaitingView()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@ -165,6 +153,20 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun export(passphrase: String, uri: Uri) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
keysExporter.export(passphrase, uri)
|
||||||
|
toast(getString(R.string.encryption_exported_successfully))
|
||||||
|
setResult(Activity.RESULT_OK, Intent().apply { putExtra(MANUAL_EXPORT, true) })
|
||||||
|
finish()
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
toast(failure.localizedMessage ?: getString(R.string.unexpected_error))
|
||||||
|
}
|
||||||
|
hideWaitingView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
if (viewModel.shouldPromptOnBack) {
|
if (viewModel.shouldPromptOnBack) {
|
||||||
if (waitingView?.isVisible == true) {
|
if (waitingView?.isVisible == true) {
|
||||||
|
@ -20,6 +20,7 @@ package im.vector.app.features.settings
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@ -60,11 +61,11 @@ import im.vector.app.features.raw.wellknown.isE2EByDefault
|
|||||||
import im.vector.app.features.themes.ThemeUtils
|
import im.vector.app.features.themes.ThemeUtils
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.disposables.Disposable
|
import io.reactivex.disposables.Disposable
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import me.gujun.android.span.span
|
import me.gujun.android.span.span
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
import org.matrix.android.sdk.api.MatrixCallback
|
||||||
import org.matrix.android.sdk.api.extensions.getFingerprintHumanReadable
|
import org.matrix.android.sdk.api.extensions.getFingerprintHumanReadable
|
||||||
import org.matrix.android.sdk.internal.crypto.crosssigning.isVerified
|
import org.matrix.android.sdk.internal.crypto.crosssigning.isVerified
|
||||||
import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse
|
import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse
|
||||||
import org.matrix.android.sdk.rx.SecretsSynchronisationInfo
|
import org.matrix.android.sdk.rx.SecretsSynchronisationInfo
|
||||||
@ -75,6 +76,8 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
|||||||
private val vectorPreferences: VectorPreferences,
|
private val vectorPreferences: VectorPreferences,
|
||||||
private val activeSessionHolder: ActiveSessionHolder,
|
private val activeSessionHolder: ActiveSessionHolder,
|
||||||
private val pinCodeStore: PinCodeStore,
|
private val pinCodeStore: PinCodeStore,
|
||||||
|
private val keysExporter: KeysExporter,
|
||||||
|
private val keysImporter: KeysImporter,
|
||||||
private val navigator: Navigator
|
private val navigator: Navigator
|
||||||
) : VectorSettingsBaseFragment() {
|
) : VectorSettingsBaseFragment() {
|
||||||
|
|
||||||
@ -320,29 +323,24 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
|||||||
override fun onPassphrase(passphrase: String) {
|
override fun onPassphrase(passphrase: String) {
|
||||||
displayLoadingView()
|
displayLoadingView()
|
||||||
|
|
||||||
KeysExporter(session)
|
export(passphrase, uri)
|
||||||
.export(requireContext(),
|
|
||||||
passphrase,
|
|
||||||
uri,
|
|
||||||
object : MatrixCallback<Boolean> {
|
|
||||||
override fun onSuccess(data: Boolean) {
|
|
||||||
if (data) {
|
|
||||||
requireActivity().toast(getString(R.string.encryption_exported_successfully))
|
|
||||||
} else {
|
|
||||||
requireActivity().toast(getString(R.string.unexpected_error))
|
|
||||||
}
|
|
||||||
hideLoadingView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
|
||||||
onCommonDone(failure.localizedMessage)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun export(passphrase: String, uri: Uri) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
keysExporter.export(passphrase, uri)
|
||||||
|
requireActivity().toast(getString(R.string.encryption_exported_successfully))
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
requireActivity().toast(errorFormatter.toHumanReadable(failure))
|
||||||
|
}
|
||||||
|
hideLoadingView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val pinActivityResultLauncher = registerStartForActivityResult {
|
private val pinActivityResultLauncher = registerStartForActivityResult {
|
||||||
if (it.resultCode == Activity.RESULT_OK) {
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
doOpenPinCodePreferenceScreen()
|
doOpenPinCodePreferenceScreen()
|
||||||
@ -474,34 +472,25 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
|||||||
|
|
||||||
displayLoadingView()
|
displayLoadingView()
|
||||||
|
|
||||||
KeysImporter(session)
|
lifecycleScope.launch {
|
||||||
.import(requireContext(),
|
val data = try {
|
||||||
uri,
|
keysImporter.import(uri, mimetype, password)
|
||||||
mimetype,
|
} catch (failure: Throwable) {
|
||||||
password,
|
appContext.toast(errorFormatter.toHumanReadable(failure))
|
||||||
object : MatrixCallback<ImportRoomKeysResult> {
|
null
|
||||||
override fun onSuccess(data: ImportRoomKeysResult) {
|
}
|
||||||
if (!isAdded) {
|
hideLoadingView()
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
hideLoadingView()
|
|
||||||
|
|
||||||
MaterialAlertDialogBuilder(thisActivity)
|
|
||||||
.setMessage(resources.getQuantityString(R.plurals.encryption_import_room_keys_success,
|
|
||||||
data.successfullyNumberOfImportedKeys,
|
|
||||||
data.successfullyNumberOfImportedKeys,
|
|
||||||
data.totalNumberOfKeys))
|
|
||||||
.setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
|
||||||
appContext.toast(failure.localizedMessage ?: getString(R.string.unexpected_error))
|
|
||||||
hideLoadingView()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
if (data != null) {
|
||||||
|
MaterialAlertDialogBuilder(thisActivity)
|
||||||
|
.setMessage(resources.getQuantityString(R.plurals.encryption_import_room_keys_success,
|
||||||
|
data.successfullyNumberOfImportedKeys,
|
||||||
|
data.successfullyNumberOfImportedKeys,
|
||||||
|
data.totalNumberOfKeys))
|
||||||
|
.setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
importDialog.dismiss()
|
importDialog.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,9 +42,7 @@ import im.vector.app.features.crypto.keysbackup.setup.KeysBackupSetupActivity
|
|||||||
import im.vector.app.features.crypto.recover.BootstrapBottomSheet
|
import im.vector.app.features.crypto.recover.BootstrapBottomSheet
|
||||||
import im.vector.app.features.crypto.recover.SetupMode
|
import im.vector.app.features.crypto.recover.SetupMode
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.MatrixCallback
|
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
// TODO this needs to be refactored to current standard and remove legacy
|
// TODO this needs to be refactored to current standard and remove legacy
|
||||||
@ -113,31 +111,6 @@ class SignOutBottomSheetDialogFragment :
|
|||||||
views.setupMegolmBackupButton.action = {
|
views.setupMegolmBackupButton.action = {
|
||||||
setupBackupActivityResultLauncher.launch(KeysBackupSetupActivity.intent(requireContext(), true))
|
setupBackupActivityResultLauncher.launch(KeysBackupSetupActivity.intent(requireContext(), true))
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.observeViewEvents {
|
|
||||||
when (it) {
|
|
||||||
is SignoutCheckViewModel.ViewEvents.ExportKeys -> {
|
|
||||||
it.exporter
|
|
||||||
.export(requireContext(),
|
|
||||||
it.passphrase,
|
|
||||||
it.uri,
|
|
||||||
object : MatrixCallback<Boolean> {
|
|
||||||
override fun onSuccess(data: Boolean) {
|
|
||||||
if (data) {
|
|
||||||
viewModel.handle(SignoutCheckViewModel.Actions.KeySuccessfullyManuallyExported)
|
|
||||||
} else {
|
|
||||||
viewModel.handle(SignoutCheckViewModel.Actions.KeyExportFailed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
|
||||||
Timber.e("## Failed to export manually keys ${failure.localizedMessage}")
|
|
||||||
viewModel.handle(SignoutCheckViewModel.Actions.KeyExportFailed)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun invalidate() = withState(viewModel) { state ->
|
override fun invalidate() = withState(viewModel) { state ->
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
package im.vector.app.features.workers.signout
|
package im.vector.app.features.workers.signout
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.airbnb.mvrx.ActivityViewModelContext
|
import com.airbnb.mvrx.ActivityViewModelContext
|
||||||
import com.airbnb.mvrx.Async
|
import com.airbnb.mvrx.Async
|
||||||
import com.airbnb.mvrx.FragmentViewModelContext
|
import com.airbnb.mvrx.FragmentViewModelContext
|
||||||
@ -27,13 +28,14 @@ import com.airbnb.mvrx.Success
|
|||||||
import com.airbnb.mvrx.Uninitialized
|
import com.airbnb.mvrx.Uninitialized
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.core.extensions.exhaustive
|
import im.vector.app.core.extensions.exhaustive
|
||||||
import im.vector.app.core.platform.VectorViewEvents
|
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.features.crypto.keys.KeysExporter
|
import im.vector.app.features.crypto.keys.KeysExporter
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
||||||
import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
||||||
@ -41,6 +43,7 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_S
|
|||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
|
||||||
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener
|
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener
|
||||||
import org.matrix.android.sdk.rx.rx
|
import org.matrix.android.sdk.rx.rx
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
data class SignoutCheckViewState(
|
data class SignoutCheckViewState(
|
||||||
val userId: String = "",
|
val userId: String = "",
|
||||||
@ -50,18 +53,15 @@ data class SignoutCheckViewState(
|
|||||||
val hasBeenExportedToFile: Async<Boolean> = Uninitialized
|
val hasBeenExportedToFile: Async<Boolean> = Uninitialized
|
||||||
) : MvRxState
|
) : MvRxState
|
||||||
|
|
||||||
class SignoutCheckViewModel @AssistedInject constructor(@Assisted initialState: SignoutCheckViewState,
|
class SignoutCheckViewModel @AssistedInject constructor(
|
||||||
private val session: Session)
|
@Assisted initialState: SignoutCheckViewState,
|
||||||
: VectorViewModel<SignoutCheckViewState, SignoutCheckViewModel.Actions, SignoutCheckViewModel.ViewEvents>(initialState), KeysBackupStateListener {
|
private val session: Session,
|
||||||
|
private val keysExporter: KeysExporter
|
||||||
|
) : VectorViewModel<SignoutCheckViewState, SignoutCheckViewModel.Actions, EmptyViewEvents>(initialState), KeysBackupStateListener {
|
||||||
|
|
||||||
sealed class Actions : VectorViewModelAction {
|
sealed class Actions : VectorViewModelAction {
|
||||||
data class ExportKeys(val passphrase: String, val uri: Uri) : Actions()
|
data class ExportKeys(val passphrase: String, val uri: Uri) : Actions()
|
||||||
object KeySuccessfullyManuallyExported : Actions()
|
object KeySuccessfullyManuallyExported : Actions()
|
||||||
object KeyExportFailed : Actions()
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class ViewEvents : VectorViewEvents {
|
|
||||||
data class ExportKeys(val exporter: KeysExporter, val passphrase: String, val uri: Uri) : ViewEvents()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
@ -128,22 +128,32 @@ class SignoutCheckViewModel @AssistedInject constructor(@Assisted initialState:
|
|||||||
|
|
||||||
override fun handle(action: Actions) {
|
override fun handle(action: Actions) {
|
||||||
when (action) {
|
when (action) {
|
||||||
is Actions.ExportKeys -> {
|
is Actions.ExportKeys -> handleExportKeys(action)
|
||||||
setState {
|
|
||||||
copy(hasBeenExportedToFile = Loading())
|
|
||||||
}
|
|
||||||
_viewEvents.post(ViewEvents.ExportKeys(KeysExporter(session), action.passphrase, action.uri))
|
|
||||||
}
|
|
||||||
Actions.KeySuccessfullyManuallyExported -> {
|
Actions.KeySuccessfullyManuallyExported -> {
|
||||||
setState {
|
setState {
|
||||||
copy(hasBeenExportedToFile = Success(true))
|
copy(hasBeenExportedToFile = Success(true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Actions.KeyExportFailed -> {
|
|
||||||
setState {
|
|
||||||
copy(hasBeenExportedToFile = Uninitialized)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleExportKeys(action: Actions.ExportKeys) {
|
||||||
|
setState {
|
||||||
|
copy(hasBeenExportedToFile = Loading())
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
val newState = try {
|
||||||
|
keysExporter.export(action.passphrase, action.uri)
|
||||||
|
Success(true)
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
Timber.e("## Failed to export manually keys ${failure.localizedMessage}")
|
||||||
|
Uninitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
setState {
|
||||||
|
copy(hasBeenExportedToFile = newState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user