From dd6c7a8364df18dd97ab82b11fb672068e72b206 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Wed, 2 Dec 2020 17:15:16 +0100 Subject: [PATCH 001/145] Write a test to cover VM call to saveDraft usecase At now, this was unsuccessful because mocking all the fields that are used by saveDraft method on VM gets things complicated Will get back to this in a while or drop it, depending on how the task evolves MAILAND-1018 --- .../api/services/PostMessageServiceFactory.kt | 13 ++- .../android/usecase/compose/SaveDraft.kt | 29 +++++++ .../compose/ComposeMessageViewModelTest.kt | 86 +++++++++++++++++++ 3 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt create mode 100644 app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt diff --git a/app/src/main/java/ch/protonmail/android/api/services/PostMessageServiceFactory.kt b/app/src/main/java/ch/protonmail/android/api/services/PostMessageServiceFactory.kt index cb1a04508..30406a576 100644 --- a/app/src/main/java/ch/protonmail/android/api/services/PostMessageServiceFactory.kt +++ b/app/src/main/java/ch/protonmail/android/api/services/PostMessageServiceFactory.kt @@ -57,7 +57,18 @@ class PostMessageServiceFactory @Inject constructor( private val bgDispatcher: CoroutineDispatcher = Dispatchers.IO - suspend fun startCreateDraftService(messageId: Long, localMessageId: String, parentId: String?, actionType: Constants.MessageActionType, content: String, uploadAttachments: Boolean, newAttachments: List, oldSenderId: String, isTransient: Boolean, username: String = userManager.username) { + suspend fun startCreateDraftService( + messageId: Long, + localMessageId: String, + parentId: String?, + actionType: Constants.MessageActionType, + content: String, + uploadAttachments: Boolean, + newAttachments: List, + oldSenderId: String, + isTransient: Boolean, + username: String = userManager.username + ) { val message = handleMessage(messageId, content, username) ?: return insertPendingDraft(ProtonMailApplication.getApplication(), messageId) handleCreateDraft(message, localMessageId, uploadAttachments, newAttachments, ProtonMailApplication.getApplication()) diff --git a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt new file mode 100644 index 000000000..d8c8cb6e9 --- /dev/null +++ b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * + * This file is part of ProtonMail. + * + * ProtonMail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonMail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonMail. If not, see https://www.gnu.org/licenses/. + */ + +package ch.protonmail.android.usecase.compose + +import ch.protonmail.android.api.models.room.messages.Message + +class SaveDraft { + + suspend operator fun invoke(message: Message) { + + } +} diff --git a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt new file mode 100644 index 000000000..44fc4f1e4 --- /dev/null +++ b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * + * This file is part of ProtonMail. + * + * ProtonMail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonMail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonMail. If not, see https://www.gnu.org/licenses/. + */ + +package ch.protonmail.android.compose + +import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository +import ch.protonmail.android.api.NetworkConfigurator +import ch.protonmail.android.api.models.room.messages.Message +import ch.protonmail.android.api.services.PostMessageServiceFactory +import ch.protonmail.android.core.UserManager +import ch.protonmail.android.usecase.VerifyConnection +import ch.protonmail.android.usecase.compose.SaveDraft +import ch.protonmail.android.usecase.delete.DeleteMessage +import ch.protonmail.android.usecase.fetch.FetchPublicKeys +import io.mockk.coVerify +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.impl.annotations.RelaxedMockK +import io.mockk.junit5.MockKExtension +import kotlinx.coroutines.test.runBlockingTest +import org.junit.Ignore +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(MockKExtension::class) +class ComposeMessageViewModelTest { + + @MockK + lateinit var composeMessageRepository: ComposeMessageRepository + + @RelaxedMockK + lateinit var userManager: UserManager + + @MockK + lateinit var messageDetailsRepository: MessageDetailsRepository + + @MockK + lateinit var postMessageServiceFactory: PostMessageServiceFactory + + @MockK + lateinit var deleteMessage: DeleteMessage + + @MockK + lateinit var fetchPublicKeys: FetchPublicKeys + + @MockK + lateinit var networkConfigurator: NetworkConfigurator + + @MockK + lateinit var verifyConnection: VerifyConnection + + @MockK + lateinit var saveDraft: SaveDraft + + @InjectMockKs + lateinit var viewModel: ComposeMessageViewModel + + @Test + @Ignore("Pending to figure out how to deal with all the class fields that needs to be initialised") + fun saveDraftCallsSaveDraftUseCaseWhenTheDraftIsNew() = + runBlockingTest { + val message = Message() + val parentId = "parentId" + viewModel.prepareMessageData("message title", arrayListOf()) + + viewModel.saveDraft(message, parentId, hasConnectivity = false) + + coVerify { saveDraft(message) } + } +} From 79531d39c257819bd354356826e392f482451cde Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Thu, 3 Dec 2020 12:57:49 +0100 Subject: [PATCH 002/145] SaveDraft saves message with encrypted body locally - Create AddressCrypto.Factory to perfor assisted injection of AddressCrypto - Rename saveMessageInDB to saveMessageLocally and remove dispatchers param (as it's now injected in the repository directly) MAILAND-1018 --- .../repository/MessageDetailsRepository.kt | 14 ++-- .../api/services/PostMessageServiceFactory.kt | 8 +-- .../android/crypto/AddressCrypto.kt | 20 ++++-- .../android/usecase/compose/SaveDraft.kt | 24 ++++++- .../android/usecase/compose/SaveDraftTest.kt | 72 +++++++++++++++++++ 5 files changed, 119 insertions(+), 19 deletions(-) create mode 100644 app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt diff --git a/app/src/main/java/ch/protonmail/android/activities/messageDetails/repository/MessageDetailsRepository.kt b/app/src/main/java/ch/protonmail/android/activities/messageDetails/repository/MessageDetailsRepository.kt index a0993b5dd..06a956df7 100644 --- a/app/src/main/java/ch/protonmail/android/activities/messageDetails/repository/MessageDetailsRepository.kt +++ b/app/src/main/java/ch/protonmail/android/activities/messageDetails/repository/MessageDetailsRepository.kt @@ -52,6 +52,7 @@ import io.reactivex.Flowable import io.reactivex.Single import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext +import me.proton.core.util.kotlin.DispatcherProvider import me.proton.core.util.kotlin.equalsNoCase import timber.log.Timber import java.io.File @@ -72,7 +73,8 @@ class MessageDetailsRepository @Inject constructor( @Named("messages_search") var searchDatabaseDao: MessagesDao, private var pendingActionsDatabase: PendingActionsDao, private val applicationContext: Context, - var databaseProvider: DatabaseProvider + var databaseProvider: DatabaseProvider, + private val dispatchers: DispatcherProvider ) { private var messagesDao: MessagesDao = databaseProvider.provideMessagesDao() @@ -218,12 +220,12 @@ class MessageDetailsRepository @Inject constructor( return messagesDao.saveMessage(message) } - suspend fun saveMessageInDB(message: Message, dispatcher: CoroutineDispatcher): Long = - withContext(dispatcher) { - saveMessageInDB(message) - } + suspend fun saveMessageLocally(message: Message): Long = + withContext(dispatchers.Io) { + saveMessageInDB(message) + } - fun saveAllMessages(messages:List) { + fun saveAllMessages(messages: List) { messages.map(this::saveMessageInDB) } diff --git a/app/src/main/java/ch/protonmail/android/api/services/PostMessageServiceFactory.kt b/app/src/main/java/ch/protonmail/android/api/services/PostMessageServiceFactory.kt index 30406a576..de06fec4d 100644 --- a/app/src/main/java/ch/protonmail/android/api/services/PostMessageServiceFactory.kt +++ b/app/src/main/java/ch/protonmail/android/api/services/PostMessageServiceFactory.kt @@ -104,7 +104,7 @@ class PostMessageServiceFactory @Inject constructor( try { val tct = crypto.encrypt(content, true) message.messageBody = tct.armored - messageDetailsRepository.saveMessageInDB(message, bgDispatcher) + messageDetailsRepository.saveMessageLocally(message) } catch (e: Exception) { Timber.e(e, "handleMessage in PostMessageTask failed") } @@ -119,7 +119,7 @@ class PostMessageServiceFactory @Inject constructor( } val hasAttachment = message.numAttachments >= 1 message.setLabelIDs(listOf(Constants.MessageLocationType.ALL_DRAFT.messageLocationTypeValue.toString(), Constants.MessageLocationType.ALL_MAIL.messageLocationTypeValue.toString(), Constants.MessageLocationType.DRAFT.messageLocationTypeValue.toString())) - messageDetailsRepository.saveMessageInDB(message, bgDispatcher) + messageDetailsRepository.saveMessageLocally(message) if (hasAttachment && uploadAttachments && newAttachments.isNotEmpty()) { insertPendingUpload(context, message.messageId!!) @@ -131,7 +131,7 @@ class PostMessageServiceFactory @Inject constructor( return } message.setLabelIDs(listOf(Constants.MessageLocationType.ALL_DRAFT.messageLocationTypeValue.toString(), Constants.MessageLocationType.ALL_MAIL.messageLocationTypeValue.toString(), Constants.MessageLocationType.DRAFT.messageLocationTypeValue.toString())) - messageDetailsRepository.saveMessageInDB(message, bgDispatcher) + messageDetailsRepository.saveMessageLocally(message) if (uploadAttachments && newAttachments.isNotEmpty()) { insertPendingUpload(context, message.messageId!!) } @@ -149,7 +149,7 @@ class PostMessageServiceFactory @Inject constructor( message.sender = message.sender message.isInline = message.isInline message.parsedHeaders = message.parsedHeaders - messageDetailsRepository.saveMessageInDB(message, bgDispatcher) + messageDetailsRepository.saveMessageLocally(message) insertPendingSend(context, message.messageId, message.dbId) } diff --git a/app/src/main/java/ch/protonmail/android/crypto/AddressCrypto.kt b/app/src/main/java/ch/protonmail/android/crypto/AddressCrypto.kt index 9248ba406..b1ab52414 100644 --- a/app/src/main/java/ch/protonmail/android/crypto/AddressCrypto.kt +++ b/app/src/main/java/ch/protonmail/android/crypto/AddressCrypto.kt @@ -37,21 +37,29 @@ import com.proton.gopenpgp.constants.Constants import com.proton.gopenpgp.crypto.KeyRing import com.proton.gopenpgp.crypto.PlainMessage import com.proton.gopenpgp.crypto.SessionKey +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject import timber.log.Timber import com.proton.gopenpgp.crypto.Crypto as GoOpenPgpCrypto -class AddressCrypto( +class AddressCrypto @AssistedInject constructor( val userManager: UserManager, openPgp: OpenPGP, username: Name, - private val addressId: Id, + @Assisted private val addressId: Id, userMapper: UserBridgeMapper = UserBridgeMapper.buildDefault() ) : Crypto(userManager, openPgp, username, userMapper) { - private val address get() = - // Address here cannot be null - user.addresses.findBy(addressId) - ?: throw IllegalArgumentException("Cannot find an address with given id") + @AssistedInject.Factory + interface Factory { + fun create(addressId: Id): AddressCrypto + } + + private val address + get() = + // Address here cannot be null + user.addresses.findBy(addressId) + ?: throw IllegalArgumentException("Cannot find an address with given id") private val addressKeys: AddressKeys get() = address.keys diff --git a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt index d8c8cb6e9..c00bde995 100644 --- a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt +++ b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt @@ -19,11 +19,29 @@ package ch.protonmail.android.usecase.compose +import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository import ch.protonmail.android.api.models.room.messages.Message +import ch.protonmail.android.crypto.AddressCrypto +import ch.protonmail.android.domain.entity.Id +import kotlinx.coroutines.withContext +import me.proton.core.util.kotlin.DispatcherProvider +import javax.inject.Inject -class SaveDraft { +class SaveDraft @Inject constructor( + private val addressCryptoFactory: AddressCrypto.Factory, + private val messageDetailsRepository: MessageDetailsRepository, + private val dispatchers: DispatcherProvider +) { - suspend operator fun invoke(message: Message) { + suspend operator fun invoke(message: Message) = + withContext(dispatchers.Io) { + val addressId = message.addressID + requireNotNull(addressId) - } + val addressCrypto = addressCryptoFactory.create(Id(addressId)) + val encryptedBody = addressCrypto.encrypt(message.decryptedBody ?: "", true).armored + + message.messageBody = encryptedBody + messageDetailsRepository.saveMessageLocally(message) + } } diff --git a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt new file mode 100644 index 000000000..a2aaa894b --- /dev/null +++ b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * + * This file is part of ProtonMail. + * + * ProtonMail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonMail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonMail. If not, see https://www.gnu.org/licenses/. + */ + +package ch.protonmail.android.usecase.compose + +import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository +import ch.protonmail.android.api.models.room.messages.Message +import ch.protonmail.android.crypto.AddressCrypto +import ch.protonmail.android.domain.entity.Id +import io.mockk.coVerify +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.impl.annotations.RelaxedMockK +import io.mockk.junit5.MockKExtension +import io.mockk.mockk +import kotlinx.coroutines.test.runBlockingTest +import me.proton.core.test.kotlin.CoroutinesTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(MockKExtension::class) +class SaveDraftTest : CoroutinesTest { + + @MockK + private lateinit var addressCryptoFactory: AddressCrypto.Factory + + @RelaxedMockK + lateinit var messageDetailsRepository: MessageDetailsRepository + + @InjectMockKs + lateinit var saveDraft: SaveDraft + + @Test + fun saveDraftSavesEncryptMessageToDb() = + runBlockingTest { + val decryptedMessageBody = "Message body in plain text" + val addressId = "addressId" + val message = Message().apply { + dbId = 123L + addressID = addressId + decryptedBody = decryptedMessageBody + } + val encryptedArmoredBody = "encrypted armored content" + val addressCrypto = mockk { + every { encrypt(decryptedMessageBody, true).armored } returns encryptedArmoredBody + } + every { addressCryptoFactory.create(Id(addressId)) } returns addressCrypto + + saveDraft(message) + + val expectedMessage = message.copy(messageBody = encryptedArmoredBody) + coVerify { messageDetailsRepository.saveMessageLocally(expectedMessage) } + } +} + From f3ac0d2748c6f8e6caaec11a89f77997fed235d5 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Thu, 3 Dec 2020 17:02:44 +0100 Subject: [PATCH 003/145] SaveDraft inserts pending draft in pending actions DB - Remove dispatchers passed as method params, use injected one instead MAILAND-1018 --- .../repository/MessageDetailsRepository.kt | 11 +++++---- .../compose/ComposeMessageViewModel.kt | 9 +++---- .../android/usecase/compose/SaveDraft.kt | 3 ++- .../android/usecase/compose/SaveDraftTest.kt | 24 +++++++++++++++++++ 4 files changed, 36 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/activities/messageDetails/repository/MessageDetailsRepository.kt b/app/src/main/java/ch/protonmail/android/activities/messageDetails/repository/MessageDetailsRepository.kt index 06a956df7..79b7b23d3 100644 --- a/app/src/main/java/ch/protonmail/android/activities/messageDetails/repository/MessageDetailsRepository.kt +++ b/app/src/main/java/ch/protonmail/android/activities/messageDetails/repository/MessageDetailsRepository.kt @@ -463,10 +463,13 @@ class MessageDetailsRepository @Inject constructor( jobManager.addJobInBackground(PostReadJob(listOf(messageId))) } - suspend fun insertPendingDraft(messageDbId: Long, dispatcher: CoroutineDispatcher) = - withContext(dispatcher) { - pendingActionsDatabase.insertPendingDraft(PendingDraft(messageDbId)) - } + /** + * TODO this have nothing to do with MessageDetails, extract to PendingActionsRepository + */ + suspend fun insertPendingDraft(messageDbId: Long) = + withContext(dispatchers.Io) { + pendingActionsDatabase.insertPendingDraft(PendingDraft(messageDbId)) + } fun deletePendingDraft(messageDbId: Long) = pendingActionsDatabase.deletePendingDraftById(messageDbId) diff --git a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt index 83998184e..758c1404d 100644 --- a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt +++ b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt @@ -394,7 +394,9 @@ class ComposeMessageViewModel @Inject constructor( fun insertPendingDraft() { viewModelScope.launch { _dbId?.let { - insertPendingDraft(it, IO) + withContext(IO) { + messageDetailsRepository.insertPendingDraft(it) + } } } } @@ -489,11 +491,6 @@ class ComposeMessageViewModel @Inject constructor( messageDetailsRepository.deletePendingDraft(messageDbId) } - private suspend fun insertPendingDraft(messageDbId: Long, dispatcher: CoroutineDispatcher) = - withContext(dispatcher) { - messageDetailsRepository.insertPendingDraft(messageDbId, dispatcher) - } - private suspend fun saveMessage(message: Message, dispatcher: CoroutineDispatcher): Long = withContext(dispatcher) { messageDetailsRepository.saveMessageInDB(message) diff --git a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt index c00bde995..3f33fefc0 100644 --- a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt +++ b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt @@ -42,6 +42,7 @@ class SaveDraft @Inject constructor( val encryptedBody = addressCrypto.encrypt(message.decryptedBody ?: "", true).armored message.messageBody = encryptedBody - messageDetailsRepository.saveMessageLocally(message) + val messageId = messageDetailsRepository.saveMessageLocally(message) + messageDetailsRepository.insertPendingDraft(messageId) } } diff --git a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt index a2aaa894b..8f876019d 100644 --- a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt +++ b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt @@ -23,6 +23,7 @@ import ch.protonmail.android.activities.messageDetails.repository.MessageDetails import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.crypto.AddressCrypto import ch.protonmail.android.domain.entity.Id +import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.InjectMockKs @@ -68,5 +69,28 @@ class SaveDraftTest : CoroutinesTest { val expectedMessage = message.copy(messageBody = encryptedArmoredBody) coVerify { messageDetailsRepository.saveMessageLocally(expectedMessage) } } + + @Test + fun saveDraftInsertsPendingDraftInPendingActionsDatabase() = + runBlockingTest { + val decryptedMessageBody = "Message body in plain text" + val addressId = "addressId" + val messageDbId = 123L + val message = Message().apply { + dbId = messageDbId + addressID = addressId + decryptedBody = decryptedMessageBody + } + val encryptedArmoredBody = "encrypted armored content" + val addressCrypto = mockk { + every { encrypt(decryptedMessageBody, true).armored } returns encryptedArmoredBody + } + every { addressCryptoFactory.create(Id(addressId)) } returns addressCrypto + coEvery { messageDetailsRepository.saveMessageLocally(message) } returns messageDbId + + saveDraft(message) + + coVerify { messageDetailsRepository.insertPendingDraft(messageDbId) } + } } From 56a320f6908a2ebd16de62986d16daa21f10e092 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Thu, 3 Dec 2020 17:30:12 +0100 Subject: [PATCH 004/145] SaveDrafts sets message location as DRAFT when saving to DB MAILAND-1018 --- .../android/usecase/compose/SaveDraft.kt | 11 +++++++++ .../android/usecase/compose/SaveDraftTest.kt | 23 +++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt index 3f33fefc0..c9cbd3525 100644 --- a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt +++ b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt @@ -21,6 +21,9 @@ package ch.protonmail.android.usecase.compose import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository import ch.protonmail.android.api.models.room.messages.Message +import ch.protonmail.android.core.Constants.MessageLocationType.ALL_DRAFT +import ch.protonmail.android.core.Constants.MessageLocationType.ALL_MAIL +import ch.protonmail.android.core.Constants.MessageLocationType.DRAFT import ch.protonmail.android.crypto.AddressCrypto import ch.protonmail.android.domain.entity.Id import kotlinx.coroutines.withContext @@ -42,6 +45,14 @@ class SaveDraft @Inject constructor( val encryptedBody = addressCrypto.encrypt(message.decryptedBody ?: "", true).armored message.messageBody = encryptedBody + message.setLabelIDs( + listOf( + ALL_DRAFT.messageLocationTypeValue.toString(), + ALL_MAIL.messageLocationTypeValue.toString(), + DRAFT.messageLocationTypeValue.toString() + ) + ) + val messageId = messageDetailsRepository.saveMessageLocally(message) messageDetailsRepository.insertPendingDraft(messageId) } diff --git a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt index 8f876019d..baaf34be9 100644 --- a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt +++ b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt @@ -21,6 +21,9 @@ package ch.protonmail.android.usecase.compose import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository import ch.protonmail.android.api.models.room.messages.Message +import ch.protonmail.android.core.Constants.MessageLocationType.ALL_DRAFT +import ch.protonmail.android.core.Constants.MessageLocationType.ALL_MAIL +import ch.protonmail.android.core.Constants.MessageLocationType.DRAFT import ch.protonmail.android.crypto.AddressCrypto import ch.protonmail.android.domain.entity.Id import io.mockk.coEvery @@ -49,12 +52,14 @@ class SaveDraftTest : CoroutinesTest { lateinit var saveDraft: SaveDraft @Test - fun saveDraftSavesEncryptMessageToDb() = + fun saveDraftSavesEncryptedDraftMessageToDb() = runBlockingTest { + // Given val decryptedMessageBody = "Message body in plain text" val addressId = "addressId" + val messageDbId = 123L val message = Message().apply { - dbId = 123L + dbId = messageDbId addressID = addressId decryptedBody = decryptedMessageBody } @@ -63,16 +68,27 @@ class SaveDraftTest : CoroutinesTest { every { encrypt(decryptedMessageBody, true).armored } returns encryptedArmoredBody } every { addressCryptoFactory.create(Id(addressId)) } returns addressCrypto + coEvery { messageDetailsRepository.saveMessageLocally(message) } returns messageDbId + // When saveDraft(message) + // Then val expectedMessage = message.copy(messageBody = encryptedArmoredBody) + expectedMessage.setLabelIDs( + listOf( + ALL_DRAFT.messageLocationTypeValue.toString(), + ALL_MAIL.messageLocationTypeValue.toString(), + DRAFT.messageLocationTypeValue.toString() + ) + ) coVerify { messageDetailsRepository.saveMessageLocally(expectedMessage) } } @Test fun saveDraftInsertsPendingDraftInPendingActionsDatabase() = runBlockingTest { + // Given val decryptedMessageBody = "Message body in plain text" val addressId = "addressId" val messageDbId = 123L @@ -88,9 +104,12 @@ class SaveDraftTest : CoroutinesTest { every { addressCryptoFactory.create(Id(addressId)) } returns addressCrypto coEvery { messageDetailsRepository.saveMessageLocally(message) } returns messageDbId + // When saveDraft(message) + // Then coVerify { messageDetailsRepository.insertPendingDraft(messageDbId) } } + } From 42db3f6c2932ef95abaaee0fcd00bbed25614e0e Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Fri, 4 Dec 2020 12:58:21 +0100 Subject: [PATCH 005/145] SaveDraft sets new attachments as pending for uploading - Rename filterUploadedAttachments method to clarify intent MAILAND-1018 --- .../compose/ComposeMessageViewModel.kt | 6 +- .../android/usecase/compose/SaveDraft.kt | 21 ++++-- .../compose/ComposeMessageViewModelTest.kt | 2 +- .../android/usecase/compose/SaveDraftTest.kt | 64 +++++++++++++++++-- 4 files changed, 79 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt index 758c1404d..fa5acdb64 100644 --- a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt +++ b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt @@ -339,7 +339,7 @@ class ComposeMessageViewModel @Inject constructor( } } - private fun saveAttachmentsToDatabase( + private fun filterUploadedAttachments( localAttachments: List, uploadAttachments: Boolean ): List { @@ -447,7 +447,7 @@ class ComposeMessageViewModel @Inject constructor( if (uploadAttachments && listOfAttachments.isNotEmpty()) { message.numAttachments = listOfAttachments.size saveMessage(message, IO) - newAttachments = saveAttachmentsToDatabase( + newAttachments = filterUploadedAttachments( composeMessageRepository.createAttachmentList(_messageDataResult.attachmentList, IO), uploadAttachments ) @@ -477,7 +477,7 @@ class ComposeMessageViewModel @Inject constructor( // we need to compare them and find out which are new attachments if (uploadAttachments && localAttachmentsList.isNotEmpty()) { - newAttachments = saveAttachmentsToDatabase( + newAttachments = filterUploadedAttachments( composeMessageRepository.createAttachmentList(localAttachmentsList, IO), uploadAttachments ) } diff --git a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt index c9cbd3525..b4f038bd4 100644 --- a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt +++ b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt @@ -20,7 +20,10 @@ package ch.protonmail.android.usecase.compose import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository +import ch.protonmail.android.api.models.room.messages.Attachment import ch.protonmail.android.api.models.room.messages.Message +import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDao +import ch.protonmail.android.api.models.room.pendingActions.PendingUpload import ch.protonmail.android.core.Constants.MessageLocationType.ALL_DRAFT import ch.protonmail.android.core.Constants.MessageLocationType.ALL_MAIL import ch.protonmail.android.core.Constants.MessageLocationType.DRAFT @@ -33,13 +36,14 @@ import javax.inject.Inject class SaveDraft @Inject constructor( private val addressCryptoFactory: AddressCrypto.Factory, private val messageDetailsRepository: MessageDetailsRepository, - private val dispatchers: DispatcherProvider + private val dispatchers: DispatcherProvider, + private val pendingActionsDao: PendingActionsDao ) { - suspend operator fun invoke(message: Message) = + suspend operator fun invoke(message: Message, newAttachments: List) = withContext(dispatchers.Io) { - val addressId = message.addressID - requireNotNull(addressId) + val messageId = requireNotNull(message.messageId) + val addressId = requireNotNull(message.addressID) val addressCrypto = addressCryptoFactory.create(Id(addressId)) val encryptedBody = addressCrypto.encrypt(message.decryptedBody ?: "", true).armored @@ -53,7 +57,12 @@ class SaveDraft @Inject constructor( ) ) - val messageId = messageDetailsRepository.saveMessageLocally(message) - messageDetailsRepository.insertPendingDraft(messageId) + val messageDbId = messageDetailsRepository.saveMessageLocally(message) + messageDetailsRepository.insertPendingDraft(messageDbId) + + if (newAttachments.isNotEmpty()) { + pendingActionsDao.insertPendingForUpload(PendingUpload(messageId)) + } + } } diff --git a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt index 44fc4f1e4..03c159af0 100644 --- a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt +++ b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt @@ -81,6 +81,6 @@ class ComposeMessageViewModelTest { viewModel.saveDraft(message, parentId, hasConnectivity = false) - coVerify { saveDraft(message) } + coVerify { saveDraft(message, emptyList()) } } } diff --git a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt index baaf34be9..73ee51f6a 100644 --- a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt +++ b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt @@ -20,7 +20,10 @@ package ch.protonmail.android.usecase.compose import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository +import ch.protonmail.android.api.models.room.messages.Attachment import ch.protonmail.android.api.models.room.messages.Message +import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDao +import ch.protonmail.android.api.models.room.pendingActions.PendingUpload import ch.protonmail.android.core.Constants.MessageLocationType.ALL_DRAFT import ch.protonmail.android.core.Constants.MessageLocationType.ALL_MAIL import ch.protonmail.android.core.Constants.MessageLocationType.DRAFT @@ -30,10 +33,10 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.InjectMockKs -import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.RelaxedMockK import io.mockk.junit5.MockKExtension import io.mockk.mockk +import io.mockk.verify import kotlinx.coroutines.test.runBlockingTest import me.proton.core.test.kotlin.CoroutinesTest import org.junit.jupiter.api.Test @@ -42,7 +45,10 @@ import org.junit.jupiter.api.extension.ExtendWith @ExtendWith(MockKExtension::class) class SaveDraftTest : CoroutinesTest { - @MockK + @RelaxedMockK + private lateinit var pendingActionsDao: PendingActionsDao + + @RelaxedMockK private lateinit var addressCryptoFactory: AddressCrypto.Factory @RelaxedMockK @@ -60,6 +66,7 @@ class SaveDraftTest : CoroutinesTest { val messageDbId = 123L val message = Message().apply { dbId = messageDbId + this.messageId = "456" addressID = addressId decryptedBody = decryptedMessageBody } @@ -71,7 +78,7 @@ class SaveDraftTest : CoroutinesTest { coEvery { messageDetailsRepository.saveMessageLocally(message) } returns messageDbId // When - saveDraft(message) + saveDraft(message, emptyList()) // Then val expectedMessage = message.copy(messageBody = encryptedArmoredBody) @@ -94,6 +101,7 @@ class SaveDraftTest : CoroutinesTest { val messageDbId = 123L val message = Message().apply { dbId = messageDbId + this.messageId = "456" addressID = addressId decryptedBody = decryptedMessageBody } @@ -105,11 +113,59 @@ class SaveDraftTest : CoroutinesTest { coEvery { messageDetailsRepository.saveMessageLocally(message) } returns messageDbId // When - saveDraft(message) + saveDraft(message, emptyList()) // Then coVerify { messageDetailsRepository.insertPendingDraft(messageDbId) } } + @Test + fun saveDraftsInsertsPendingUploadWhenThereAreNewAttachments() = + runBlockingTest { + // Given + val decryptedMessageBody = "Message body in plain text" + val addressId = "addressId" + val messageDbId = 123L + val messageId = "456" + val message = Message().apply { + dbId = messageDbId + this.messageId = messageId + addressID = addressId + decryptedBody = decryptedMessageBody + } + val encryptedArmoredBody = "encrypted armored content" + val addressCrypto = mockk { + every { encrypt(decryptedMessageBody, true).armored } returns encryptedArmoredBody + } + every { addressCryptoFactory.create(Id(addressId)) } returns addressCrypto + coEvery { messageDetailsRepository.saveMessageLocally(message) } returns messageDbId + + // When + val newAttachments = listOf(Attachment()) + saveDraft.invoke(message, newAttachments) + + // Then + verify { pendingActionsDao.insertPendingForUpload(PendingUpload(messageId)) } + } + + @Test + fun saveDraftsDoesNotInsertsPendingUploadWhenThereAreNoNewAttachments() = + runBlockingTest { + // Given + val message = Message().apply { + dbId = 123L + this.messageId = "456" + addressID = "addressId" + decryptedBody = "Message body in plain text" + } + coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 9833L + + // When + saveDraft.invoke(message, emptyList()) + + // Then + verify(exactly = 0) { pendingActionsDao.insertPendingForUpload(any()) } + } + } From bbb80bf4cb10da10c2b9d9eb177435962caab9ff Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Fri, 4 Dec 2020 13:36:40 +0100 Subject: [PATCH 006/145] SaveDrafts use case invokes CreateDraftWorker - Inline variables in tests to reduce "Given" section size MAILAND-1018 --- .../android/usecase/compose/SaveDraft.kt | 5 +- .../android/worker/CreateDraftWorker.kt | 56 +++++++++++++ .../android/usecase/compose/SaveDraftTest.kt | 82 +++++++++++-------- 3 files changed, 106 insertions(+), 37 deletions(-) create mode 100644 app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt diff --git a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt index b4f038bd4..2abce2b29 100644 --- a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt +++ b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt @@ -29,6 +29,7 @@ import ch.protonmail.android.core.Constants.MessageLocationType.ALL_MAIL import ch.protonmail.android.core.Constants.MessageLocationType.DRAFT import ch.protonmail.android.crypto.AddressCrypto import ch.protonmail.android.domain.entity.Id +import ch.protonmail.android.worker.CreateDraftWorker import kotlinx.coroutines.withContext import me.proton.core.util.kotlin.DispatcherProvider import javax.inject.Inject @@ -37,7 +38,8 @@ class SaveDraft @Inject constructor( private val addressCryptoFactory: AddressCrypto.Factory, private val messageDetailsRepository: MessageDetailsRepository, private val dispatchers: DispatcherProvider, - private val pendingActionsDao: PendingActionsDao + private val pendingActionsDao: PendingActionsDao, + private val createDraftWorker: CreateDraftWorker.Enqueuer ) { suspend operator fun invoke(message: Message, newAttachments: List) = @@ -64,5 +66,6 @@ class SaveDraft @Inject constructor( pendingActionsDao.insertPendingForUpload(PendingUpload(messageId)) } + createDraftWorker.enqueue() } } diff --git a/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt b/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt new file mode 100644 index 000000000..43eae89cc --- /dev/null +++ b/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * + * This file is part of ProtonMail. + * + * ProtonMail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonMail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonMail. If not, see https://www.gnu.org/licenses/. + */ + +package ch.protonmail.android.worker + +import android.content.Context +import androidx.hilt.Assisted +import androidx.hilt.work.WorkerInject +import androidx.lifecycle.LiveData +import androidx.work.CoroutineWorker +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkInfo +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import androidx.work.workDataOf +import javax.inject.Inject + +class CreateDraftWorker @WorkerInject constructor( + @Assisted context: Context, + @Assisted params: WorkerParameters +) : CoroutineWorker(context, params) { + + override suspend fun doWork(): Result { + TODO("Not yet implemented") + } + + class Enqueuer @Inject constructor(private val workManager: WorkManager) { + fun enqueue(): LiveData { + + val createDraftRequest = OneTimeWorkRequestBuilder() + .setInputData( + workDataOf() + ).build() + + workManager.enqueue(createDraftRequest) + return workManager.getWorkInfoByIdLiveData(createDraftRequest.id) + } + } + +} diff --git a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt index 73ee51f6a..eba88214c 100644 --- a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt +++ b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt @@ -29,6 +29,7 @@ import ch.protonmail.android.core.Constants.MessageLocationType.ALL_MAIL import ch.protonmail.android.core.Constants.MessageLocationType.DRAFT import ch.protonmail.android.crypto.AddressCrypto import ch.protonmail.android.domain.entity.Id +import ch.protonmail.android.worker.CreateDraftWorker import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every @@ -45,6 +46,9 @@ import org.junit.jupiter.api.extension.ExtendWith @ExtendWith(MockKExtension::class) class SaveDraftTest : CoroutinesTest { + @RelaxedMockK + private lateinit var createDraftEnqueuer: CreateDraftWorker.Enqueuer + @RelaxedMockK private lateinit var pendingActionsDao: PendingActionsDao @@ -61,27 +65,23 @@ class SaveDraftTest : CoroutinesTest { fun saveDraftSavesEncryptedDraftMessageToDb() = runBlockingTest { // Given - val decryptedMessageBody = "Message body in plain text" - val addressId = "addressId" - val messageDbId = 123L val message = Message().apply { - dbId = messageDbId + dbId = 123L this.messageId = "456" - addressID = addressId - decryptedBody = decryptedMessageBody + addressID = "addressId" + decryptedBody = "Message body in plain text" } - val encryptedArmoredBody = "encrypted armored content" val addressCrypto = mockk { - every { encrypt(decryptedMessageBody, true).armored } returns encryptedArmoredBody + every { encrypt("Message body in plain text", true).armored } returns "encrypted armored content" } - every { addressCryptoFactory.create(Id(addressId)) } returns addressCrypto - coEvery { messageDetailsRepository.saveMessageLocally(message) } returns messageDbId + every { addressCryptoFactory.create(Id("addressId")) } returns addressCrypto + coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 123L // When saveDraft(message, emptyList()) // Then - val expectedMessage = message.copy(messageBody = encryptedArmoredBody) + val expectedMessage = message.copy(messageBody = "encrypted armored content") expectedMessage.setLabelIDs( listOf( ALL_DRAFT.messageLocationTypeValue.toString(), @@ -96,56 +96,47 @@ class SaveDraftTest : CoroutinesTest { fun saveDraftInsertsPendingDraftInPendingActionsDatabase() = runBlockingTest { // Given - val decryptedMessageBody = "Message body in plain text" - val addressId = "addressId" - val messageDbId = 123L val message = Message().apply { - dbId = messageDbId + dbId = 123L this.messageId = "456" - addressID = addressId - decryptedBody = decryptedMessageBody + addressID = "addressId" + decryptedBody = "Message body in plain text" } - val encryptedArmoredBody = "encrypted armored content" val addressCrypto = mockk { - every { encrypt(decryptedMessageBody, true).armored } returns encryptedArmoredBody + every { encrypt("Message body in plain text", true).armored } returns "encrypted armored content" } - every { addressCryptoFactory.create(Id(addressId)) } returns addressCrypto - coEvery { messageDetailsRepository.saveMessageLocally(message) } returns messageDbId + every { addressCryptoFactory.create(Id("addressId")) } returns addressCrypto + coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 123L // When saveDraft(message, emptyList()) // Then - coVerify { messageDetailsRepository.insertPendingDraft(messageDbId) } + coVerify { messageDetailsRepository.insertPendingDraft(123L) } } @Test fun saveDraftsInsertsPendingUploadWhenThereAreNewAttachments() = runBlockingTest { // Given - val decryptedMessageBody = "Message body in plain text" - val addressId = "addressId" - val messageDbId = 123L - val messageId = "456" val message = Message().apply { - dbId = messageDbId - this.messageId = messageId - addressID = addressId - decryptedBody = decryptedMessageBody + dbId = 123L + this.messageId = "456" + addressID = "addressId" + decryptedBody = "Message body in plain text" } - val encryptedArmoredBody = "encrypted armored content" val addressCrypto = mockk { - every { encrypt(decryptedMessageBody, true).armored } returns encryptedArmoredBody + every { encrypt("Message body in plain text", true).armored } returns "encrypted armored content" } - every { addressCryptoFactory.create(Id(addressId)) } returns addressCrypto - coEvery { messageDetailsRepository.saveMessageLocally(message) } returns messageDbId + every { addressCryptoFactory.create(Id("addressId")) } returns addressCrypto + coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 123L // When val newAttachments = listOf(Attachment()) saveDraft.invoke(message, newAttachments) // Then - verify { pendingActionsDao.insertPendingForUpload(PendingUpload(messageId)) } + verify { pendingActionsDao.insertPendingForUpload(PendingUpload("456")) } } @Test @@ -166,6 +157,25 @@ class SaveDraftTest : CoroutinesTest { // Then verify(exactly = 0) { pendingActionsDao.insertPendingForUpload(any()) } } - + + @Test + fun saveDraftsSchedulesCreateDraftWorker() = + runBlockingTest { + // Given + val message = Message().apply { + dbId = 123L + this.messageId = "456" + addressID = "addressId" + decryptedBody = "Message body in plain text" + } + coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 9833L + + // When + saveDraft.invoke(message, emptyList()) + + // Then + verify { createDraftEnqueuer.enqueue() } + } + } From 2a4f74d3be60663ff770fb4cdbf1c7d6a4ec1812 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Fri, 4 Dec 2020 17:20:44 +0100 Subject: [PATCH 007/145] ComposeMessageVM calls saveDraft useCase to save draft - Inject dispatchers in VM instead of using static Dispatchers class - Assisted inject for username in AddressCrypto - Remove unused methods from PostMessageServiceFactory MAILAND-1018 --- .../ComposeMessageActivity.java | 2 +- .../api/services/PostMessageServiceFactory.kt | 36 -------------- .../compose/ComposeMessageViewModel.kt | 48 +++++++++---------- .../android/crypto/AddressCrypto.kt | 4 +- .../android/usecase/compose/SaveDraft.kt | 12 +++-- .../compose/ComposeMessageViewModelTest.kt | 25 ++++++---- .../android/usecase/compose/SaveDraftTest.kt | 16 ++++--- 7 files changed, 56 insertions(+), 87 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java b/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java index a82c45f72..513934e27 100644 --- a/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java +++ b/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java @@ -2273,7 +2273,7 @@ public void onChanged(@Nullable Event messageEvent) { // draft fillMessageFromUserInputs(localMessage, true); localMessage.setExpirationTime(0); - composeMessageViewModel.saveDraft(localMessage, composeMessageViewModel.getParentId(), mNetworkUtil.isConnected()); + composeMessageViewModel.saveDraft(localMessage, mNetworkUtil.isConnected()); new Handler(Looper.getMainLooper()).postDelayed(() -> disableSendButton(false), 500); if (userAction == UserAction.SAVE_DRAFT_EXIT) { finishActivity(); diff --git a/app/src/main/java/ch/protonmail/android/api/services/PostMessageServiceFactory.kt b/app/src/main/java/ch/protonmail/android/api/services/PostMessageServiceFactory.kt index de06fec4d..deb27ec2d 100644 --- a/app/src/main/java/ch/protonmail/android/api/services/PostMessageServiceFactory.kt +++ b/app/src/main/java/ch/protonmail/android/api/services/PostMessageServiceFactory.kt @@ -31,12 +31,8 @@ import ch.protonmail.android.core.ProtonMailApplication import ch.protonmail.android.core.QueueNetworkUtil import ch.protonmail.android.core.UserManager import ch.protonmail.android.crypto.Crypto -import ch.protonmail.android.events.DraftCreatedEvent -import ch.protonmail.android.events.Status -import ch.protonmail.android.jobs.CreateAndPostDraftJob import ch.protonmail.android.jobs.UpdateAndPostDraftJob import ch.protonmail.android.jobs.messages.PostMessageJob -import ch.protonmail.android.utils.AppUtil import ch.protonmail.android.utils.ServerTime import com.birbit.android.jobqueue.JobManager import kotlinx.coroutines.CoroutineDispatcher @@ -57,24 +53,6 @@ class PostMessageServiceFactory @Inject constructor( private val bgDispatcher: CoroutineDispatcher = Dispatchers.IO - suspend fun startCreateDraftService( - messageId: Long, - localMessageId: String, - parentId: String?, - actionType: Constants.MessageActionType, - content: String, - uploadAttachments: Boolean, - newAttachments: List, - oldSenderId: String, - isTransient: Boolean, - username: String = userManager.username - ) { - val message = handleMessage(messageId, content, username) ?: return - insertPendingDraft(ProtonMailApplication.getApplication(), messageId) - handleCreateDraft(message, localMessageId, uploadAttachments, newAttachments, ProtonMailApplication.getApplication()) - jobManager.addJobInBackground(CreateAndPostDraftJob(messageId, localMessageId, parentId, actionType, uploadAttachments, newAttachments, oldSenderId, isTransient, username)) - } - fun startUpdateDraftService(messageId: Long, content: String, newAttachments: List, uploadAttachments: Boolean, oldSenderId: String, username: String = userManager.username) { // this is temp fix GlobalScope.launch { @@ -112,20 +90,6 @@ class PostMessageServiceFactory @Inject constructor( return message } - private suspend fun handleCreateDraft(message: Message, localMessageId: String, uploadAttachments: Boolean, newAttachments: List, context: Context) { - if (!networkUtil.isConnected()) { - AppUtil.postEventOnUi(DraftCreatedEvent(message.messageId, localMessageId, null, Status.NO_NETWORK)) - return - } - val hasAttachment = message.numAttachments >= 1 - message.setLabelIDs(listOf(Constants.MessageLocationType.ALL_DRAFT.messageLocationTypeValue.toString(), Constants.MessageLocationType.ALL_MAIL.messageLocationTypeValue.toString(), Constants.MessageLocationType.DRAFT.messageLocationTypeValue.toString())) - messageDetailsRepository.saveMessageLocally(message) - - if (hasAttachment && uploadAttachments && newAttachments.isNotEmpty()) { - insertPendingUpload(context, message.messageId!!) - } - } - private suspend fun handleUpdateDraft(message: Message, uploadAttachments: Boolean, newAttachments: List, context: Context) { if (!networkUtil.isConnected()) { return diff --git a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt index fa5acdb64..2fb1d73c4 100644 --- a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt +++ b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt @@ -53,6 +53,7 @@ import ch.protonmail.android.events.FetchMessageDetailEvent import ch.protonmail.android.events.Status import ch.protonmail.android.jobs.contacts.GetSendPreferenceJob import ch.protonmail.android.usecase.VerifyConnection +import ch.protonmail.android.usecase.compose.SaveDraft import ch.protonmail.android.usecase.delete.DeleteMessage import ch.protonmail.android.usecase.fetch.FetchPublicKeys import ch.protonmail.android.usecase.model.FetchPublicKeysRequest @@ -64,11 +65,11 @@ import ch.protonmail.android.viewmodel.ConnectivityBaseViewModel import com.squareup.otto.Subscribe import io.reactivex.Observable import io.reactivex.Single -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import me.proton.core.util.kotlin.DispatcherProvider import timber.log.Timber import java.util.HashMap import java.util.UUID @@ -89,6 +90,8 @@ class ComposeMessageViewModel @Inject constructor( private val postMessageServiceFactory: PostMessageServiceFactory, private val deleteMessage: DeleteMessage, private val fetchPublicKeys: FetchPublicKeys, + private val saveDraft: SaveDraft, + private val dispatchers: DispatcherProvider, verifyConnection: VerifyConnection, networkConfigurator: NetworkConfigurator ) : ConnectivityBaseViewModel(verifyConnection, networkConfigurator) { @@ -401,18 +404,18 @@ class ComposeMessageViewModel @Inject constructor( } } - fun saveDraft(message: Message, parentId: String?, hasConnectivity: Boolean) { + fun saveDraft(message: Message, hasConnectivity: Boolean) { val uploadAttachments = _messageDataResult.uploadAttachments - GlobalScope.launch { + viewModelScope.launch(dispatchers.Main) { if (_dbId == null) { - _dbId = saveMessage(message, IO) + _dbId = saveMessage(message) message.dbId = _dbId } else { message.dbId = _dbId - saveMessage(message, IO) + saveMessage(message) } - if (!TextUtils.isEmpty(draftId)) { + if (draftId.isNotEmpty()) { if (MessageUtils.isLocalMessageId(_draftId.get()) && hasConnectivity) { return@launch } @@ -435,33 +438,26 @@ class ComposeMessageViewModel @Inject constructor( //region new draft here _savingDraftInProcess.set(true) setOfflineDraftSaved(true) - if (TextUtils.isEmpty(draftId) && TextUtils.isEmpty(message.messageId)) { + if (draftId.isEmpty() && message.messageId.isNullOrEmpty()) { val newDraftId = UUID.randomUUID().toString() _draftId.set(newDraftId) message.messageId = newDraftId - saveMessage(message, IO) + saveMessage(message) watchForMessageSent() } - var newAttachments: List = ArrayList() + var newAttachmentIds: List = ArrayList() val listOfAttachments = ArrayList(message.Attachments) if (uploadAttachments && listOfAttachments.isNotEmpty()) { message.numAttachments = listOfAttachments.size - saveMessage(message, IO) - newAttachments = filterUploadedAttachments( + saveMessage(message) + newAttachmentIds = filterUploadedAttachments( composeMessageRepository.createAttachmentList(_messageDataResult.attachmentList, IO), uploadAttachments ) } - postMessageServiceFactory.startCreateDraftService( - _dbId!!, - _draftId.get(), - parentId, - _actionId, message.decryptedBody ?: "", - uploadAttachments, - newAttachments, - _oldSenderAddressId, - _messageDataResult.isTransient - ) + + saveDraft(message, newAttachmentIds) + _oldSenderAddressId = "" setIsDirty(false) //endregion @@ -491,8 +487,8 @@ class ComposeMessageViewModel @Inject constructor( messageDetailsRepository.deletePendingDraft(messageDbId) } - private suspend fun saveMessage(message: Message, dispatcher: CoroutineDispatcher): Long = - withContext(dispatcher) { + private suspend fun saveMessage(message: Message): Long = + withContext(dispatchers.Io) { messageDetailsRepository.saveMessageInDB(message) } @@ -745,7 +741,7 @@ class ComposeMessageViewModel @Inject constructor( // if db ID is null this means we do not have local DB row of the message we are about to send // and we are saving it. also draftId should be null message.messageId = UUID.randomUUID().toString() - _dbId = saveMessage(message, IO) + _dbId = saveMessage(message) } else { // this will ensure the message get latest message id if it was already saved in a create/update // draft job and also that the message has all the latest edits in between draft saving (creation) @@ -758,7 +754,7 @@ class ComposeMessageViewModel @Inject constructor( } else { message.messageId = _draftId.get() } - saveMessage(message, IO) + saveMessage(message) } } @@ -1255,7 +1251,7 @@ class ComposeMessageViewModel @Inject constructor( @SuppressLint("CheckResult") fun watchForMessageSent() { - if (!TextUtils.isEmpty(_draftId.get())) { + if (_draftId.get().isNotEmpty()) { composeMessageRepository.findMessageByIdObservable(_draftId.get()).toObservable() .subscribeOn(ThreadSchedulers.io()) .observeOn(ThreadSchedulers.main()) diff --git a/app/src/main/java/ch/protonmail/android/crypto/AddressCrypto.kt b/app/src/main/java/ch/protonmail/android/crypto/AddressCrypto.kt index b1ab52414..f50bbdd28 100644 --- a/app/src/main/java/ch/protonmail/android/crypto/AddressCrypto.kt +++ b/app/src/main/java/ch/protonmail/android/crypto/AddressCrypto.kt @@ -45,14 +45,14 @@ import com.proton.gopenpgp.crypto.Crypto as GoOpenPgpCrypto class AddressCrypto @AssistedInject constructor( val userManager: UserManager, openPgp: OpenPGP, - username: Name, + @Assisted username: Name, @Assisted private val addressId: Id, userMapper: UserBridgeMapper = UserBridgeMapper.buildDefault() ) : Crypto(userManager, openPgp, username, userMapper) { @AssistedInject.Factory interface Factory { - fun create(addressId: Id): AddressCrypto + fun create(addressId: Id, username: Name): AddressCrypto } private val address diff --git a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt index 2abce2b29..af162e8e9 100644 --- a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt +++ b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt @@ -20,7 +20,6 @@ package ch.protonmail.android.usecase.compose import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository -import ch.protonmail.android.api.models.room.messages.Attachment import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDao import ch.protonmail.android.api.models.room.pendingActions.PendingUpload @@ -28,7 +27,9 @@ import ch.protonmail.android.core.Constants.MessageLocationType.ALL_DRAFT import ch.protonmail.android.core.Constants.MessageLocationType.ALL_MAIL import ch.protonmail.android.core.Constants.MessageLocationType.DRAFT import ch.protonmail.android.crypto.AddressCrypto +import ch.protonmail.android.di.CurrentUsername import ch.protonmail.android.domain.entity.Id +import ch.protonmail.android.domain.entity.Name import ch.protonmail.android.worker.CreateDraftWorker import kotlinx.coroutines.withContext import me.proton.core.util.kotlin.DispatcherProvider @@ -39,15 +40,16 @@ class SaveDraft @Inject constructor( private val messageDetailsRepository: MessageDetailsRepository, private val dispatchers: DispatcherProvider, private val pendingActionsDao: PendingActionsDao, - private val createDraftWorker: CreateDraftWorker.Enqueuer + private val createDraftWorker: CreateDraftWorker.Enqueuer, + @CurrentUsername private val username: String ) { - suspend operator fun invoke(message: Message, newAttachments: List) = + suspend operator fun invoke(message: Message, newAttachmentIds: List): Unit = withContext(dispatchers.Io) { val messageId = requireNotNull(message.messageId) val addressId = requireNotNull(message.addressID) - val addressCrypto = addressCryptoFactory.create(Id(addressId)) + val addressCrypto = addressCryptoFactory.create(Id(addressId), Name(username)) val encryptedBody = addressCrypto.encrypt(message.decryptedBody ?: "", true).armored message.messageBody = encryptedBody @@ -62,7 +64,7 @@ class SaveDraft @Inject constructor( val messageDbId = messageDetailsRepository.saveMessageLocally(message) messageDetailsRepository.insertPendingDraft(messageDbId) - if (newAttachments.isNotEmpty()) { + if (newAttachmentIds.isNotEmpty()) { pendingActionsDao.insertPendingForUpload(PendingUpload(messageId)) } diff --git a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt index 03c159af0..a1978deed 100644 --- a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt +++ b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt @@ -24,6 +24,7 @@ import ch.protonmail.android.api.NetworkConfigurator import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.api.services.PostMessageServiceFactory import ch.protonmail.android.core.UserManager +import ch.protonmail.android.testAndroid.rx.TrampolineScheduler import ch.protonmail.android.usecase.VerifyConnection import ch.protonmail.android.usecase.compose.SaveDraft import ch.protonmail.android.usecase.delete.DeleteMessage @@ -34,22 +35,29 @@ import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.RelaxedMockK import io.mockk.junit5.MockKExtension import kotlinx.coroutines.test.runBlockingTest -import org.junit.Ignore +import me.proton.core.test.kotlin.CoroutinesTest +import org.junit.Rule import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @ExtendWith(MockKExtension::class) -class ComposeMessageViewModelTest { +class ComposeMessageViewModelTest : CoroutinesTest { - @MockK + @Rule + private val trampolineSchedulerRule = TrampolineScheduler() + + @RelaxedMockK lateinit var composeMessageRepository: ComposeMessageRepository @RelaxedMockK lateinit var userManager: UserManager - @MockK + @RelaxedMockK lateinit var messageDetailsRepository: MessageDetailsRepository + @RelaxedMockK + lateinit var saveDraft: SaveDraft + @MockK lateinit var postMessageServiceFactory: PostMessageServiceFactory @@ -65,21 +73,18 @@ class ComposeMessageViewModelTest { @MockK lateinit var verifyConnection: VerifyConnection - @MockK - lateinit var saveDraft: SaveDraft - @InjectMockKs lateinit var viewModel: ComposeMessageViewModel @Test - @Ignore("Pending to figure out how to deal with all the class fields that needs to be initialised") fun saveDraftCallsSaveDraftUseCaseWhenTheDraftIsNew() = runBlockingTest { val message = Message() val parentId = "parentId" - viewModel.prepareMessageData("message title", arrayListOf()) + // Needed to set a value to _messageDataResult field + viewModel.prepareMessageData(false, "addressId", "mail-alias", false) - viewModel.saveDraft(message, parentId, hasConnectivity = false) + viewModel.saveDraft(message, hasConnectivity = false) coVerify { saveDraft(message, emptyList()) } } diff --git a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt index eba88214c..bd38466e6 100644 --- a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt +++ b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt @@ -20,7 +20,6 @@ package ch.protonmail.android.usecase.compose import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository -import ch.protonmail.android.api.models.room.messages.Attachment import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDao import ch.protonmail.android.api.models.room.pendingActions.PendingUpload @@ -29,6 +28,7 @@ import ch.protonmail.android.core.Constants.MessageLocationType.ALL_MAIL import ch.protonmail.android.core.Constants.MessageLocationType.DRAFT import ch.protonmail.android.crypto.AddressCrypto import ch.protonmail.android.domain.entity.Id +import ch.protonmail.android.domain.entity.Name import ch.protonmail.android.worker.CreateDraftWorker import io.mockk.coEvery import io.mockk.coVerify @@ -47,7 +47,7 @@ import org.junit.jupiter.api.extension.ExtendWith class SaveDraftTest : CoroutinesTest { @RelaxedMockK - private lateinit var createDraftEnqueuer: CreateDraftWorker.Enqueuer + private lateinit var createDraftScheduler: CreateDraftWorker.Enqueuer @RelaxedMockK private lateinit var pendingActionsDao: PendingActionsDao @@ -61,6 +61,8 @@ class SaveDraftTest : CoroutinesTest { @InjectMockKs lateinit var saveDraft: SaveDraft + private val currentUsername = "username" + @Test fun saveDraftSavesEncryptedDraftMessageToDb() = runBlockingTest { @@ -74,7 +76,7 @@ class SaveDraftTest : CoroutinesTest { val addressCrypto = mockk { every { encrypt("Message body in plain text", true).armored } returns "encrypted armored content" } - every { addressCryptoFactory.create(Id("addressId")) } returns addressCrypto + every { addressCryptoFactory.create(Id("addressId"), Name(currentUsername)) } returns addressCrypto coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 123L // When @@ -105,7 +107,7 @@ class SaveDraftTest : CoroutinesTest { val addressCrypto = mockk { every { encrypt("Message body in plain text", true).armored } returns "encrypted armored content" } - every { addressCryptoFactory.create(Id("addressId")) } returns addressCrypto + every { addressCryptoFactory.create(Id("addressId"), Name(currentUsername)) } returns addressCrypto coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 123L // When @@ -128,11 +130,11 @@ class SaveDraftTest : CoroutinesTest { val addressCrypto = mockk { every { encrypt("Message body in plain text", true).armored } returns "encrypted armored content" } - every { addressCryptoFactory.create(Id("addressId")) } returns addressCrypto + every { addressCryptoFactory.create(Id("addressId"), Name(currentUsername)) } returns addressCrypto coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 123L // When - val newAttachments = listOf(Attachment()) + val newAttachments = listOf("attachmentId") saveDraft.invoke(message, newAttachments) // Then @@ -174,7 +176,7 @@ class SaveDraftTest : CoroutinesTest { saveDraft.invoke(message, emptyList()) // Then - verify { createDraftEnqueuer.enqueue() } + verify { createDraftScheduler.enqueue() } } } From 5ddf00ce8597831cccb7acc5f0f10d92df2fa70a Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Mon, 7 Dec 2020 20:08:33 +0100 Subject: [PATCH 008/145] CreateDraftWorker validates params and builds API draft model MAILAND-1018 --- .../models/messages/receive/MessageFactory.kt | 12 +- .../compose/ComposeMessageViewModel.kt | 2 +- .../android/di/ApplicationModule.kt | 10 ++ .../android/jobs/messages/PostMessageJob.java | 2 + .../android/usecase/compose/SaveDraft.kt | 43 ++--- .../android/worker/CreateDraftWorker.kt | 79 ++++++++- .../compose/ComposeMessageViewModelTest.kt | 2 +- .../android/usecase/compose/SaveDraftTest.kt | 12 +- .../android/worker/CreateDraftWorkerTest.kt | 161 ++++++++++++++++++ 9 files changed, 288 insertions(+), 35 deletions(-) create mode 100644 app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt diff --git a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageFactory.kt b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageFactory.kt index acd1d77cb..96b230378 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageFactory.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageFactory.kt @@ -18,6 +18,7 @@ */ package ch.protonmail.android.api.models.messages.receive +import ch.protonmail.android.api.models.NewMessage import ch.protonmail.android.api.models.enumerations.MessageFlag import ch.protonmail.android.api.models.factories.checkIfSet import ch.protonmail.android.api.models.factories.makeInt @@ -26,12 +27,17 @@ import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.core.Constants import ch.protonmail.android.utils.MessageUtils import ch.protonmail.android.utils.extensions.notNull +import javax.inject.Inject -class MessageFactory( - private val attachmentFactory: IAttachmentFactory, - private val messageSenderFactory: IMessageSenderFactory +class MessageFactory @Inject constructor( + private val attachmentFactory: IAttachmentFactory, + private val messageSenderFactory: IMessageSenderFactory ) : IMessageFactory { + override fun createDraftApiMessage(message: Message): NewMessage { + TODO("Not yet implemented") + } + override fun createServerMessage(message: Message): ServerMessage { return message.let { val serverMessage = ServerMessage() diff --git a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt index 2fb1d73c4..725d666c4 100644 --- a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt +++ b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt @@ -456,7 +456,7 @@ class ComposeMessageViewModel @Inject constructor( ) } - saveDraft(message, newAttachmentIds) + saveDraft(message, newAttachmentIds, parentId) _oldSenderAddressId = "" setIsDirty(false) diff --git a/app/src/main/java/ch/protonmail/android/di/ApplicationModule.kt b/app/src/main/java/ch/protonmail/android/di/ApplicationModule.kt index bf8a95fcb..03e85e4f9 100644 --- a/app/src/main/java/ch/protonmail/android/di/ApplicationModule.kt +++ b/app/src/main/java/ch/protonmail/android/di/ApplicationModule.kt @@ -33,6 +33,10 @@ import ch.protonmail.android.api.interceptors.ProtonMailAuthenticator import ch.protonmail.android.api.models.contacts.receive.ContactLabelFactory import ch.protonmail.android.api.models.doh.Proxies import ch.protonmail.android.api.models.factories.IConverterFactory +import ch.protonmail.android.api.models.messages.receive.AttachmentFactory +import ch.protonmail.android.api.models.messages.receive.IAttachmentFactory +import ch.protonmail.android.api.models.messages.receive.IMessageSenderFactory +import ch.protonmail.android.api.models.messages.receive.MessageSenderFactory import ch.protonmail.android.api.models.messages.receive.ServerLabel import ch.protonmail.android.api.models.room.contacts.ContactLabel import ch.protonmail.android.api.segments.event.AlarmReceiver @@ -206,6 +210,12 @@ object ApplicationModule { @Provides fun providesArmorer(): Armorer = OpenPgpArmorer() + + @Provides + fun messageSenderFactory(): IMessageSenderFactory = MessageSenderFactory() + + @Provides + fun attachmentFactory(): IAttachmentFactory = AttachmentFactory() } @Module diff --git a/app/src/main/java/ch/protonmail/android/jobs/messages/PostMessageJob.java b/app/src/main/java/ch/protonmail/android/jobs/messages/PostMessageJob.java index a2a637764..844f90a85 100644 --- a/app/src/main/java/ch/protonmail/android/jobs/messages/PostMessageJob.java +++ b/app/src/main/java/ch/protonmail/android/jobs/messages/PostMessageJob.java @@ -235,6 +235,8 @@ public void onRun() throws Throwable { // create the draft if there was no connectivity previously for execution the create and post draft job // this however should not happen, because the jobs with the same ID are executed serial, // but just in case that there is no any bug on the JobQueue library + + // TODO verify whether this is actually needed or can be done through saveDtaft use case final MessageResponse draftResponse = getApi().createDraft(newMessage); message.setMessageId(draftResponse.getMessageId()); } diff --git a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt index af162e8e9..aee1fe692 100644 --- a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt +++ b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt @@ -44,30 +44,33 @@ class SaveDraft @Inject constructor( @CurrentUsername private val username: String ) { - suspend operator fun invoke(message: Message, newAttachmentIds: List): Unit = - withContext(dispatchers.Io) { - val messageId = requireNotNull(message.messageId) - val addressId = requireNotNull(message.addressID) + suspend operator fun invoke( + message: Message, + newAttachmentIds: List, + parentId: String? + ): Unit = withContext(dispatchers.Io) { + val messageId = requireNotNull(message.messageId) + val addressId = requireNotNull(message.addressID) - val addressCrypto = addressCryptoFactory.create(Id(addressId), Name(username)) - val encryptedBody = addressCrypto.encrypt(message.decryptedBody ?: "", true).armored + val addressCrypto = addressCryptoFactory.create(Id(addressId), Name(username)) + val encryptedBody = addressCrypto.encrypt(message.decryptedBody ?: "", true).armored - message.messageBody = encryptedBody - message.setLabelIDs( - listOf( - ALL_DRAFT.messageLocationTypeValue.toString(), - ALL_MAIL.messageLocationTypeValue.toString(), - DRAFT.messageLocationTypeValue.toString() - ) + message.messageBody = encryptedBody + message.setLabelIDs( + listOf( + ALL_DRAFT.messageLocationTypeValue.toString(), + ALL_MAIL.messageLocationTypeValue.toString(), + DRAFT.messageLocationTypeValue.toString() ) + ) - val messageDbId = messageDetailsRepository.saveMessageLocally(message) - messageDetailsRepository.insertPendingDraft(messageDbId) + val messageDbId = messageDetailsRepository.saveMessageLocally(message) + messageDetailsRepository.insertPendingDraft(messageDbId) - if (newAttachmentIds.isNotEmpty()) { - pendingActionsDao.insertPendingForUpload(PendingUpload(messageId)) - } - - createDraftWorker.enqueue() + if (newAttachmentIds.isNotEmpty()) { + pendingActionsDao.insertPendingForUpload(PendingUpload(messageId)) } + + createDraftWorker.enqueue(message, parentId) + } } diff --git a/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt b/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt index 43eae89cc..4cd8ed06a 100644 --- a/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt +++ b/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt @@ -23,29 +23,100 @@ import android.content.Context import androidx.hilt.Assisted import androidx.hilt.work.WorkerInject import androidx.lifecycle.LiveData +import androidx.work.Constraints import androidx.work.CoroutineWorker +import androidx.work.NetworkType import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkInfo import androidx.work.WorkManager import androidx.work.WorkerParameters import androidx.work.workDataOf +import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository +import ch.protonmail.android.api.models.messages.receive.MessageFactory +import ch.protonmail.android.api.models.room.messages.Message +import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDao import javax.inject.Inject +internal const val KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_DB_ID = "keyCreateDraftMessageDbId" +internal const val KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_LOCAL_ID = "keyCreateDraftMessageLocalId" +internal const val KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_PARENT_ID = "keyCreateDraftMessageParentId" + +internal const val KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_ERROR_ENUM = "keyCreateDraftErrorResult" + +private const val INPUT_MESSAGE_DB_ID_NOT_FOUND = -1L + class CreateDraftWorker @WorkerInject constructor( @Assisted context: Context, - @Assisted params: WorkerParameters + @Assisted params: WorkerParameters, + private val messageDetailsRepository: MessageDetailsRepository, + private val pendingActionsDao: PendingActionsDao, + private val messageFactory: MessageFactory ) : CoroutineWorker(context, params) { + override suspend fun doWork(): Result { - TODO("Not yet implemented") + val message = messageDetailsRepository.findMessageByMessageDbId(getInputMessageDbId()) + ?: return failureWithError(CreateDraftWorkerErrors.MessageNotFound) + + val pendingForSending = pendingActionsDao.findPendingSendByDbId(requireNotNull(message.dbId)) + + if (pendingForSending != null) { + // TODO allow draft to be saved in this case when starting to use SaveDraft use case in PostMessageJob too + // sending already pressed and in process, so no need to create draft, it will be created from the post send job + return failureWithError(CreateDraftWorkerErrors.SendingInProgressError) + } + + // not needed as done by the use case already (setLabels) *************************************************************** +// message.setLocation(Constants.MessageLocationType.DRAFT.getMessageLocationTypeValue()); + + + val draftApiModel = messageFactory.createDraftApiMessage(message) + val parentMessage: Message? = null + val inputParentId = getInputParentId() + inputParentId?.let { + draftApiModel.setParentID(inputParentId); +// draftApiModel.setAction(mActionType.getMessageActionTypeValue()); +// if(!isTransient) { +// parentMessage = getMessageDetailsRepository().findMessageById(mParentId); +// } else { +// parentMessage = getMessageDetailsRepository().findSearchMessageById(mParentId); +// } + } + + return Result.failure() + } + + private fun getInputParentId(): String? { + return inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_PARENT_ID) + } + + private fun failureWithError(error: CreateDraftWorkerErrors): Result { + val errorData = workDataOf(KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_ERROR_ENUM to error.name) + return Result.failure(errorData) + } + + private fun getInputMessageDbId() = + inputData.getLong(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_DB_ID, INPUT_MESSAGE_DB_ID_NOT_FOUND) + + enum class CreateDraftWorkerErrors { + SendingInProgressError, + MessageNotFound } class Enqueuer @Inject constructor(private val workManager: WorkManager) { - fun enqueue(): LiveData { + fun enqueue(message: Message, parentId: String?): LiveData { + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() val createDraftRequest = OneTimeWorkRequestBuilder() + .setConstraints(constraints) .setInputData( - workDataOf() + workDataOf( + KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_DB_ID to message.dbId, + KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_LOCAL_ID to message.messageId, + KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_PARENT_ID to parentId + ) ).build() workManager.enqueue(createDraftRequest) diff --git a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt index a1978deed..6914b907d 100644 --- a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt +++ b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt @@ -86,6 +86,6 @@ class ComposeMessageViewModelTest : CoroutinesTest { viewModel.saveDraft(message, hasConnectivity = false) - coVerify { saveDraft(message, emptyList()) } + coVerify { saveDraft(message, emptyList(), "parentId") } } } diff --git a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt index bd38466e6..c14af7fce 100644 --- a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt +++ b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt @@ -80,7 +80,7 @@ class SaveDraftTest : CoroutinesTest { coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 123L // When - saveDraft(message, emptyList()) + saveDraft(message, emptyList(), null) // Then val expectedMessage = message.copy(messageBody = "encrypted armored content") @@ -111,7 +111,7 @@ class SaveDraftTest : CoroutinesTest { coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 123L // When - saveDraft(message, emptyList()) + saveDraft(message, emptyList(), null) // Then coVerify { messageDetailsRepository.insertPendingDraft(123L) } @@ -135,7 +135,7 @@ class SaveDraftTest : CoroutinesTest { // When val newAttachments = listOf("attachmentId") - saveDraft.invoke(message, newAttachments) + saveDraft.invoke(message, newAttachments, "parentId") // Then verify { pendingActionsDao.insertPendingForUpload(PendingUpload("456")) } @@ -154,7 +154,7 @@ class SaveDraftTest : CoroutinesTest { coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 9833L // When - saveDraft.invoke(message, emptyList()) + saveDraft.invoke(message, emptyList(), "parentId") // Then verify(exactly = 0) { pendingActionsDao.insertPendingForUpload(any()) } @@ -173,10 +173,10 @@ class SaveDraftTest : CoroutinesTest { coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 9833L // When - saveDraft.invoke(message, emptyList()) + saveDraft.invoke(message, emptyList(), "parentId123") // Then - verify { createDraftScheduler.enqueue() } + verify { createDraftScheduler.enqueue(message, "parentId123") } } } diff --git a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt new file mode 100644 index 000000000..658b59b1a --- /dev/null +++ b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * + * This file is part of ProtonMail. + * + * ProtonMail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonMail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonMail. If not, see https://www.gnu.org/licenses/. + */ + +package ch.protonmail.android.worker + +import android.content.Context +import androidx.work.Data +import androidx.work.ListenableWorker +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository +import ch.protonmail.android.api.models.NewMessage +import ch.protonmail.android.api.models.messages.receive.MessageFactory +import ch.protonmail.android.api.models.room.messages.Message +import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDao +import ch.protonmail.android.api.models.room.pendingActions.PendingSend +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.RelaxedMockK +import io.mockk.junit5.MockKExtension +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import kotlinx.coroutines.test.runBlockingTest +import me.proton.core.test.kotlin.CoroutinesTest +import org.junit.Assert.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(MockKExtension::class) +class CreateDraftWorkerTest : CoroutinesTest { + + + @RelaxedMockK + private lateinit var context: Context + + @RelaxedMockK + private lateinit var parameters: WorkerParameters + + @RelaxedMockK + private lateinit var messageFactory: MessageFactory + + @RelaxedMockK + private lateinit var messageDetailsRepository: MessageDetailsRepository + + @RelaxedMockK + private lateinit var pendingActionsDao: PendingActionsDao + + @RelaxedMockK + private lateinit var workManager: WorkManager + + @InjectMockKs + private lateinit var worker: CreateDraftWorker + + @Test + fun workerEnqueuerCreatesOneTimeRequestWorkerWithParams() { + runBlockingTest { + val messageParentId = "98234" + val messageLocalId = "2834" + val messageDbId = 534L + val message = Message(messageLocalId) + message.dbId = messageDbId + val requestSlot = slot() + every { workManager.enqueue(capture(requestSlot)) } answers { mockk() } + + CreateDraftWorker.Enqueuer(workManager).enqueue(message, messageParentId) + + val constraints = requestSlot.captured.workSpec.constraints + val inputData = requestSlot.captured.workSpec.input + val actualMessageDbId = inputData.getLong(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_DB_ID, -1) + val actualMessageLocalId = inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_LOCAL_ID) + val actualMessageParentId = inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_PARENT_ID) + assertEquals(message.dbId, actualMessageDbId) + assertEquals(message.messageId, actualMessageLocalId) + assertEquals(messageParentId, actualMessageParentId) + assertEquals(NetworkType.CONNECTED, constraints.requiredNetworkType) + verify { workManager.getWorkInfoByIdLiveData(any()) } + } + } + + @Test + fun workerReturnsSendingInProgressErrorWhenMessageIsAlreadyBeingSent() { + runBlockingTest { + val messageDbId = 345L + val message = Message().apply { dbId = messageDbId } + givenMessageIdInput(messageDbId) + every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message + every { pendingActionsDao.findPendingSendByDbId(messageDbId) } returns PendingSend("anyMessageId") + + val result = worker.doWork() + + val error = CreateDraftWorker.CreateDraftWorkerErrors.SendingInProgressError + val expectedFailure = ListenableWorker.Result.failure( + Data.Builder().putString(KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_ERROR_ENUM, error.name).build() + ) + assertEquals(expectedFailure, result) + } + } + + @Test + fun workerReturnsMessageNotFoundErrorWhenMessageDetailsRepositoryDoesNotReturnAValidMessage() { + runBlockingTest { + val messageDbId = 345L + givenMessageIdInput(messageDbId) + every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns null + + val result = worker.doWork() + + val error = CreateDraftWorker.CreateDraftWorkerErrors.MessageNotFound + val expectedFailure = ListenableWorker.Result.failure( + Data.Builder().putString(KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_ERROR_ENUM, error.name).build() + ) + assertEquals(expectedFailure, result) + } + } + + @Test + fun workerCreatesDraftAPIObjectWithParentIdWhenParentIdIsGiven() { + runBlockingTest { + val parentId = "89345" + val messageDbId = 345L + val message = Message().apply { dbId = messageDbId } + val apiDraftMessage = mockk(relaxed = true) + givenMessageIdInput(messageDbId) + givenParentIdInput(parentId) + every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message + every { pendingActionsDao.findPendingSendByDbId(messageDbId) } returns null + every { messageFactory.createDraftApiMessage(message) } answers { apiDraftMessage } + + worker.doWork() + + verify { apiDraftMessage.setParentID(parentId) } + } + } + + private fun givenParentIdInput(parentId: String) { + every { parameters.inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_PARENT_ID) } answers { parentId } + } + + private fun givenMessageIdInput(messageDbId: Long) { + every { parameters.inputData.getLong(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_DB_ID, -1) } answers { messageDbId } + } +} From 1c50a8c972313b240a22a9c4cd2a3074ad09901d Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Mon, 7 Dec 2020 20:09:41 +0100 Subject: [PATCH 009/145] Code formatting for existing classes MAILAND-1018 --- .../messages/receive/AttachmentFactory.kt | 40 +++++++++++++------ .../messages/receive/IAttachmentFactory.kt | 2 - .../messages/receive/IMessageFactory.kt | 6 +-- .../messages/receive/IMessageSenderFactory.kt | 2 - .../models/messages/receive/MessageFactory.kt | 1 + .../messages/receive/MessageSenderFactory.kt | 19 ++++----- 6 files changed, 41 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/AttachmentFactory.kt b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/AttachmentFactory.kt index c4bf365ed..6b6865722 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/AttachmentFactory.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/AttachmentFactory.kt @@ -25,20 +25,34 @@ import ch.protonmail.android.utils.extensions.notNull import ch.protonmail.android.utils.extensions.notNullOrEmpty class AttachmentFactory : IAttachmentFactory { - override fun createServerAttachment(attachment: Attachment): ServerAttachment { - val (attachmentId, fileName, mimeType, fileSize, keyPackets, messageId, isUploaded, isUploading, signature, headers, _, _, _) = attachment + override fun createServerAttachment(attachment: Attachment): ServerAttachment { + val ( + attachmentId, + fileName, + mimeType, + fileSize, + keyPackets, + messageId, + isUploaded, + isUploading, + signature, + headers, + _, + _, + _ + ) = attachment return ServerAttachment( - attachmentId, - fileName, - mimeType, - fileSize, - keyPackets, - messageId, - isUploaded.makeInt(), - isUploading.makeInt(), - signature, - headers) + attachmentId, + fileName, + mimeType, + fileSize, + keyPackets, + messageId, + isUploaded.makeInt(), + isUploading.makeInt(), + signature, + headers) } override fun createAttachment(serverAttachment: ServerAttachment): Attachment { @@ -58,4 +72,4 @@ class AttachmentFactory : IAttachmentFactory { headers = headers ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/IAttachmentFactory.kt b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/IAttachmentFactory.kt index 1b5170825..167e28be3 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/IAttachmentFactory.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/IAttachmentFactory.kt @@ -20,8 +20,6 @@ package ch.protonmail.android.api.models.messages.receive import ch.protonmail.android.api.models.room.messages.Attachment -/** - * Created by Kamil Rajtar on 19.07.18. */ interface IAttachmentFactory{ fun createAttachment(serverAttachment:ServerAttachment):Attachment fun createServerAttachment(attachment:Attachment):ServerAttachment diff --git a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/IMessageFactory.kt b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/IMessageFactory.kt index aeee8b7ff..8c60dcd76 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/IMessageFactory.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/IMessageFactory.kt @@ -18,11 +18,11 @@ */ package ch.protonmail.android.api.models.messages.receive +import ch.protonmail.android.api.models.NewMessage import ch.protonmail.android.api.models.room.messages.Message -/** - * Created by Kamil Rajtar on 18.07.18. */ interface IMessageFactory { fun createMessage(serverMessage:ServerMessage):Message - fun createServerMessage(message:Message):ServerMessage + fun createServerMessage(message: Message): ServerMessage + fun createDraftApiMessage(message: Message): NewMessage } diff --git a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/IMessageSenderFactory.kt b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/IMessageSenderFactory.kt index fa83a0b0b..d8a60948e 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/IMessageSenderFactory.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/IMessageSenderFactory.kt @@ -20,8 +20,6 @@ package ch.protonmail.android.api.models.messages.receive import ch.protonmail.android.api.models.room.messages.MessageSender -/** - * Created by Kamil Rajtar on 25.07.18. */ interface IMessageSenderFactory{ fun createMessageSender(serverMessageSender:ServerMessageSender):MessageSender fun createServerMessageSender(messageSender:MessageSender):ServerMessageSender diff --git a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageFactory.kt b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageFactory.kt index 96b230378..7360a01b7 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageFactory.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageFactory.kt @@ -131,4 +131,5 @@ class MessageFactory @Inject constructor( message } } + } diff --git a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageSenderFactory.kt b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageSenderFactory.kt index bdf473754..41e88b0f7 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageSenderFactory.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageSenderFactory.kt @@ -21,15 +21,16 @@ package ch.protonmail.android.api.models.messages.receive import ch.protonmail.android.api.models.room.messages.MessageSender import ch.protonmail.android.utils.extensions.notNull -class MessageSenderFactory:IMessageSenderFactory { - override fun createServerMessageSender(messageSender:MessageSender):ServerMessageSender { - val (name,emailAddress)=messageSender - return ServerMessageSender(name,emailAddress) +class MessageSenderFactory : IMessageSenderFactory { + + override fun createServerMessageSender(messageSender: MessageSender): ServerMessageSender { + val (name, emailAddress) = messageSender + return ServerMessageSender(name, emailAddress) } - override fun createMessageSender(serverMessageSender:ServerMessageSender):MessageSender { - val name=serverMessageSender.Name - val emailAddress=serverMessageSender.Address.notNull("emailAddress") - return MessageSender(name,emailAddress) + override fun createMessageSender(serverMessageSender: ServerMessageSender): MessageSender { + val name = serverMessageSender.Name + val emailAddress = serverMessageSender.Address.notNull("emailAddress") + return MessageSender(name, emailAddress) } -} \ No newline at end of file +} From 65789051335b72a5ed5ac4c148978ff2cca5b453 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Tue, 8 Dec 2020 13:28:49 +0100 Subject: [PATCH 010/145] Move responsibility to check if message is sending to use case - Worker set sender and messageBody on create draft request --- .../messages/receive/IMessageFactory.kt | 4 +- .../models/messages/receive/MessageFactory.kt | 5 +- .../android/jobs/CreateAndPostDraftJob.java | 3 + .../android/usecase/compose/SaveDraft.kt | 14 ++- .../android/worker/CreateDraftWorker.kt | 42 ++++++--- .../android/usecase/compose/SaveDraftTest.kt | 29 ++++++ .../android/worker/CreateDraftWorkerTest.kt | 90 +++++++++++++------ .../android/domain/entity/user/User.kt | 4 + 8 files changed, 146 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/IMessageFactory.kt b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/IMessageFactory.kt index 8c60dcd76..7bd065416 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/IMessageFactory.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/IMessageFactory.kt @@ -22,7 +22,7 @@ import ch.protonmail.android.api.models.NewMessage import ch.protonmail.android.api.models.room.messages.Message interface IMessageFactory { - fun createMessage(serverMessage:ServerMessage):Message + fun createMessage(serverMessage: ServerMessage): Message fun createServerMessage(message: Message): ServerMessage - fun createDraftApiMessage(message: Message): NewMessage + fun createDraftApiRequest(message: Message): NewMessage } diff --git a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageFactory.kt b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageFactory.kt index 7360a01b7..3f1344bd6 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageFactory.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageFactory.kt @@ -34,9 +34,8 @@ class MessageFactory @Inject constructor( private val messageSenderFactory: IMessageSenderFactory ) : IMessageFactory { - override fun createDraftApiMessage(message: Message): NewMessage { - TODO("Not yet implemented") - } + override fun createDraftApiRequest(message: Message): NewMessage = + NewMessage(createServerMessage(message)) override fun createServerMessage(message: Message): ServerMessage { return message.let { diff --git a/app/src/main/java/ch/protonmail/android/jobs/CreateAndPostDraftJob.java b/app/src/main/java/ch/protonmail/android/jobs/CreateAndPostDraftJob.java index d2a5a8ec6..c41d54cbb 100644 --- a/app/src/main/java/ch/protonmail/android/jobs/CreateAndPostDraftJob.java +++ b/app/src/main/java/ch/protonmail/android/jobs/CreateAndPostDraftJob.java @@ -242,6 +242,9 @@ private void updateAttachmentKeyPackets(List attachmentList, DraftBo } } + /** + * @deprecated replace with UploadAttachments use case + */ private static class PostCreateDraftAttachmentsJob extends ProtonMailBaseJob { private final String mMessageId; private final String mOldMessageId; diff --git a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt index aee1fe692..f326f169e 100644 --- a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt +++ b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt @@ -48,7 +48,7 @@ class SaveDraft @Inject constructor( message: Message, newAttachmentIds: List, parentId: String? - ): Unit = withContext(dispatchers.Io) { + ): Result = withContext(dispatchers.Io) { val messageId = requireNotNull(message.messageId) val addressId = requireNotNull(message.addressID) @@ -71,6 +71,18 @@ class SaveDraft @Inject constructor( pendingActionsDao.insertPendingForUpload(PendingUpload(messageId)) } + pendingActionsDao.findPendingSendByDbId(messageDbId)?.let { + // TODO allow draft to be saved in this case when starting to use SaveDraft use case in PostMessageJob too + return@withContext Result.SendingInProgressError + } + createDraftWorker.enqueue(message, parentId) + + return@withContext Result.Success + } + + sealed class Result { + object SendingInProgressError : Result() + object Success : Result() } } diff --git a/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt b/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt index 4cd8ed06a..4294ac04f 100644 --- a/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt +++ b/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt @@ -34,7 +34,10 @@ import androidx.work.workDataOf import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository import ch.protonmail.android.api.models.messages.receive.MessageFactory import ch.protonmail.android.api.models.room.messages.Message -import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDao +import ch.protonmail.android.api.models.room.messages.MessageSender +import ch.protonmail.android.api.utils.Fields +import ch.protonmail.android.core.UserManager +import ch.protonmail.android.domain.entity.Id import javax.inject.Inject internal const val KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_DB_ID = "keyCreateDraftMessageDbId" @@ -49,8 +52,8 @@ class CreateDraftWorker @WorkerInject constructor( @Assisted context: Context, @Assisted params: WorkerParameters, private val messageDetailsRepository: MessageDetailsRepository, - private val pendingActionsDao: PendingActionsDao, - private val messageFactory: MessageFactory + private val messageFactory: MessageFactory, + private val userManager: UserManager ) : CoroutineWorker(context, params) { @@ -58,23 +61,14 @@ class CreateDraftWorker @WorkerInject constructor( val message = messageDetailsRepository.findMessageByMessageDbId(getInputMessageDbId()) ?: return failureWithError(CreateDraftWorkerErrors.MessageNotFound) - val pendingForSending = pendingActionsDao.findPendingSendByDbId(requireNotNull(message.dbId)) - - if (pendingForSending != null) { - // TODO allow draft to be saved in this case when starting to use SaveDraft use case in PostMessageJob too - // sending already pressed and in process, so no need to create draft, it will be created from the post send job - return failureWithError(CreateDraftWorkerErrors.SendingInProgressError) - } - // not needed as done by the use case already (setLabels) *************************************************************** // message.setLocation(Constants.MessageLocationType.DRAFT.getMessageLocationTypeValue()); - - val draftApiModel = messageFactory.createDraftApiMessage(message) + val createDraftRequest = messageFactory.createDraftApiRequest(message) val parentMessage: Message? = null val inputParentId = getInputParentId() inputParentId?.let { - draftApiModel.setParentID(inputParentId); + createDraftRequest.setParentID(inputParentId); // draftApiModel.setAction(mActionType.getMessageActionTypeValue()); // if(!isTransient) { // parentMessage = getMessageDetailsRepository().findMessageById(mParentId); @@ -83,6 +77,26 @@ class CreateDraftWorker @WorkerInject constructor( // } } + val addressId = requireNotNull(message.addressID) + val encryptedMessage = requireNotNull(message.messageBody) + + // TODO can be deleted as it's duplicated with just reading the message body (access same data) +// if (!TextUtils.isEmpty(message.getMessageId())) { +// Message savedMessage = getMessageDetailsRepository().findMessageById(message.getMessageId()); +// if (savedMessage != null) { +// encryptedMessage = savedMessage.getMessageBody(); +// } +// } + + + val username = userManager.username + val user = userManager.getUser(username).loadNew(username) + user.findAddressById(Id(addressId))?.let { + createDraftRequest.setSender(MessageSender(it.displayName?.s, it.email.s)); + createDraftRequest.addMessageBody(Fields.Message.SELF, encryptedMessage); + } + + return Result.failure() } diff --git a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt index c14af7fce..bdeb48d07 100644 --- a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt +++ b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt @@ -22,6 +22,7 @@ package ch.protonmail.android.usecase.compose import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDao +import ch.protonmail.android.api.models.room.pendingActions.PendingSend import ch.protonmail.android.api.models.room.pendingActions.PendingUpload import ch.protonmail.android.core.Constants.MessageLocationType.ALL_DRAFT import ch.protonmail.android.core.Constants.MessageLocationType.ALL_MAIL @@ -40,6 +41,7 @@ import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.test.runBlockingTest import me.proton.core.test.kotlin.CoroutinesTest +import org.junit.Assert import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -160,6 +162,32 @@ class SaveDraftTest : CoroutinesTest { verify(exactly = 0) { pendingActionsDao.insertPendingForUpload(any()) } } + + @Test + fun sendDraftReturnsSendingInProgressErrorWhenMessageIsAlreadyBeingSent() { + runBlockingTest { + // Given + val messageDbId = 345L + val message = Message().apply { + dbId = messageDbId + this.messageId = "456" + addressID = "addressId" + decryptedBody = "Message body in plain text" + } + every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message + coEvery { messageDetailsRepository.saveMessageLocally(message) } returns messageDbId + every { pendingActionsDao.findPendingSendByDbId(messageDbId) } returns PendingSend("anyMessageId") + + // When + val result = saveDraft.invoke(message, emptyList(), "parentId123") + + // Then + val expectedError = SaveDraft.Result.SendingInProgressError + Assert.assertEquals(expectedError, result) + verify(exactly = 0) { createDraftScheduler.enqueue(any(), any()) } + } + } + @Test fun saveDraftsSchedulesCreateDraftWorker() = runBlockingTest { @@ -171,6 +199,7 @@ class SaveDraftTest : CoroutinesTest { decryptedBody = "Message body in plain text" } coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 9833L + every { pendingActionsDao.findPendingSendByDbId(9833L) } returns null // When saveDraft.invoke(message, emptyList(), "parentId123") diff --git a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt index 658b59b1a..0f6998ea9 100644 --- a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt +++ b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt @@ -30,8 +30,14 @@ import ch.protonmail.android.activities.messageDetails.repository.MessageDetails import ch.protonmail.android.api.models.NewMessage import ch.protonmail.android.api.models.messages.receive.MessageFactory import ch.protonmail.android.api.models.room.messages.Message -import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDao -import ch.protonmail.android.api.models.room.pendingActions.PendingSend +import ch.protonmail.android.api.models.room.messages.MessageSender +import ch.protonmail.android.api.utils.Fields.Message.SELF +import ch.protonmail.android.core.UserManager +import ch.protonmail.android.domain.entity.EmailAddress +import ch.protonmail.android.domain.entity.Id +import ch.protonmail.android.domain.entity.Name +import ch.protonmail.android.domain.entity.user.Address +import ch.protonmail.android.domain.entity.user.AddressKeys import io.mockk.every import io.mockk.impl.annotations.InjectMockKs import io.mockk.impl.annotations.RelaxedMockK @@ -62,10 +68,10 @@ class CreateDraftWorkerTest : CoroutinesTest { private lateinit var messageDetailsRepository: MessageDetailsRepository @RelaxedMockK - private lateinit var pendingActionsDao: PendingActionsDao + private lateinit var workManager: WorkManager @RelaxedMockK - private lateinit var workManager: WorkManager + private lateinit var userManager: UserManager @InjectMockKs private lateinit var worker: CreateDraftWorker @@ -73,6 +79,7 @@ class CreateDraftWorkerTest : CoroutinesTest { @Test fun workerEnqueuerCreatesOneTimeRequestWorkerWithParams() { runBlockingTest { + // Given val messageParentId = "98234" val messageLocalId = "2834" val messageDbId = 534L @@ -81,8 +88,10 @@ class CreateDraftWorkerTest : CoroutinesTest { val requestSlot = slot() every { workManager.enqueue(capture(requestSlot)) } answers { mockk() } + // When CreateDraftWorker.Enqueuer(workManager).enqueue(message, messageParentId) + // Then val constraints = requestSlot.captured.workSpec.constraints val inputData = requestSlot.captured.workSpec.input val actualMessageDbId = inputData.getLong(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_DB_ID, -1) @@ -97,17 +106,18 @@ class CreateDraftWorkerTest : CoroutinesTest { } @Test - fun workerReturnsSendingInProgressErrorWhenMessageIsAlreadyBeingSent() { + fun workerReturnsMessageNotFoundErrorWhenMessageDetailsRepositoryDoesNotReturnAValidMessage() { runBlockingTest { + // Given val messageDbId = 345L - val message = Message().apply { dbId = messageDbId } givenMessageIdInput(messageDbId) - every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message - every { pendingActionsDao.findPendingSendByDbId(messageDbId) } returns PendingSend("anyMessageId") + every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns null + // When val result = worker.doWork() - val error = CreateDraftWorker.CreateDraftWorkerErrors.SendingInProgressError + // Then + val error = CreateDraftWorker.CreateDraftWorkerErrors.MessageNotFound val expectedFailure = ListenableWorker.Result.failure( Data.Builder().putString(KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_ERROR_ENUM, error.name).build() ) @@ -116,38 +126,68 @@ class CreateDraftWorkerTest : CoroutinesTest { } @Test - fun workerReturnsMessageNotFoundErrorWhenMessageDetailsRepositoryDoesNotReturnAValidMessage() { + fun workerSetsParentIdOnCreateDraftRequestWhenParentIdIsGiven() { runBlockingTest { + // Given + val parentId = "89345" val messageDbId = 345L + val message = Message().apply { + dbId = messageDbId + addressID = "addressId" + messageBody = "messageBody" + } + val apiDraftMessage = mockk(relaxed = true) givenMessageIdInput(messageDbId) - every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns null + givenParentIdInput(parentId) + every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message + every { messageFactory.createDraftApiRequest(message) } answers { apiDraftMessage } - val result = worker.doWork() + // When + worker.doWork() - val error = CreateDraftWorker.CreateDraftWorkerErrors.MessageNotFound - val expectedFailure = ListenableWorker.Result.failure( - Data.Builder().putString(KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_ERROR_ENUM, error.name).build() - ) - assertEquals(expectedFailure, result) + // Then + verify { apiDraftMessage.setParentID(parentId) } } } @Test - fun workerCreatesDraftAPIObjectWithParentIdWhenParentIdIsGiven() { + fun workerSetsSenderAndMessageBodyOnCreateDraftRequest() { runBlockingTest { - val parentId = "89345" + // Given val messageDbId = 345L - val message = Message().apply { dbId = messageDbId } + val addressId = "addressId" + val message = Message().apply { + dbId = messageDbId + addressID = addressId + messageBody = "messageBody" + } val apiDraftMessage = mockk(relaxed = true) givenMessageIdInput(messageDbId) - givenParentIdInput(parentId) every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message - every { pendingActionsDao.findPendingSendByDbId(messageDbId) } returns null - every { messageFactory.createDraftApiMessage(message) } answers { apiDraftMessage } - + every { messageFactory.createDraftApiRequest(message) } answers { apiDraftMessage } + every { userManager.username } returns "username" + every { userManager.getUser("username").loadNew("username") } returns mockk { + val address = Address( + Id(addressId), + null, + EmailAddress("sender@email.it"), + Name("senderName"), + true, + Address.Type.ORIGINAL, + allowedToSend = true, + allowedToReceive = false, + keys = AddressKeys(null, emptyList()) + ) + every { findAddressById(Id(addressId)) } returns address + } + + // When worker.doWork() - verify { apiDraftMessage.setParentID(parentId) } + // Then + val messageSender = MessageSender("senderName", "sender@email.it") + verify { apiDraftMessage.setSender(messageSender) } + verify { apiDraftMessage.addMessageBody(SELF, "messageBody") } } } diff --git a/domain/src/main/kotlin/ch/protonmail/android/domain/entity/user/User.kt b/domain/src/main/kotlin/ch/protonmail/android/domain/entity/user/User.kt index 667338578..4e465bda0 100644 --- a/domain/src/main/kotlin/ch/protonmail/android/domain/entity/user/User.kt +++ b/domain/src/main/kotlin/ch/protonmail/android/domain/entity/user/User.kt @@ -86,6 +86,10 @@ data class User( // TODO: consider naming UserInfo or similar get() = with(addresses.primary?.keys?.primaryKey) { this?.signature == null && this?.token == null } + + fun findAddressById(addressId: Id): Address? { + return addresses.addresses.values.find { it.id == addressId } + } } sealed class Delinquent(val i: UInt, val mailRoutesAccessible: Boolean = true) { From ffe6a94157d6059a7ae5430513c1fcb146e7e805 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Tue, 8 Dec 2020 16:24:37 +0100 Subject: [PATCH 011/145] Worker uses message sender to create draft when sending by alias - Avoid reading same data from local DB multiple times - Avoid setting "DRAFT" location again on message (use case did it) - Remove unneeded optional chain in User class - Wrap "is alias" check into User MAILAND-1018 --- .../api/models/room/messages/Message.kt | 12 +++--- .../android/worker/CreateDraftWorker.kt | 38 ++++++++----------- .../android/worker/CreateDraftWorkerTest.kt | 26 +++++++++++++ 3 files changed, 48 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/api/models/room/messages/Message.kt b/app/src/main/java/ch/protonmail/android/api/models/room/messages/Message.kt index 9ae5e5bcd..6fe70d7fa 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/room/messages/Message.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/room/messages/Message.kt @@ -47,7 +47,6 @@ import com.google.gson.annotations.SerializedName import org.apache.commons.lang3.StringEscapeUtils import java.io.Serializable import java.util.ArrayList -import java.util.Arrays import java.util.concurrent.TimeUnit import java.util.regex.Pattern import javax.mail.internet.InternetHeaders @@ -220,11 +219,10 @@ data class Message @JvmOverloads constructor( val replyToEmails: List get() { return replyTos - ?.asSequence() - ?.filterNot { TextUtils.isEmpty(it.address) } - ?.map { it.address } - ?.toList() - ?: Arrays.asList(sender?.emailAddress!!) + .asSequence() + .filterNot { TextUtils.isEmpty(it.address) } + .map { it.address } + .toList() } val toListString get() = MessageUtils.toContactString(toList) @@ -534,6 +532,8 @@ data class Message @JvmOverloads constructor( } } + fun isSenderEmailAlias() = senderEmail.contains("+") + enum class MessageType { INBOX, DRAFT, SENT, INBOX_AND_SENT } diff --git a/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt b/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt index 4294ac04f..71bd50004 100644 --- a/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt +++ b/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt @@ -61,9 +61,6 @@ class CreateDraftWorker @WorkerInject constructor( val message = messageDetailsRepository.findMessageByMessageDbId(getInputMessageDbId()) ?: return failureWithError(CreateDraftWorkerErrors.MessageNotFound) - // not needed as done by the use case already (setLabels) *************************************************************** -// message.setLocation(Constants.MessageLocationType.DRAFT.getMessageLocationTypeValue()); - val createDraftRequest = messageFactory.createDraftApiRequest(message) val parentMessage: Message? = null val inputParentId = getInputParentId() @@ -77,31 +74,25 @@ class CreateDraftWorker @WorkerInject constructor( // } } - val addressId = requireNotNull(message.addressID) val encryptedMessage = requireNotNull(message.messageBody) + createDraftRequest.addMessageBody(Fields.Message.SELF, encryptedMessage); + createDraftRequest.setSender(getMessageSender(message)) - // TODO can be deleted as it's duplicated with just reading the message body (access same data) -// if (!TextUtils.isEmpty(message.getMessageId())) { -// Message savedMessage = getMessageDetailsRepository().findMessageById(message.getMessageId()); -// if (savedMessage != null) { -// encryptedMessage = savedMessage.getMessageBody(); -// } -// } + return Result.failure() + } - val username = userManager.username - val user = userManager.getUser(username).loadNew(username) + private fun getMessageSender(message: Message): MessageSender? { + val addressId = requireNotNull(message.addressID) + val user = userManager.getUser(userManager.username).loadNew(userManager.username) user.findAddressById(Id(addressId))?.let { - createDraftRequest.setSender(MessageSender(it.displayName?.s, it.email.s)); - createDraftRequest.addMessageBody(Fields.Message.SELF, encryptedMessage); + return MessageSender(it.displayName?.s, it.email.s) } - - return Result.failure() - } - - private fun getInputParentId(): String? { - return inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_PARENT_ID) + if (message.isSenderEmailAlias()) { + return MessageSender(message.senderName, message.senderEmail) + } + return null } private fun failureWithError(error: CreateDraftWorkerErrors): Result { @@ -109,11 +100,14 @@ class CreateDraftWorker @WorkerInject constructor( return Result.failure(errorData) } + private fun getInputParentId(): String? { + return inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_PARENT_ID) + } + private fun getInputMessageDbId() = inputData.getLong(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_DB_ID, INPUT_MESSAGE_DB_ID_NOT_FOUND) enum class CreateDraftWorkerErrors { - SendingInProgressError, MessageNotFound } diff --git a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt index 0f6998ea9..e33326222 100644 --- a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt +++ b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt @@ -191,6 +191,32 @@ class CreateDraftWorkerTest : CoroutinesTest { } } + @Test + fun workerUsesMessageSenderToRequestDraftCreationWhenMessageIsBeingSentByAlias() { + runBlockingTest { + // Given + val messageDbId = 89234L + val addressId = "addressId234" + val message = Message().apply { + dbId = messageDbId + addressID = addressId + messageBody = "messageBody2341" + sender = MessageSender("sender by alias", "sender+alias@pm.me") + } + val apiDraftMessage = mockk(relaxed = true) + givenMessageIdInput(messageDbId) + every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message + every { messageFactory.createDraftApiRequest(message) } answers { apiDraftMessage } + + // When + worker.doWork() + + // Then + val messageSender = MessageSender("sender by alias", "sender+alias@pm.me") + verify { apiDraftMessage.setSender(messageSender) } + } + } + private fun givenParentIdInput(parentId: String) { every { parameters.inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_PARENT_ID) } answers { parentId } } From bc2541f9f85cf262b2d63f5f01f79c9962db9b83 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Tue, 8 Dec 2020 17:39:38 +0100 Subject: [PATCH 012/145] Add SaveDraftParameters object to wrap SaveDraft input data The purpose of this is to reduce the burden of changing the input params of this use case, as they will need to change few times across this refactoring MAILAND-1018 --- .../protonmail/android/api/models/User.java | 2 +- .../compose/ComposeMessageViewModel.kt | 2 +- .../android/usecase/compose/SaveDraft.kt | 15 +++++++++----- .../android/worker/CreateDraftWorker.kt | 7 ++++--- .../compose/ComposeMessageViewModelTest.kt | 10 ++++++---- .../android/usecase/compose/SaveDraftTest.kt | 20 +++++++++---------- 6 files changed, 32 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/api/models/User.java b/app/src/main/java/ch/protonmail/android/api/models/User.java index 191c32d09..7178bc68d 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/User.java +++ b/app/src/main/java/ch/protonmail/android/api/models/User.java @@ -676,7 +676,7 @@ public CopyOnWriteArrayList
getAddresses() { @Deprecated @kotlin.Deprecated(message = GENERIC_DEPRECATION_MESSAGE + - "\nfrom: 'addresses.values.find { it.id == addressId }'") + "\nfrom: 'newUser.findAddressById(addressId) }'") public Address getAddressById(String addressId) { tryLoadAddresses(); String addrId = addressId; diff --git a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt index 725d666c4..cf8ab8914 100644 --- a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt +++ b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt @@ -456,7 +456,7 @@ class ComposeMessageViewModel @Inject constructor( ) } - saveDraft(message, newAttachmentIds, parentId) + saveDraft(SaveDraft.SaveDraftParameters(message, newAttachmentIds, parentId)) _oldSenderAddressId = "" setIsDirty(false) diff --git a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt index f326f169e..81a53474a 100644 --- a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt +++ b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt @@ -45,10 +45,9 @@ class SaveDraft @Inject constructor( ) { suspend operator fun invoke( - message: Message, - newAttachmentIds: List, - parentId: String? + params: SaveDraftParameters ): Result = withContext(dispatchers.Io) { + val message = params.message val messageId = requireNotNull(message.messageId) val addressId = requireNotNull(message.addressID) @@ -67,7 +66,7 @@ class SaveDraft @Inject constructor( val messageDbId = messageDetailsRepository.saveMessageLocally(message) messageDetailsRepository.insertPendingDraft(messageDbId) - if (newAttachmentIds.isNotEmpty()) { + if (params.newAttachmentIds.isNotEmpty()) { pendingActionsDao.insertPendingForUpload(PendingUpload(messageId)) } @@ -76,7 +75,7 @@ class SaveDraft @Inject constructor( return@withContext Result.SendingInProgressError } - createDraftWorker.enqueue(message, parentId) + createDraftWorker.enqueue(message, params.parentId) return@withContext Result.Success } @@ -85,4 +84,10 @@ class SaveDraft @Inject constructor( object SendingInProgressError : Result() object Success : Result() } + + data class SaveDraftParameters( + val message: Message, + val newAttachmentIds: List, + val parentId: String? + ) } diff --git a/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt b/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt index 71bd50004..755d28fde 100644 --- a/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt +++ b/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt @@ -83,15 +83,16 @@ class CreateDraftWorker @WorkerInject constructor( } private fun getMessageSender(message: Message): MessageSender? { + if (message.isSenderEmailAlias()) { + return MessageSender(message.senderName, message.senderEmail) + } + val addressId = requireNotNull(message.addressID) val user = userManager.getUser(userManager.username).loadNew(userManager.username) user.findAddressById(Id(addressId))?.let { return MessageSender(it.displayName?.s, it.email.s) } - if (message.isSenderEmailAlias()) { - return MessageSender(message.senderName, message.senderEmail) - } return null } diff --git a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt index 6914b907d..e5b2f641d 100644 --- a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt +++ b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt @@ -23,6 +23,7 @@ import ch.protonmail.android.activities.messageDetails.repository.MessageDetails import ch.protonmail.android.api.NetworkConfigurator import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.api.services.PostMessageServiceFactory +import ch.protonmail.android.core.Constants import ch.protonmail.android.core.UserManager import ch.protonmail.android.testAndroid.rx.TrampolineScheduler import ch.protonmail.android.usecase.VerifyConnection @@ -44,7 +45,7 @@ import org.junit.jupiter.api.extension.ExtendWith class ComposeMessageViewModelTest : CoroutinesTest { @Rule - private val trampolineSchedulerRule = TrampolineScheduler() + val trampolineSchedulerRule = TrampolineScheduler() @RelaxedMockK lateinit var composeMessageRepository: ComposeMessageRepository @@ -80,12 +81,13 @@ class ComposeMessageViewModelTest : CoroutinesTest { fun saveDraftCallsSaveDraftUseCaseWhenTheDraftIsNew() = runBlockingTest { val message = Message() - val parentId = "parentId" - // Needed to set a value to _messageDataResult field + // Needed to set class fields to the right value viewModel.prepareMessageData(false, "addressId", "mail-alias", false) + viewModel.setupComposingNewMessage(false, Constants.MessageActionType.FORWARD, "parentId823", "") viewModel.saveDraft(message, hasConnectivity = false) - coVerify { saveDraft(message, emptyList(), "parentId") } + val parameters = SaveDraft.SaveDraftParameters(message, emptyList(), "parentId823") + coVerify { saveDraft(parameters) } } } diff --git a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt index bdeb48d07..bcab3a7c4 100644 --- a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt +++ b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt @@ -30,6 +30,8 @@ import ch.protonmail.android.core.Constants.MessageLocationType.DRAFT import ch.protonmail.android.crypto.AddressCrypto import ch.protonmail.android.domain.entity.Id import ch.protonmail.android.domain.entity.Name +import ch.protonmail.android.usecase.compose.SaveDraft.Result +import ch.protonmail.android.usecase.compose.SaveDraft.SaveDraftParameters import ch.protonmail.android.worker.CreateDraftWorker import io.mockk.coEvery import io.mockk.coVerify @@ -82,7 +84,7 @@ class SaveDraftTest : CoroutinesTest { coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 123L // When - saveDraft(message, emptyList(), null) + saveDraft(SaveDraftParameters(message, emptyList(), null)) // Then val expectedMessage = message.copy(messageBody = "encrypted armored content") @@ -113,7 +115,7 @@ class SaveDraftTest : CoroutinesTest { coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 123L // When - saveDraft(message, emptyList(), null) + saveDraft(SaveDraftParameters(message, emptyList(), null)) // Then coVerify { messageDetailsRepository.insertPendingDraft(123L) } @@ -137,7 +139,7 @@ class SaveDraftTest : CoroutinesTest { // When val newAttachments = listOf("attachmentId") - saveDraft.invoke(message, newAttachments, "parentId") + saveDraft.invoke(SaveDraftParameters(message, newAttachments, "parentId")) // Then verify { pendingActionsDao.insertPendingForUpload(PendingUpload("456")) } @@ -156,15 +158,14 @@ class SaveDraftTest : CoroutinesTest { coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 9833L // When - saveDraft.invoke(message, emptyList(), "parentId") + saveDraft.invoke(SaveDraftParameters(message, emptyList(), "parentId")) // Then verify(exactly = 0) { pendingActionsDao.insertPendingForUpload(any()) } } - @Test - fun sendDraftReturnsSendingInProgressErrorWhenMessageIsAlreadyBeingSent() { + fun sendDraftReturnsSendingInProgressErrorWhenMessageIsAlreadyBeingSent() = runBlockingTest { // Given val messageDbId = 345L @@ -179,14 +180,13 @@ class SaveDraftTest : CoroutinesTest { every { pendingActionsDao.findPendingSendByDbId(messageDbId) } returns PendingSend("anyMessageId") // When - val result = saveDraft.invoke(message, emptyList(), "parentId123") + val result = saveDraft.invoke(SaveDraftParameters(message, emptyList(), "parentId123")) // Then - val expectedError = SaveDraft.Result.SendingInProgressError + val expectedError = Result.SendingInProgressError Assert.assertEquals(expectedError, result) verify(exactly = 0) { createDraftScheduler.enqueue(any(), any()) } } - } @Test fun saveDraftsSchedulesCreateDraftWorker() = @@ -202,7 +202,7 @@ class SaveDraftTest : CoroutinesTest { every { pendingActionsDao.findPendingSendByDbId(9833L) } returns null // When - saveDraft.invoke(message, emptyList(), "parentId123") + saveDraft.invoke(SaveDraftParameters(message, emptyList(), "parentId123")) // Then verify { createDraftScheduler.enqueue(message, "parentId123") } From e59591da5f39f9943c2c15ef1083e46ead94fc65 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Wed, 9 Dec 2020 12:25:02 +0100 Subject: [PATCH 013/145] CreateDraftWorker reads parent message from messageDetailsDB and sets action on request when parent Id is given Here we are dropping some functionality by reading parent message only from messageDetailsRepository instead of using searchDB when `isTransient` is true. In this instance, isTransient seems to only be true when all the following conditions verify: - Search for a message - Tap on a message _that you previously sent_ from the search result - Perform an action on the message (reply, reply_all, forward) In this case is transient is true and the parent message is read from searchDB. The same message is also available in messageDB at the same point in time, with just the following differencies: SEARCHDB | MESSAGEDETAILSDB - isInline (false | true) - location (2 | 3) - spamScore (0 | -1) - accessTime (1607507146022 | 0) which doesn't seem to create a difference in functionality. Will come back to this decision when knowing more on `isTransient` MAILAND-1018 --- .../compose/ComposeMessageViewModel.kt | 9 +++++- .../android/usecase/compose/SaveDraft.kt | 6 ++-- .../android/worker/CreateDraftWorker.kt | 32 ++++++++++++------- .../compose/ComposeMessageViewModelTest.kt | 7 +++- .../android/usecase/compose/SaveDraftTest.kt | 19 ++++++----- .../android/worker/CreateDraftWorkerTest.kt | 30 +++++++++++++++-- 6 files changed, 76 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt index cf8ab8914..332dc161f 100644 --- a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt +++ b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt @@ -456,7 +456,14 @@ class ComposeMessageViewModel @Inject constructor( ) } - saveDraft(SaveDraft.SaveDraftParameters(message, newAttachmentIds, parentId)) + saveDraft( + SaveDraft.SaveDraftParameters( + message, + newAttachmentIds, + parentId, + _actionId + ) + ) _oldSenderAddressId = "" setIsDirty(false) diff --git a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt index 81a53474a..c5b1c45d7 100644 --- a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt +++ b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt @@ -23,6 +23,7 @@ import ch.protonmail.android.activities.messageDetails.repository.MessageDetails import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDao import ch.protonmail.android.api.models.room.pendingActions.PendingUpload +import ch.protonmail.android.core.Constants import ch.protonmail.android.core.Constants.MessageLocationType.ALL_DRAFT import ch.protonmail.android.core.Constants.MessageLocationType.ALL_MAIL import ch.protonmail.android.core.Constants.MessageLocationType.DRAFT @@ -75,7 +76,7 @@ class SaveDraft @Inject constructor( return@withContext Result.SendingInProgressError } - createDraftWorker.enqueue(message, params.parentId) + createDraftWorker.enqueue(message, params.parentId, params.actionType) return@withContext Result.Success } @@ -88,6 +89,7 @@ class SaveDraft @Inject constructor( data class SaveDraftParameters( val message: Message, val newAttachmentIds: List, - val parentId: String? + val parentId: String?, + val actionType: Constants.MessageActionType ) } diff --git a/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt b/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt index 755d28fde..fc51700a2 100644 --- a/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt +++ b/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt @@ -36,13 +36,17 @@ import ch.protonmail.android.api.models.messages.receive.MessageFactory import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.api.models.room.messages.MessageSender import ch.protonmail.android.api.utils.Fields +import ch.protonmail.android.core.Constants import ch.protonmail.android.core.UserManager import ch.protonmail.android.domain.entity.Id +import ch.protonmail.android.utils.extensions.deserialize +import ch.protonmail.android.utils.extensions.serialize import javax.inject.Inject internal const val KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_DB_ID = "keyCreateDraftMessageDbId" internal const val KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_LOCAL_ID = "keyCreateDraftMessageLocalId" internal const val KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_PARENT_ID = "keyCreateDraftMessageParentId" +internal const val KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_ACTION_TYPE_SERIALIZED = "keyCreateDraftMessageActionTypeSerialized" internal const val KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_ERROR_ENUM = "keyCreateDraftErrorResult" @@ -62,16 +66,11 @@ class CreateDraftWorker @WorkerInject constructor( ?: return failureWithError(CreateDraftWorkerErrors.MessageNotFound) val createDraftRequest = messageFactory.createDraftApiRequest(message) - val parentMessage: Message? = null val inputParentId = getInputParentId() inputParentId?.let { createDraftRequest.setParentID(inputParentId); -// draftApiModel.setAction(mActionType.getMessageActionTypeValue()); -// if(!isTransient) { -// parentMessage = getMessageDetailsRepository().findMessageById(mParentId); -// } else { -// parentMessage = getMessageDetailsRepository().findSearchMessageById(mParentId); -// } + createDraftRequest.action = getInputActionType().messageActionTypeValue + val parentMessage = messageDetailsRepository.findMessageById(inputParentId) } val encryptedMessage = requireNotNull(message.messageBody) @@ -101,9 +100,13 @@ class CreateDraftWorker @WorkerInject constructor( return Result.failure(errorData) } - private fun getInputParentId(): String? { - return inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_PARENT_ID) - } + private fun getInputActionType(): Constants.MessageActionType = + inputData + .getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_ACTION_TYPE_SERIALIZED)?.deserialize() + ?: Constants.MessageActionType.NONE + + private fun getInputParentId(): String? = + inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_PARENT_ID) private fun getInputMessageDbId() = inputData.getLong(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_DB_ID, INPUT_MESSAGE_DB_ID_NOT_FOUND) @@ -114,7 +117,11 @@ class CreateDraftWorker @WorkerInject constructor( class Enqueuer @Inject constructor(private val workManager: WorkManager) { - fun enqueue(message: Message, parentId: String?): LiveData { + fun enqueue( + message: Message, + parentId: String?, + actionType: Constants.MessageActionType + ): LiveData { val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() @@ -124,7 +131,8 @@ class CreateDraftWorker @WorkerInject constructor( workDataOf( KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_DB_ID to message.dbId, KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_LOCAL_ID to message.messageId, - KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_PARENT_ID to parentId + KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_PARENT_ID to parentId, + KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_ACTION_TYPE_SERIALIZED to actionType.serialize() ) ).build() diff --git a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt index e5b2f641d..361af77cb 100644 --- a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt +++ b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt @@ -87,7 +87,12 @@ class ComposeMessageViewModelTest : CoroutinesTest { viewModel.saveDraft(message, hasConnectivity = false) - val parameters = SaveDraft.SaveDraftParameters(message, emptyList(), "parentId823") + val parameters = SaveDraft.SaveDraftParameters( + message, + emptyList(), + "parentId823", + Constants.MessageActionType.FORWARD + ) coVerify { saveDraft(parameters) } } } diff --git a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt index bcab3a7c4..00d6bdf19 100644 --- a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt +++ b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt @@ -24,6 +24,9 @@ import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDao import ch.protonmail.android.api.models.room.pendingActions.PendingSend import ch.protonmail.android.api.models.room.pendingActions.PendingUpload +import ch.protonmail.android.core.Constants.MessageActionType.FORWARD +import ch.protonmail.android.core.Constants.MessageActionType.REPLY +import ch.protonmail.android.core.Constants.MessageActionType.REPLY_ALL import ch.protonmail.android.core.Constants.MessageLocationType.ALL_DRAFT import ch.protonmail.android.core.Constants.MessageLocationType.ALL_MAIL import ch.protonmail.android.core.Constants.MessageLocationType.DRAFT @@ -84,7 +87,7 @@ class SaveDraftTest : CoroutinesTest { coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 123L // When - saveDraft(SaveDraftParameters(message, emptyList(), null)) + saveDraft(SaveDraftParameters(message, emptyList(), null, FORWARD)) // Then val expectedMessage = message.copy(messageBody = "encrypted armored content") @@ -115,7 +118,7 @@ class SaveDraftTest : CoroutinesTest { coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 123L // When - saveDraft(SaveDraftParameters(message, emptyList(), null)) + saveDraft(SaveDraftParameters(message, emptyList(), null, FORWARD)) // Then coVerify { messageDetailsRepository.insertPendingDraft(123L) } @@ -139,7 +142,7 @@ class SaveDraftTest : CoroutinesTest { // When val newAttachments = listOf("attachmentId") - saveDraft.invoke(SaveDraftParameters(message, newAttachments, "parentId")) + saveDraft.invoke(SaveDraftParameters(message, newAttachments, "parentId", REPLY)) // Then verify { pendingActionsDao.insertPendingForUpload(PendingUpload("456")) } @@ -158,7 +161,7 @@ class SaveDraftTest : CoroutinesTest { coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 9833L // When - saveDraft.invoke(SaveDraftParameters(message, emptyList(), "parentId")) + saveDraft.invoke(SaveDraftParameters(message, emptyList(), "parentId", FORWARD)) // Then verify(exactly = 0) { pendingActionsDao.insertPendingForUpload(any()) } @@ -180,12 +183,12 @@ class SaveDraftTest : CoroutinesTest { every { pendingActionsDao.findPendingSendByDbId(messageDbId) } returns PendingSend("anyMessageId") // When - val result = saveDraft.invoke(SaveDraftParameters(message, emptyList(), "parentId123")) + val result = saveDraft.invoke(SaveDraftParameters(message, emptyList(), "parentId123", FORWARD)) // Then val expectedError = Result.SendingInProgressError Assert.assertEquals(expectedError, result) - verify(exactly = 0) { createDraftScheduler.enqueue(any(), any()) } + verify(exactly = 0) { createDraftScheduler.enqueue(any(), any(), any()) } } @Test @@ -202,10 +205,10 @@ class SaveDraftTest : CoroutinesTest { every { pendingActionsDao.findPendingSendByDbId(9833L) } returns null // When - saveDraft.invoke(SaveDraftParameters(message, emptyList(), "parentId123")) + saveDraft.invoke(SaveDraftParameters(message, emptyList(), "parentId123", REPLY_ALL)) // Then - verify { createDraftScheduler.enqueue(message, "parentId123") } + verify { createDraftScheduler.enqueue(message, "parentId123", REPLY_ALL) } } } diff --git a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt index e33326222..ab4fdc7e5 100644 --- a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt +++ b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt @@ -32,12 +32,17 @@ import ch.protonmail.android.api.models.messages.receive.MessageFactory import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.api.models.room.messages.MessageSender import ch.protonmail.android.api.utils.Fields.Message.SELF +import ch.protonmail.android.core.Constants +import ch.protonmail.android.core.Constants.MessageActionType.FORWARD +import ch.protonmail.android.core.Constants.MessageActionType.NONE +import ch.protonmail.android.core.Constants.MessageActionType.REPLY_ALL import ch.protonmail.android.core.UserManager import ch.protonmail.android.domain.entity.EmailAddress import ch.protonmail.android.domain.entity.Id import ch.protonmail.android.domain.entity.Name import ch.protonmail.android.domain.entity.user.Address import ch.protonmail.android.domain.entity.user.AddressKeys +import ch.protonmail.android.utils.extensions.serialize import io.mockk.every import io.mockk.impl.annotations.InjectMockKs import io.mockk.impl.annotations.RelaxedMockK @@ -54,7 +59,6 @@ import org.junit.jupiter.api.extension.ExtendWith @ExtendWith(MockKExtension::class) class CreateDraftWorkerTest : CoroutinesTest { - @RelaxedMockK private lateinit var context: Context @@ -83,13 +87,14 @@ class CreateDraftWorkerTest : CoroutinesTest { val messageParentId = "98234" val messageLocalId = "2834" val messageDbId = 534L + val messageActionType = REPLY_ALL val message = Message(messageLocalId) message.dbId = messageDbId val requestSlot = slot() every { workManager.enqueue(capture(requestSlot)) } answers { mockk() } // When - CreateDraftWorker.Enqueuer(workManager).enqueue(message, messageParentId) + CreateDraftWorker.Enqueuer(workManager).enqueue(message, messageParentId, messageActionType) // Then val constraints = requestSlot.captured.workSpec.constraints @@ -97,9 +102,11 @@ class CreateDraftWorkerTest : CoroutinesTest { val actualMessageDbId = inputData.getLong(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_DB_ID, -1) val actualMessageLocalId = inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_LOCAL_ID) val actualMessageParentId = inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_PARENT_ID) + val actualMessageActionType = inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_ACTION_TYPE_SERIALIZED) assertEquals(message.dbId, actualMessageDbId) assertEquals(message.messageId, actualMessageLocalId) assertEquals(messageParentId, actualMessageParentId) + assertEquals(messageActionType.serialize(), actualMessageActionType) assertEquals(NetworkType.CONNECTED, constraints.requiredNetworkType) verify { workManager.getWorkInfoByIdLiveData(any()) } } @@ -126,10 +133,11 @@ class CreateDraftWorkerTest : CoroutinesTest { } @Test - fun workerSetsParentIdOnCreateDraftRequestWhenParentIdIsGiven() { + fun workerSetsParentIdAndActionTypeOnCreateDraftRequestWhenParentIdIsGiven() { runBlockingTest { // Given val parentId = "89345" + val actionType = FORWARD val messageDbId = 345L val message = Message().apply { dbId = messageDbId @@ -139,6 +147,7 @@ class CreateDraftWorkerTest : CoroutinesTest { val apiDraftMessage = mockk(relaxed = true) givenMessageIdInput(messageDbId) givenParentIdInput(parentId) + givenActionTypeInput(actionType) every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message every { messageFactory.createDraftApiRequest(message) } answers { apiDraftMessage } @@ -147,6 +156,10 @@ class CreateDraftWorkerTest : CoroutinesTest { // Then verify { apiDraftMessage.setParentID(parentId) } + verify { apiDraftMessage.action = 2 } + // Always get parent message from messageDetailsDB, never from searchDB + // ignoring isTransient property as the values in the two DB appears to be the same + verify { messageDetailsRepository.findMessageById(parentId) } } } @@ -163,6 +176,7 @@ class CreateDraftWorkerTest : CoroutinesTest { } val apiDraftMessage = mockk(relaxed = true) givenMessageIdInput(messageDbId) + givenActionTypeInput() every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message every { messageFactory.createDraftApiRequest(message) } answers { apiDraftMessage } every { userManager.username } returns "username" @@ -205,6 +219,7 @@ class CreateDraftWorkerTest : CoroutinesTest { } val apiDraftMessage = mockk(relaxed = true) givenMessageIdInput(messageDbId) + givenActionTypeInput() every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message every { messageFactory.createDraftApiRequest(message) } answers { apiDraftMessage } @@ -217,6 +232,15 @@ class CreateDraftWorkerTest : CoroutinesTest { } } + private fun givenActionTypeInput(actionType: Constants.MessageActionType = NONE) { + every { + parameters.inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_ACTION_TYPE_SERIALIZED) + } answers { + actionType.serialize() + } + + } + private fun givenParentIdInput(parentId: String) { every { parameters.inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_PARENT_ID) } answers { parentId } } From b0a5b8ec29d9853a4651dd951933fa841baed82c Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Thu, 10 Dec 2020 14:43:27 +0100 Subject: [PATCH 014/145] Worker re-encrypts parent attachments when sender address changed MAILAND-1018 --- .../compose/ComposeMessageViewModel.kt | 3 +- .../android/di/ApplicationModule.kt | 5 + .../android/usecase/compose/SaveDraft.kt | 5 +- .../utils/base64/AndroidBase64Encoder.kt | 32 +++++ .../android/utils/base64/Base64Encoder.kt | 25 ++++ .../android/worker/CreateDraftWorker.kt | 88 +++++++++++--- .../compose/ComposeMessageViewModelTest.kt | 4 +- .../android/usecase/compose/SaveDraftTest.kt | 28 +++-- .../android/worker/CreateDraftWorkerTest.kt | 109 +++++++++++++++--- 9 files changed, 258 insertions(+), 41 deletions(-) create mode 100644 app/src/main/java/ch/protonmail/android/utils/base64/AndroidBase64Encoder.kt create mode 100644 app/src/main/java/ch/protonmail/android/utils/base64/Base64Encoder.kt diff --git a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt index 332dc161f..2b741795e 100644 --- a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt +++ b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt @@ -461,7 +461,8 @@ class ComposeMessageViewModel @Inject constructor( message, newAttachmentIds, parentId, - _actionId + _actionId, + _oldSenderAddressId ) ) diff --git a/app/src/main/java/ch/protonmail/android/di/ApplicationModule.kt b/app/src/main/java/ch/protonmail/android/di/ApplicationModule.kt index 03e85e4f9..d0521b24a 100644 --- a/app/src/main/java/ch/protonmail/android/di/ApplicationModule.kt +++ b/app/src/main/java/ch/protonmail/android/di/ApplicationModule.kt @@ -51,6 +51,8 @@ import ch.protonmail.android.crypto.UserCrypto import ch.protonmail.android.domain.entity.Name import ch.protonmail.android.domain.usecase.DownloadFile import ch.protonmail.android.utils.BuildInfo +import ch.protonmail.android.utils.base64.AndroidBase64Encoder +import ch.protonmail.android.utils.base64.Base64Encoder import ch.protonmail.android.utils.extensions.app import com.birbit.android.jobqueue.JobManager import com.squareup.inject.assisted.dagger2.AssistedModule @@ -216,6 +218,9 @@ object ApplicationModule { @Provides fun attachmentFactory(): IAttachmentFactory = AttachmentFactory() + + @Provides + fun base64Encoder(): Base64Encoder = AndroidBase64Encoder() } @Module diff --git a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt index c5b1c45d7..444e339a9 100644 --- a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt +++ b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt @@ -76,7 +76,7 @@ class SaveDraft @Inject constructor( return@withContext Result.SendingInProgressError } - createDraftWorker.enqueue(message, params.parentId, params.actionType) + createDraftWorker.enqueue(message, params.parentId, params.actionType, params.previousSenderAddressId) return@withContext Result.Success } @@ -90,6 +90,7 @@ class SaveDraft @Inject constructor( val message: Message, val newAttachmentIds: List, val parentId: String?, - val actionType: Constants.MessageActionType + val actionType: Constants.MessageActionType, + val previousSenderAddressId: String ) } diff --git a/app/src/main/java/ch/protonmail/android/utils/base64/AndroidBase64Encoder.kt b/app/src/main/java/ch/protonmail/android/utils/base64/AndroidBase64Encoder.kt new file mode 100644 index 000000000..c200c942d --- /dev/null +++ b/app/src/main/java/ch/protonmail/android/utils/base64/AndroidBase64Encoder.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * + * This file is part of ProtonMail. + * + * ProtonMail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonMail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonMail. If not, see https://www.gnu.org/licenses/. + */ + +package ch.protonmail.android.utils.base64 + +import android.util.Base64 + +class AndroidBase64Encoder : Base64Encoder { + + override fun decode(base64String: String): ByteArray = + Base64.decode(base64String, Base64.DEFAULT) + + override fun encode(base64Bytes: ByteArray): String = + Base64.encodeToString(base64Bytes, Base64.NO_WRAP) + +} diff --git a/app/src/main/java/ch/protonmail/android/utils/base64/Base64Encoder.kt b/app/src/main/java/ch/protonmail/android/utils/base64/Base64Encoder.kt new file mode 100644 index 000000000..36f20ee5b --- /dev/null +++ b/app/src/main/java/ch/protonmail/android/utils/base64/Base64Encoder.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * + * This file is part of ProtonMail. + * + * ProtonMail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonMail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonMail. If not, see https://www.gnu.org/licenses/. + */ + +package ch.protonmail.android.utils.base64 + +interface Base64Encoder { + fun decode(base64String: String): ByteArray + fun encode(base64Bytes: ByteArray): String +} diff --git a/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt b/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt index fc51700a2..1e48e9b83 100644 --- a/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt +++ b/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt @@ -38,7 +38,11 @@ import ch.protonmail.android.api.models.room.messages.MessageSender import ch.protonmail.android.api.utils.Fields import ch.protonmail.android.core.Constants import ch.protonmail.android.core.UserManager +import ch.protonmail.android.crypto.AddressCrypto import ch.protonmail.android.domain.entity.Id +import ch.protonmail.android.domain.entity.Name +import ch.protonmail.android.domain.entity.user.Address +import ch.protonmail.android.utils.base64.Base64Encoder import ch.protonmail.android.utils.extensions.deserialize import ch.protonmail.android.utils.extensions.serialize import javax.inject.Inject @@ -47,6 +51,7 @@ internal const val KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_DB_ID = "keyCreateDraftMe internal const val KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_LOCAL_ID = "keyCreateDraftMessageLocalId" internal const val KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_PARENT_ID = "keyCreateDraftMessageParentId" internal const val KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_ACTION_TYPE_SERIALIZED = "keyCreateDraftMessageActionTypeSerialized" +internal const val KEY_INPUT_DATA_CREATE_DRAFT_PREVIOUS_SENDER_ADDRESS_ID = "keyCreateDraftPreviousSenderAddressId" internal const val KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_ERROR_ENUM = "keyCreateDraftErrorResult" @@ -57,42 +62,87 @@ class CreateDraftWorker @WorkerInject constructor( @Assisted params: WorkerParameters, private val messageDetailsRepository: MessageDetailsRepository, private val messageFactory: MessageFactory, - private val userManager: UserManager + private val userManager: UserManager, + private val addressCryptoFactory: AddressCrypto.Factory, + private val base64: Base64Encoder ) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { val message = messageDetailsRepository.findMessageByMessageDbId(getInputMessageDbId()) ?: return failureWithError(CreateDraftWorkerErrors.MessageNotFound) - - val createDraftRequest = messageFactory.createDraftApiRequest(message) + val senderAddressId = requireNotNull(message.addressID) + val senderAddress = requireNotNull(getSenderAddress(senderAddressId)) val inputParentId = getInputParentId() + val createDraftRequest = messageFactory.createDraftApiRequest(message) + inputParentId?.let { createDraftRequest.setParentID(inputParentId); createDraftRequest.action = getInputActionType().messageActionTypeValue val parentMessage = messageDetailsRepository.findMessageById(inputParentId) + if (isSenderAddressChanged()) { + val encryptedAttachments = reEncryptParentAttachments(parentMessage, senderAddress) + encryptedAttachments.forEach { + createDraftRequest.addAttachmentKeyPacket(it.key, it.value) + } + } } val encryptedMessage = requireNotNull(message.messageBody) createDraftRequest.addMessageBody(Fields.Message.SELF, encryptedMessage); - createDraftRequest.setSender(getMessageSender(message)) - + createDraftRequest.setSender(getMessageSender(message, senderAddress)) return Result.failure() } - private fun getMessageSender(message: Message): MessageSender? { - if (message.isSenderEmailAlias()) { - return MessageSender(message.senderName, message.senderEmail) + private fun isSenderAddressChanged(): Boolean { + val previousSenderAddressId = getInputPreviousSenderAddressId() + return previousSenderAddressId?.isNotEmpty() == true + } + + private fun reEncryptParentAttachments( + parentMessage: Message?, + senderAddress: Address + ): MutableMap { + val attachments = parentMessage?.attachments(messageDetailsRepository.databaseProvider.provideMessagesDao()) + val previousSenderAddressId = requireNotNull(getInputPreviousSenderAddressId()) + val encryptedAttachments = mutableMapOf() + + val addressCrypto = addressCryptoFactory.create(Id(previousSenderAddressId), Name(userManager.username)) + val primaryKey = senderAddress.keys + val publicKey = addressCrypto.buildArmoredPublicKey(primaryKey.primaryKey!!.privateKey) + + attachments?.forEach { attachment -> + val actionType = getInputActionType() + if ( + actionType === Constants.MessageActionType.FORWARD || + ((actionType === Constants.MessageActionType.REPLY || + actionType === Constants.MessageActionType.REPLY_ALL) && + attachment.inline) + ) { + val keyPackets = attachment.keyPackets + if (!keyPackets.isNullOrEmpty()) { + val keyPackage = base64.decode(keyPackets) + val sessionKey = addressCrypto.decryptKeyPacket(keyPackage) + val newKeyPackage = addressCrypto.encryptKeyPacket(sessionKey, publicKey) + val newKeyPackets = base64.encode(newKeyPackage) + encryptedAttachments[attachment.attachmentId!!] = newKeyPackets + } + } } + return encryptedAttachments + } - val addressId = requireNotNull(message.addressID) + private fun getSenderAddress(senderAddressId: String): Address? { val user = userManager.getUser(userManager.username).loadNew(userManager.username) - user.findAddressById(Id(addressId))?.let { - return MessageSender(it.displayName?.s, it.email.s) - } + return user.findAddressById(Id(senderAddressId)) + } - return null + private fun getMessageSender(message: Message, senderAddress: Address): MessageSender { + if (message.isSenderEmailAlias()) { + return MessageSender(message.senderName, message.senderEmail) + } + return MessageSender(senderAddress.displayName?.s, senderAddress.email.s) } private fun failureWithError(error: CreateDraftWorkerErrors): Result { @@ -105,7 +155,11 @@ class CreateDraftWorker @WorkerInject constructor( .getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_ACTION_TYPE_SERIALIZED)?.deserialize() ?: Constants.MessageActionType.NONE - private fun getInputParentId(): String? = + + private fun getInputPreviousSenderAddressId() = + inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_PREVIOUS_SENDER_ADDRESS_ID) + + private fun getInputParentId() = inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_PARENT_ID) private fun getInputMessageDbId() = @@ -120,7 +174,8 @@ class CreateDraftWorker @WorkerInject constructor( fun enqueue( message: Message, parentId: String?, - actionType: Constants.MessageActionType + actionType: Constants.MessageActionType, + previousSenderAddressId: String ): LiveData { val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) @@ -132,7 +187,8 @@ class CreateDraftWorker @WorkerInject constructor( KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_DB_ID to message.dbId, KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_LOCAL_ID to message.messageId, KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_PARENT_ID to parentId, - KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_ACTION_TYPE_SERIALIZED to actionType.serialize() + KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_ACTION_TYPE_SERIALIZED to actionType.serialize(), + KEY_INPUT_DATA_CREATE_DRAFT_PREVIOUS_SENDER_ADDRESS_ID to previousSenderAddressId ) ).build() diff --git a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt index 361af77cb..1c82329fb 100644 --- a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt +++ b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt @@ -84,6 +84,7 @@ class ComposeMessageViewModelTest : CoroutinesTest { // Needed to set class fields to the right value viewModel.prepareMessageData(false, "addressId", "mail-alias", false) viewModel.setupComposingNewMessage(false, Constants.MessageActionType.FORWARD, "parentId823", "") + viewModel.oldSenderAddressId = "previousSenderAddressId" viewModel.saveDraft(message, hasConnectivity = false) @@ -91,7 +92,8 @@ class ComposeMessageViewModelTest : CoroutinesTest { message, emptyList(), "parentId823", - Constants.MessageActionType.FORWARD + Constants.MessageActionType.FORWARD, + "previousSenderAddressId" ) coVerify { saveDraft(parameters) } } diff --git a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt index 00d6bdf19..2aeb50745 100644 --- a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt +++ b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt @@ -87,7 +87,9 @@ class SaveDraftTest : CoroutinesTest { coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 123L // When - saveDraft(SaveDraftParameters(message, emptyList(), null, FORWARD)) + saveDraft( + SaveDraftParameters(message, emptyList(), null, FORWARD, "previousSenderId1273") + ) // Then val expectedMessage = message.copy(messageBody = "encrypted armored content") @@ -118,7 +120,9 @@ class SaveDraftTest : CoroutinesTest { coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 123L // When - saveDraft(SaveDraftParameters(message, emptyList(), null, FORWARD)) + saveDraft( + SaveDraftParameters(message, emptyList(), null, FORWARD, "previousSenderId1273") + ) // Then coVerify { messageDetailsRepository.insertPendingDraft(123L) } @@ -142,7 +146,9 @@ class SaveDraftTest : CoroutinesTest { // When val newAttachments = listOf("attachmentId") - saveDraft.invoke(SaveDraftParameters(message, newAttachments, "parentId", REPLY)) + saveDraft.invoke( + SaveDraftParameters(message, newAttachments, "parentId", REPLY, "previousSenderId1273") + ) // Then verify { pendingActionsDao.insertPendingForUpload(PendingUpload("456")) } @@ -161,7 +167,9 @@ class SaveDraftTest : CoroutinesTest { coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 9833L // When - saveDraft.invoke(SaveDraftParameters(message, emptyList(), "parentId", FORWARD)) + saveDraft.invoke( + SaveDraftParameters(message, emptyList(), "parentId", FORWARD, "previousSenderId1273") + ) // Then verify(exactly = 0) { pendingActionsDao.insertPendingForUpload(any()) } @@ -183,12 +191,14 @@ class SaveDraftTest : CoroutinesTest { every { pendingActionsDao.findPendingSendByDbId(messageDbId) } returns PendingSend("anyMessageId") // When - val result = saveDraft.invoke(SaveDraftParameters(message, emptyList(), "parentId123", FORWARD)) + val result = saveDraft.invoke( + SaveDraftParameters(message, emptyList(), "parentId123", FORWARD, "previousSenderId1273") + ) // Then val expectedError = Result.SendingInProgressError Assert.assertEquals(expectedError, result) - verify(exactly = 0) { createDraftScheduler.enqueue(any(), any(), any()) } + verify(exactly = 0) { createDraftScheduler.enqueue(any(), any(), any(), any()) } } @Test @@ -205,10 +215,12 @@ class SaveDraftTest : CoroutinesTest { every { pendingActionsDao.findPendingSendByDbId(9833L) } returns null // When - saveDraft.invoke(SaveDraftParameters(message, emptyList(), "parentId123", REPLY_ALL)) + saveDraft.invoke( + SaveDraftParameters(message, emptyList(), "parentId123", REPLY_ALL, "previousSenderId1273") + ) // Then - verify { createDraftScheduler.enqueue(message, "parentId123", REPLY_ALL) } + verify { createDraftScheduler.enqueue(message, "parentId123", REPLY_ALL, "previousSenderId1273") } } } diff --git a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt index ab4fdc7e5..c4b1b6b6a 100644 --- a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt +++ b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt @@ -29,6 +29,7 @@ import androidx.work.WorkerParameters import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository import ch.protonmail.android.api.models.NewMessage import ch.protonmail.android.api.models.messages.receive.MessageFactory +import ch.protonmail.android.api.models.room.messages.Attachment import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.api.models.room.messages.MessageSender import ch.protonmail.android.api.utils.Fields.Message.SELF @@ -37,14 +38,19 @@ import ch.protonmail.android.core.Constants.MessageActionType.FORWARD import ch.protonmail.android.core.Constants.MessageActionType.NONE import ch.protonmail.android.core.Constants.MessageActionType.REPLY_ALL import ch.protonmail.android.core.UserManager +import ch.protonmail.android.crypto.AddressCrypto import ch.protonmail.android.domain.entity.EmailAddress import ch.protonmail.android.domain.entity.Id import ch.protonmail.android.domain.entity.Name +import ch.protonmail.android.domain.entity.NotBlankString +import ch.protonmail.android.domain.entity.PgpField import ch.protonmail.android.domain.entity.user.Address import ch.protonmail.android.domain.entity.user.AddressKeys +import ch.protonmail.android.utils.base64.Base64Encoder import ch.protonmail.android.utils.extensions.serialize import io.mockk.every import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.RelaxedMockK import io.mockk.junit5.MockKExtension import io.mockk.mockk @@ -77,6 +83,12 @@ class CreateDraftWorkerTest : CoroutinesTest { @RelaxedMockK private lateinit var userManager: UserManager + @RelaxedMockK + private lateinit var addressCryptoFactory: AddressCrypto.Factory + + @MockK + private lateinit var base64: Base64Encoder + @InjectMockKs private lateinit var worker: CreateDraftWorker @@ -90,11 +102,17 @@ class CreateDraftWorkerTest : CoroutinesTest { val messageActionType = REPLY_ALL val message = Message(messageLocalId) message.dbId = messageDbId + val previousSenderAddressId = "previousSenderId82348" val requestSlot = slot() every { workManager.enqueue(capture(requestSlot)) } answers { mockk() } // When - CreateDraftWorker.Enqueuer(workManager).enqueue(message, messageParentId, messageActionType) + CreateDraftWorker.Enqueuer(workManager).enqueue( + message, + messageParentId, + messageActionType, + previousSenderAddressId + ) // Then val constraints = requestSlot.captured.workSpec.constraints @@ -103,10 +121,12 @@ class CreateDraftWorkerTest : CoroutinesTest { val actualMessageLocalId = inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_LOCAL_ID) val actualMessageParentId = inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_PARENT_ID) val actualMessageActionType = inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_ACTION_TYPE_SERIALIZED) + val actualPreviousSenderAddress = inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_PREVIOUS_SENDER_ADDRESS_ID) assertEquals(message.dbId, actualMessageDbId) assertEquals(message.messageId, actualMessageLocalId) assertEquals(messageParentId, actualMessageParentId) assertEquals(messageActionType.serialize(), actualMessageActionType) + assertEquals(previousSenderAddressId, actualPreviousSenderAddress) assertEquals(NetworkType.CONNECTED, constraints.requiredNetworkType) verify { workManager.getWorkInfoByIdLiveData(any()) } } @@ -175,23 +195,23 @@ class CreateDraftWorkerTest : CoroutinesTest { messageBody = "messageBody" } val apiDraftMessage = mockk(relaxed = true) + val address = Address( + Id(addressId), + null, + EmailAddress("sender@email.it"), + Name("senderName"), + true, + Address.Type.ORIGINAL, + allowedToSend = true, + allowedToReceive = false, + keys = AddressKeys(null, emptyList()) + ) givenMessageIdInput(messageDbId) givenActionTypeInput() every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message every { messageFactory.createDraftApiRequest(message) } answers { apiDraftMessage } every { userManager.username } returns "username" every { userManager.getUser("username").loadNew("username") } returns mockk { - val address = Address( - Id(addressId), - null, - EmailAddress("sender@email.it"), - Name("senderName"), - true, - Address.Type.ORIGINAL, - allowedToSend = true, - allowedToReceive = false, - keys = AddressKeys(null, emptyList()) - ) every { findAddressById(Id(addressId)) } returns address } @@ -232,13 +252,76 @@ class CreateDraftWorkerTest : CoroutinesTest { } } + @Test + fun workerReEncryptParentAttachmentsWhenSenderAddressChanged() { + runBlockingTest { + // Given + val parentId = "89345" + val actionType = FORWARD + val messageDbId = 345L + val message = Message().apply { + dbId = messageDbId + addressID = "addressId835" + messageBody = "messageBody" + } + val attachment = Attachment("attachment1", "pic.jpg", "image/jpeg", keyPackets = "somePackets") + val previousSenderAddressId = "previousSenderId82348" + val privateKey = PgpField.PrivateKey(NotBlankString("current sender private key")) + val username = "username934" + val senderPublicKey = "new sender public key" + val decodedPacketsBytes = "decoded attachment packets".toByteArray() + val encryptedKeyPackets = "re-encrypted att key packets".toByteArray() + + val apiDraftMessage = mockk(relaxed = true) + val parentMessage = mockk { + every { attachments(any()) } returns listOf(attachment) + } + val senderAddress = mockk
(relaxed = true) { + every { keys.primaryKey?.privateKey } returns privateKey + } + val addressCrypto = mockk(relaxed = true) { + val sessionKey = "session key".toByteArray() + every { buildArmoredPublicKey(privateKey) } returns senderPublicKey + every { decryptKeyPacket(decodedPacketsBytes) } returns sessionKey + every { encryptKeyPacket(sessionKey, senderPublicKey) } returns encryptedKeyPackets + } + givenMessageIdInput(messageDbId) + givenParentIdInput(parentId) + givenActionTypeInput(actionType) + givenPreviousSenderAddress(previousSenderAddressId) + every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message + every { messageFactory.createDraftApiRequest(message) } answers { apiDraftMessage } + every { messageDetailsRepository.findMessageById(parentId) } returns parentMessage + every { userManager.username } returns username + every { userManager.getUser(username).loadNew(username) } returns mockk { + every { findAddressById(Id("addressId835")) } returns senderAddress + } + every { addressCryptoFactory.create(Id(previousSenderAddressId), Name(username)) } returns addressCrypto + every { addressCrypto.buildArmoredPublicKey(privateKey) } returns senderPublicKey + every { base64.decode(attachment.keyPackets!!) } returns decodedPacketsBytes + every { base64.encode(encryptedKeyPackets) } returns "encrypted encoded packets" + + // When + worker.doWork() + + // Then + val attachmentReEncrypted = attachment.copy(keyPackets = "encrypted encoded packets") + verify { parentMessage.attachments(messageDetailsRepository.databaseProvider.provideMessagesDao()) } + verify { addressCrypto.buildArmoredPublicKey(privateKey) } + verify { apiDraftMessage.addAttachmentKeyPacket("attachment1", attachmentReEncrypted.keyPackets) } + } + } + + private fun givenPreviousSenderAddress(address: String) { + every { parameters.inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_PREVIOUS_SENDER_ADDRESS_ID) } answers { address } + } + private fun givenActionTypeInput(actionType: Constants.MessageActionType = NONE) { every { parameters.inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_ACTION_TYPE_SERIALIZED) } answers { actionType.serialize() } - } private fun givenParentIdInput(parentId: String) { From 4305e872068126f1d6569cf140e57a18613602b9 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Thu, 10 Dec 2020 18:47:25 +0100 Subject: [PATCH 015/145] Add existing parent attachments to request when address didn't change MAILAND-1018 --- .../android/worker/CreateDraftWorker.kt | 55 +++++++------- .../android/worker/CreateDraftWorkerTest.kt | 71 +++++++++++++++++++ 2 files changed, 102 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt b/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt index 1e48e9b83..9e7ccd6bf 100644 --- a/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt +++ b/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt @@ -33,6 +33,7 @@ import androidx.work.WorkerParameters import androidx.work.workDataOf import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository import ch.protonmail.android.api.models.messages.receive.MessageFactory +import ch.protonmail.android.api.models.room.messages.Attachment import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.api.models.room.messages.MessageSender import ch.protonmail.android.api.utils.Fields @@ -80,9 +81,18 @@ class CreateDraftWorker @WorkerInject constructor( createDraftRequest.setParentID(inputParentId); createDraftRequest.action = getInputActionType().messageActionTypeValue val parentMessage = messageDetailsRepository.findMessageById(inputParentId) - if (isSenderAddressChanged()) { - val encryptedAttachments = reEncryptParentAttachments(parentMessage, senderAddress) - encryptedAttachments.forEach { + val attachments = parentMessage?.attachments(messageDetailsRepository.databaseProvider.provideMessagesDao()) + + if (shouldAddParentAttachment()) { + if (isSenderAddressChanged()) { + reEncryptParentAttachments(attachments, senderAddress) + } else { + val requestAttachments = mutableMapOf() + attachments?.forEach { + requestAttachments[it.attachmentId!!] = it.keyPackets!! + } + requestAttachments + }.forEach { createDraftRequest.addAttachmentKeyPacket(it.key, it.value) } } @@ -90,21 +100,27 @@ class CreateDraftWorker @WorkerInject constructor( val encryptedMessage = requireNotNull(message.messageBody) createDraftRequest.addMessageBody(Fields.Message.SELF, encryptedMessage); - createDraftRequest.setSender(getMessageSender(message, senderAddress)) + createDraftRequest.setSender(buildMessageSender(message, senderAddress)) return Result.failure() } + private fun shouldAddParentAttachment(): Boolean { + val actionType = getInputActionType() + return actionType == Constants.MessageActionType.FORWARD || + actionType == Constants.MessageActionType.REPLY || + actionType == Constants.MessageActionType.REPLY_ALL + } + private fun isSenderAddressChanged(): Boolean { val previousSenderAddressId = getInputPreviousSenderAddressId() return previousSenderAddressId?.isNotEmpty() == true } private fun reEncryptParentAttachments( - parentMessage: Message?, + parentAttachments: List?, senderAddress: Address ): MutableMap { - val attachments = parentMessage?.attachments(messageDetailsRepository.databaseProvider.provideMessagesDao()) val previousSenderAddressId = requireNotNull(getInputPreviousSenderAddressId()) val encryptedAttachments = mutableMapOf() @@ -112,22 +128,14 @@ class CreateDraftWorker @WorkerInject constructor( val primaryKey = senderAddress.keys val publicKey = addressCrypto.buildArmoredPublicKey(primaryKey.primaryKey!!.privateKey) - attachments?.forEach { attachment -> - val actionType = getInputActionType() - if ( - actionType === Constants.MessageActionType.FORWARD || - ((actionType === Constants.MessageActionType.REPLY || - actionType === Constants.MessageActionType.REPLY_ALL) && - attachment.inline) - ) { - val keyPackets = attachment.keyPackets - if (!keyPackets.isNullOrEmpty()) { - val keyPackage = base64.decode(keyPackets) - val sessionKey = addressCrypto.decryptKeyPacket(keyPackage) - val newKeyPackage = addressCrypto.encryptKeyPacket(sessionKey, publicKey) - val newKeyPackets = base64.encode(newKeyPackage) - encryptedAttachments[attachment.attachmentId!!] = newKeyPackets - } + parentAttachments?.forEach { attachment -> + val keyPackets = attachment.keyPackets + if (!keyPackets.isNullOrEmpty()) { + val keyPackage = base64.decode(keyPackets) + val sessionKey = addressCrypto.decryptKeyPacket(keyPackage) + val newKeyPackage = addressCrypto.encryptKeyPacket(sessionKey, publicKey) + val newKeyPackets = base64.encode(newKeyPackage) + encryptedAttachments[attachment.attachmentId!!] = newKeyPackets } } return encryptedAttachments @@ -138,7 +146,7 @@ class CreateDraftWorker @WorkerInject constructor( return user.findAddressById(Id(senderAddressId)) } - private fun getMessageSender(message: Message, senderAddress: Address): MessageSender { + private fun buildMessageSender(message: Message, senderAddress: Address): MessageSender { if (message.isSenderEmailAlias()) { return MessageSender(message.senderName, message.senderEmail) } @@ -155,7 +163,6 @@ class CreateDraftWorker @WorkerInject constructor( .getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_ACTION_TYPE_SERIALIZED)?.deserialize() ?: Constants.MessageActionType.NONE - private fun getInputPreviousSenderAddressId() = inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_PREVIOUS_SENDER_ADDRESS_ID) diff --git a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt index c4b1b6b6a..c6b8a53ad 100644 --- a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt +++ b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt @@ -56,6 +56,7 @@ import io.mockk.junit5.MockKExtension import io.mockk.mockk import io.mockk.slot import io.mockk.verify +import io.mockk.verifyOrder import kotlinx.coroutines.test.runBlockingTest import me.proton.core.test.kotlin.CoroutinesTest import org.junit.Assert.assertEquals @@ -312,6 +313,76 @@ class CreateDraftWorkerTest : CoroutinesTest { } } + @Test + fun workerAddsExistingParentAttachmentsToRequestWhenSenderAddressWasNotChanged() { + runBlockingTest { + // Given + val parentId = "89345" + val messageDbId = 345L + val message = Message().apply { + dbId = messageDbId + addressID = "addressId835" + messageBody = "messageBody" + } + val apiDraftMessage = mockk(relaxed = true) + val parentMessage = mockk { + every { attachments(any()) } returns listOf( + Attachment("attachment", keyPackets = "OriginalAttachmentPackets", inline = true), + Attachment("attachment1", keyPackets = "Attachment1KeyPackets", inline = false), + Attachment("attachment2", keyPackets = "Attachment2KeyPackets", inline = true) + ) + } + givenMessageIdInput(messageDbId) + givenParentIdInput(parentId) + givenActionTypeInput(REPLY_ALL) + givenPreviousSenderAddress("") + every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message + every { messageFactory.createDraftApiRequest(message) } answers { apiDraftMessage } + every { messageDetailsRepository.findMessageById(parentId) } returns parentMessage + + // When + worker.doWork() + + // Then + verify { parentMessage.attachments(messageDetailsRepository.databaseProvider.provideMessagesDao()) } + verifyOrder { + apiDraftMessage.addAttachmentKeyPacket("attachment", "OriginalAttachmentPackets") + apiDraftMessage.addAttachmentKeyPacket("attachment2", "Attachment2KeyPackets") + } + + // TODO figure if we actually need this (regarding inline attachments, discussing with the team) + verify(exactly = 0) { apiDraftMessage.addAttachmentKeyPacket("attachment1", "Attachment1KeyPackets") } + } + } + + @Test + fun workerDoesNotAddParentAttachmentsToRequestWhenActionTypeIsOtherThenForwardReplyReplyAll() { + runBlockingTest { + // Given + val parentId = "89345" + val messageDbId = 345L + val message = Message().apply { + dbId = messageDbId + addressID = "addressId835" + messageBody = "messageBody" + } + + val apiDraftMessage = mockk(relaxed = true) + givenMessageIdInput(messageDbId) + givenParentIdInput(parentId) + givenActionTypeInput(NONE) + givenPreviousSenderAddress("") + every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message + every { messageFactory.createDraftApiRequest(message) } answers { apiDraftMessage } + + // When + worker.doWork() + + // Then + verify(exactly = 0) { apiDraftMessage.addAttachmentKeyPacket(any(), any()) } + } + } + private fun givenPreviousSenderAddress(address: String) { every { parameters.inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_PREVIOUS_SENDER_ADDRESS_ID) } answers { address } } From 77a4dd9c5d1f85c9e82dbe29c0ead7c520d44531 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Fri, 11 Dec 2020 16:44:24 +0100 Subject: [PATCH 016/145] Refactor adding parent attachments to create draft request MAILAND-1018 --- .../android/worker/CreateDraftWorker.kt | 90 +++++++++++-------- .../android/worker/CreateDraftWorkerTest.kt | 90 +++++++++++++++++-- 2 files changed, 136 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt b/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt index 9e7ccd6bf..55e1e92c3 100644 --- a/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt +++ b/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt @@ -38,6 +38,10 @@ import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.api.models.room.messages.MessageSender import ch.protonmail.android.api.utils.Fields import ch.protonmail.android.core.Constants +import ch.protonmail.android.core.Constants.MessageActionType.FORWARD +import ch.protonmail.android.core.Constants.MessageActionType.NONE +import ch.protonmail.android.core.Constants.MessageActionType.REPLY +import ch.protonmail.android.core.Constants.MessageActionType.REPLY_ALL import ch.protonmail.android.core.UserManager import ch.protonmail.android.crypto.AddressCrypto import ch.protonmail.android.domain.entity.Id @@ -83,18 +87,8 @@ class CreateDraftWorker @WorkerInject constructor( val parentMessage = messageDetailsRepository.findMessageById(inputParentId) val attachments = parentMessage?.attachments(messageDetailsRepository.databaseProvider.provideMessagesDao()) - if (shouldAddParentAttachment()) { - if (isSenderAddressChanged()) { - reEncryptParentAttachments(attachments, senderAddress) - } else { - val requestAttachments = mutableMapOf() - attachments?.forEach { - requestAttachments[it.attachmentId!!] = it.keyPackets!! - } - requestAttachments - }.forEach { - createDraftRequest.addAttachmentKeyPacket(it.key, it.value) - } + buildDraftRequestParentAttachments(attachments, senderAddress).forEach { + createDraftRequest.addAttachmentKeyPacket(it.key, it.value) } } @@ -105,40 +99,58 @@ class CreateDraftWorker @WorkerInject constructor( return Result.failure() } - private fun shouldAddParentAttachment(): Boolean { - val actionType = getInputActionType() - return actionType == Constants.MessageActionType.FORWARD || - actionType == Constants.MessageActionType.REPLY || - actionType == Constants.MessageActionType.REPLY_ALL - } + private fun buildDraftRequestParentAttachments( + attachments: List?, + senderAddress: Address + ): Map { + if (shouldAddParentAttachments().not()) { + return emptyMap() + } - private fun isSenderAddressChanged(): Boolean { - val previousSenderAddressId = getInputPreviousSenderAddressId() - return previousSenderAddressId?.isNotEmpty() == true + val draftAttachments = mutableMapOf() + attachments?.forEach { attachment -> + if (shouldSkipAttachment(attachment)) { + return@forEach + } + val keyPackets = if (isSenderAddressChanged()) { + reEncryptAttachment(senderAddress, attachment) + } else { + attachment.keyPackets!! + } + draftAttachments[attachment.attachmentId!!] = keyPackets + } + return draftAttachments } - private fun reEncryptParentAttachments( - parentAttachments: List?, - senderAddress: Address - ): MutableMap { + private fun reEncryptAttachment(senderAddress: Address, attachment: Attachment): String { val previousSenderAddressId = requireNotNull(getInputPreviousSenderAddressId()) - val encryptedAttachments = mutableMapOf() - val addressCrypto = addressCryptoFactory.create(Id(previousSenderAddressId), Name(userManager.username)) val primaryKey = senderAddress.keys val publicKey = addressCrypto.buildArmoredPublicKey(primaryKey.primaryKey!!.privateKey) - parentAttachments?.forEach { attachment -> - val keyPackets = attachment.keyPackets - if (!keyPackets.isNullOrEmpty()) { - val keyPackage = base64.decode(keyPackets) - val sessionKey = addressCrypto.decryptKeyPacket(keyPackage) - val newKeyPackage = addressCrypto.encryptKeyPacket(sessionKey, publicKey) - val newKeyPackets = base64.encode(newKeyPackage) - encryptedAttachments[attachment.attachmentId!!] = newKeyPackets - } - } - return encryptedAttachments + val keyPackage = base64.decode(attachment.keyPackets!!) + val sessionKey = addressCrypto.decryptKeyPacket(keyPackage) + val newKeyPackage = addressCrypto.encryptKeyPacket(sessionKey, publicKey) + return base64.encode(newKeyPackage) + } + + private fun shouldSkipAttachment(attachment: Attachment): Boolean { + val actionType = getInputActionType() + val isReplying = actionType == REPLY || actionType == REPLY_ALL + + return isReplying && attachment.inline.not() + } + + private fun shouldAddParentAttachments(): Boolean { + val actionType = getInputActionType() + return actionType == FORWARD || + actionType == REPLY || + actionType == REPLY_ALL + } + + private fun isSenderAddressChanged(): Boolean { + val previousSenderAddressId = getInputPreviousSenderAddressId() + return previousSenderAddressId?.isNotEmpty() == true } private fun getSenderAddress(senderAddressId: String): Address? { @@ -161,7 +173,7 @@ class CreateDraftWorker @WorkerInject constructor( private fun getInputActionType(): Constants.MessageActionType = inputData .getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_ACTION_TYPE_SERIALIZED)?.deserialize() - ?: Constants.MessageActionType.NONE + ?: NONE private fun getInputPreviousSenderAddressId() = inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_PREVIOUS_SENDER_ADDRESS_ID) diff --git a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt index c6b8a53ad..ce61f3585 100644 --- a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt +++ b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt @@ -36,6 +36,7 @@ import ch.protonmail.android.api.utils.Fields.Message.SELF import ch.protonmail.android.core.Constants import ch.protonmail.android.core.Constants.MessageActionType.FORWARD import ch.protonmail.android.core.Constants.MessageActionType.NONE +import ch.protonmail.android.core.Constants.MessageActionType.REPLY import ch.protonmail.android.core.Constants.MessageActionType.REPLY_ALL import ch.protonmail.android.core.UserManager import ch.protonmail.android.crypto.AddressCrypto @@ -50,7 +51,6 @@ import ch.protonmail.android.utils.base64.Base64Encoder import ch.protonmail.android.utils.extensions.serialize import io.mockk.every import io.mockk.impl.annotations.InjectMockKs -import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.RelaxedMockK import io.mockk.junit5.MockKExtension import io.mockk.mockk @@ -87,12 +87,13 @@ class CreateDraftWorkerTest : CoroutinesTest { @RelaxedMockK private lateinit var addressCryptoFactory: AddressCrypto.Factory - @MockK + @RelaxedMockK private lateinit var base64: Base64Encoder @InjectMockKs private lateinit var worker: CreateDraftWorker + @Test fun workerEnqueuerCreatesOneTimeRequestWorkerWithParams() { runBlockingTest { @@ -254,7 +255,7 @@ class CreateDraftWorkerTest : CoroutinesTest { } @Test - fun workerReEncryptParentAttachmentsWhenSenderAddressChanged() { + fun workerAddsReEncryptedParentAttachmentsToRequestWhenActionIsForwardAndSenderAddressChanged() { runBlockingTest { // Given val parentId = "89345" @@ -313,6 +314,42 @@ class CreateDraftWorkerTest : CoroutinesTest { } } + @Test + fun workerSkipsNonInlineParentAttachmentsWhenActionIsReplyAllAndSenderAddressChanged() { + runBlockingTest { + // Given + val parentId = "89345" + val messageDbId = 345L + val message = Message().apply { + dbId = messageDbId + addressID = "addressId835" + messageBody = "messageBody" + } + val attachment = Attachment("attachment1", "pic.jpg", "image/jpeg", keyPackets = "somePackets", inline = true) + val attachment2 = Attachment("attachment2", "pic2.jpg", keyPackets = "somePackets2", inline = false) + val previousSenderAddressId = "previousSenderId82348" + + val apiDraftMessage = mockk(relaxed = true) + val parentMessage = mockk { + every { attachments(any()) } returns listOf(attachment, attachment2) + } + givenMessageIdInput(messageDbId) + givenParentIdInput(parentId) + givenActionTypeInput(REPLY_ALL) + givenPreviousSenderAddress(previousSenderAddressId) + every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message + every { messageFactory.createDraftApiRequest(message) } answers { apiDraftMessage } + every { messageDetailsRepository.findMessageById(parentId) } returns parentMessage + every { userManager.username } returns "username93w" + + // When + worker.doWork() + + // Then + verify(exactly = 0) { apiDraftMessage.addAttachmentKeyPacket("attachment2", any()) } + } + } + @Test fun workerAddsExistingParentAttachmentsToRequestWhenSenderAddressWasNotChanged() { runBlockingTest { @@ -334,7 +371,47 @@ class CreateDraftWorkerTest : CoroutinesTest { } givenMessageIdInput(messageDbId) givenParentIdInput(parentId) - givenActionTypeInput(REPLY_ALL) + givenActionTypeInput(FORWARD) + givenPreviousSenderAddress("") + every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message + every { messageFactory.createDraftApiRequest(message) } answers { apiDraftMessage } + every { messageDetailsRepository.findMessageById(parentId) } returns parentMessage + + // When + worker.doWork() + + // Then + verify { parentMessage.attachments(messageDetailsRepository.databaseProvider.provideMessagesDao()) } + verifyOrder { + apiDraftMessage.addAttachmentKeyPacket("attachment", "OriginalAttachmentPackets") + apiDraftMessage.addAttachmentKeyPacket("attachment1", "Attachment1KeyPackets") + apiDraftMessage.addAttachmentKeyPacket("attachment2", "Attachment2KeyPackets") + } + } + } + + @Test + fun workerAddsOnlyInlineParentAttachmentsToRequestWhenActionIsReplyAndSenderAddressWasNotChanged() { + runBlockingTest { + // Given + val parentId = "89345" + val messageDbId = 345L + val message = Message().apply { + dbId = messageDbId + addressID = "addressId835" + messageBody = "messageBody" + } + val apiDraftMessage = mockk(relaxed = true) + val parentMessage = mockk { + every { attachments(any()) } returns listOf( + Attachment("attachment", keyPackets = "OriginalAttachmentPackets", inline = true), + Attachment("attachment1", keyPackets = "Attachment1KeyPackets", inline = false), + Attachment("attachment2", keyPackets = "Attachment2KeyPackets", inline = true) + ) + } + givenMessageIdInput(messageDbId) + givenParentIdInput(parentId) + givenActionTypeInput(REPLY) givenPreviousSenderAddress("") every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message every { messageFactory.createDraftApiRequest(message) } answers { apiDraftMessage } @@ -350,8 +427,11 @@ class CreateDraftWorkerTest : CoroutinesTest { apiDraftMessage.addAttachmentKeyPacket("attachment2", "Attachment2KeyPackets") } - // TODO figure if we actually need this (regarding inline attachments, discussing with the team) verify(exactly = 0) { apiDraftMessage.addAttachmentKeyPacket("attachment1", "Attachment1KeyPackets") } + verifyOrder { + apiDraftMessage.addAttachmentKeyPacket("attachment", "OriginalAttachmentPackets") + apiDraftMessage.addAttachmentKeyPacket("attachment2", "Attachment2KeyPackets") + } } } From 541cdaaf7b20dbd2bc8f02d4928c14c35ace745c Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Mon, 14 Dec 2020 10:20:47 +0100 Subject: [PATCH 017/145] Worker calls create Draft API and returns created draft - Call is performed using new non-blocking createDraft method - Returned value is built using both response's message and local one MAILAND-1018 --- .../android/api/ProtonMailApiManager.kt | 4 +- .../api/segments/message/MessageApi.kt | 22 ++-- .../api/segments/message/MessageApiSpec.kt | 12 +- .../api/segments/message/MessageService.kt | 14 ++- .../android/jobs/CreateAndPostDraftJob.java | 2 +- .../android/jobs/messages/PostMessageJob.java | 2 +- .../android/worker/CreateDraftWorker.kt | 36 ++++-- .../android/worker/CreateDraftWorkerTest.kt | 105 ++++++++++++++++++ 8 files changed, 166 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/api/ProtonMailApiManager.kt b/app/src/main/java/ch/protonmail/android/api/ProtonMailApiManager.kt index 3ed1df043..a429885f0 100644 --- a/app/src/main/java/ch/protonmail/android/api/ProtonMailApiManager.kt +++ b/app/src/main/java/ch/protonmail/android/api/ProtonMailApiManager.kt @@ -314,7 +314,9 @@ class ProtonMailApiManager @Inject constructor(var api: ProtonMailApi) : override fun searchByLabelAndTime(query: String, unixTime: Long): MessagesResponse = api.searchByLabelAndTime(query, unixTime) - override fun createDraft(draftBody: DraftBody): MessageResponse? = api.createDraft(draftBody) + override fun createDraftBlocking(draftBody: DraftBody): MessageResponse? = api.createDraftBlocking(draftBody) + + override suspend fun createDraft(draftBody: DraftBody): MessageResponse = api.createDraft(draftBody) override fun updateDraft(messageId: String, draftBody: DraftBody, retrofitTag: RetrofitTag): MessageResponse? = api.updateDraft(messageId, draftBody, retrofitTag) diff --git a/app/src/main/java/ch/protonmail/android/api/segments/message/MessageApi.kt b/app/src/main/java/ch/protonmail/android/api/segments/message/MessageApi.kt index 772d3cee1..487d21f55 100644 --- a/app/src/main/java/ch/protonmail/android/api/segments/message/MessageApi.kt +++ b/app/src/main/java/ch/protonmail/android/api/segments/message/MessageApi.kt @@ -1,18 +1,18 @@ /* * Copyright (c) 2020 Proton Technologies AG - * + * * This file is part of ProtonMail. - * + * * ProtonMail is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * ProtonMail is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with ProtonMail. If not, see https://www.gnu.org/licenses/. */ @@ -118,22 +118,24 @@ class MessageApi(private val service: MessageService) : BaseApi(), MessageApiSpe @Throws(IOException::class) override fun searchByLabelAndPage(query: String, page: Int): MessagesResponse = - ParseUtils.parse(service.searchByLabel(query, page).execute()) + ParseUtils.parse(service.searchByLabel(query, page).execute()) @Throws(IOException::class) override fun searchByLabelAndTime(query: String, unixTime: Long): MessagesResponse = - ParseUtils.parse(service.searchByLabel(query, unixTime).execute()) + ParseUtils.parse(service.searchByLabel(query, unixTime).execute()) @Throws(IOException::class) - override fun createDraft(draftBody: DraftBody): MessageResponse? = - ParseUtils.parse(service.createDraft(draftBody).execute()) + override fun createDraftBlocking(draftBody: DraftBody): MessageResponse? = + ParseUtils.parse(service.createDraftCall(draftBody).execute()) + + override suspend fun createDraft(draftBody: DraftBody): MessageResponse = service.createDraft(draftBody) @Throws(IOException::class) override fun updateDraft(messageId: String, draftBody: DraftBody, retrofitTag: RetrofitTag): MessageResponse? = - ParseUtils.parse(service.updateDraft(messageId, draftBody, retrofitTag).execute()) + ParseUtils.parse(service.updateDraft(messageId, draftBody, retrofitTag).execute()) override fun sendMessage(messageId: String, message: MessageSendBody, retrofitTag: RetrofitTag): Call = - service.sendMessage(messageId, message, retrofitTag) + service.sendMessage(messageId, message, retrofitTag) @Throws(IOException::class) override fun unlabelMessages(idList: IDList) { diff --git a/app/src/main/java/ch/protonmail/android/api/segments/message/MessageApiSpec.kt b/app/src/main/java/ch/protonmail/android/api/segments/message/MessageApiSpec.kt index d3ac1ae5a..dd8275996 100644 --- a/app/src/main/java/ch/protonmail/android/api/segments/message/MessageApiSpec.kt +++ b/app/src/main/java/ch/protonmail/android/api/segments/message/MessageApiSpec.kt @@ -1,18 +1,18 @@ /* * Copyright (c) 2020 Proton Technologies AG - * + * * This file is part of ProtonMail. - * + * * ProtonMail is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * ProtonMail is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with ProtonMail. If not, see https://www.gnu.org/licenses/. */ @@ -92,7 +92,9 @@ interface MessageApiSpec { fun searchByLabelAndTime(query: String, unixTime: Long): MessagesResponse @Throws(IOException::class) - fun createDraft(draftBody: DraftBody): MessageResponse? + fun createDraftBlocking(draftBody: DraftBody): MessageResponse? + + suspend fun createDraft(draftBody: DraftBody): MessageResponse @Throws(IOException::class) fun updateDraft(messageId: String, draftBody: DraftBody, retrofitTag: RetrofitTag): MessageResponse? diff --git a/app/src/main/java/ch/protonmail/android/api/segments/message/MessageService.kt b/app/src/main/java/ch/protonmail/android/api/segments/message/MessageService.kt index 836d361ad..e5271a51b 100644 --- a/app/src/main/java/ch/protonmail/android/api/segments/message/MessageService.kt +++ b/app/src/main/java/ch/protonmail/android/api/segments/message/MessageService.kt @@ -1,18 +1,18 @@ /* * Copyright (c) 2020 Proton Technologies AG - * + * * This file is part of ProtonMail. - * + * * ProtonMail is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * ProtonMail is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with ProtonMail. If not, see https://www.gnu.org/licenses/. */ @@ -103,7 +103,11 @@ interface MessageService { @POST("mail/v4/messages") @Headers(CONTENT_TYPE, ACCEPT_HEADER_V1) - fun createDraft(@Body draftBody: DraftBody): Call + fun createDraftCall(@Body draftBody: DraftBody): Call + + @POST("mail/v4/messages") + @Headers(CONTENT_TYPE, ACCEPT_HEADER_V1) + suspend fun createDraft(@Body draftBody: DraftBody): MessageResponse @GET("mail/v4/messages") @Headers(CONTENT_TYPE, ACCEPT_HEADER_V1) diff --git a/app/src/main/java/ch/protonmail/android/jobs/CreateAndPostDraftJob.java b/app/src/main/java/ch/protonmail/android/jobs/CreateAndPostDraftJob.java index c41d54cbb..2a023a274 100644 --- a/app/src/main/java/ch/protonmail/android/jobs/CreateAndPostDraftJob.java +++ b/app/src/main/java/ch/protonmail/android/jobs/CreateAndPostDraftJob.java @@ -160,7 +160,7 @@ public void onRun() throws Throwable { if (message.getSenderEmail().contains("+")) { // it's being sent by alias newDraft.getMessage().setSender(new ServerMessageSender(message.getSenderName(), message.getSenderEmail())); } - final MessageResponse draftResponse = getApi().createDraft(newDraft); + final MessageResponse draftResponse = getApi().createDraftBlocking(newDraft); // on success update draft with messageId String newId = draftResponse.getMessageId(); diff --git a/app/src/main/java/ch/protonmail/android/jobs/messages/PostMessageJob.java b/app/src/main/java/ch/protonmail/android/jobs/messages/PostMessageJob.java index 844f90a85..cb16c8c41 100644 --- a/app/src/main/java/ch/protonmail/android/jobs/messages/PostMessageJob.java +++ b/app/src/main/java/ch/protonmail/android/jobs/messages/PostMessageJob.java @@ -237,7 +237,7 @@ public void onRun() throws Throwable { // but just in case that there is no any bug on the JobQueue library // TODO verify whether this is actually needed or can be done through saveDtaft use case - final MessageResponse draftResponse = getApi().createDraft(newMessage); + final MessageResponse draftResponse = getApi().createDraftBlocking(newMessage); message.setMessageId(draftResponse.getMessageId()); } message.setTime(ServerTime.currentTimeMillis() / 1000); diff --git a/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt b/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt index 55e1e92c3..9e9f13c61 100644 --- a/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt +++ b/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt @@ -32,6 +32,7 @@ import androidx.work.WorkManager import androidx.work.WorkerParameters import androidx.work.workDataOf import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository +import ch.protonmail.android.api.ProtonMailApiManager import ch.protonmail.android.api.models.messages.receive.MessageFactory import ch.protonmail.android.api.models.room.messages.Attachment import ch.protonmail.android.api.models.room.messages.Message @@ -69,7 +70,8 @@ class CreateDraftWorker @WorkerInject constructor( private val messageFactory: MessageFactory, private val userManager: UserManager, private val addressCryptoFactory: AddressCrypto.Factory, - private val base64: Base64Encoder + private val base64: Base64Encoder, + val apiManager: ProtonMailApiManager ) : CoroutineWorker(context, params) { @@ -78,13 +80,13 @@ class CreateDraftWorker @WorkerInject constructor( ?: return failureWithError(CreateDraftWorkerErrors.MessageNotFound) val senderAddressId = requireNotNull(message.addressID) val senderAddress = requireNotNull(getSenderAddress(senderAddressId)) - val inputParentId = getInputParentId() + val parentId = getInputParentId() val createDraftRequest = messageFactory.createDraftApiRequest(message) - inputParentId?.let { - createDraftRequest.setParentID(inputParentId); + parentId?.let { + createDraftRequest.setParentID(parentId) createDraftRequest.action = getInputActionType().messageActionTypeValue - val parentMessage = messageDetailsRepository.findMessageById(inputParentId) + val parentMessage = messageDetailsRepository.findMessageById(parentId) val attachments = parentMessage?.attachments(messageDetailsRepository.databaseProvider.provideMessagesDao()) buildDraftRequestParentAttachments(attachments, senderAddress).forEach { @@ -96,7 +98,25 @@ class CreateDraftWorker @WorkerInject constructor( createDraftRequest.addMessageBody(Fields.Message.SELF, encryptedMessage); createDraftRequest.setSender(buildMessageSender(message, senderAddress)) - return Result.failure() + val response = apiManager.createDraft(createDraftRequest) + + val responseDraft = response.message + responseDraft.dbId = message.dbId + responseDraft.toList = message.toList + responseDraft.ccList = message.ccList + responseDraft.bccList = message.bccList + responseDraft.replyTos = message.replyTos + responseDraft.sender = message.sender + responseDraft.setLabelIDs(message.getEventLabelIDs()) + responseDraft.parsedHeaders = message.parsedHeaders + responseDraft.isDownloaded = true + responseDraft.setIsRead(true) + responseDraft.numAttachments = message.numAttachments + responseDraft.localId = message.messageId + + messageDetailsRepository.saveMessageInDB(responseDraft) + + return Result.success() } private fun buildDraftRequestParentAttachments( @@ -109,7 +129,7 @@ class CreateDraftWorker @WorkerInject constructor( val draftAttachments = mutableMapOf() attachments?.forEach { attachment -> - if (shouldSkipAttachment(attachment)) { + if (isReplyActionAndAttachmentNotInline(attachment)) { return@forEach } val keyPackets = if (isSenderAddressChanged()) { @@ -134,7 +154,7 @@ class CreateDraftWorker @WorkerInject constructor( return base64.encode(newKeyPackage) } - private fun shouldSkipAttachment(attachment: Attachment): Boolean { + private fun isReplyActionAndAttachmentNotInline(attachment: Attachment): Boolean { val actionType = getInputActionType() val isReplying = actionType == REPLY || actionType == REPLY_ALL diff --git a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt index ce61f3585..b8c8118a6 100644 --- a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt +++ b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt @@ -27,8 +27,11 @@ import androidx.work.OneTimeWorkRequest import androidx.work.WorkManager import androidx.work.WorkerParameters import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository +import ch.protonmail.android.api.ProtonMailApiManager import ch.protonmail.android.api.models.NewMessage +import ch.protonmail.android.api.models.messages.ParsedHeaders import ch.protonmail.android.api.models.messages.receive.MessageFactory +import ch.protonmail.android.api.models.messages.receive.MessageResponse import ch.protonmail.android.api.models.room.messages.Attachment import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.api.models.room.messages.MessageSender @@ -49,6 +52,8 @@ import ch.protonmail.android.domain.entity.user.Address import ch.protonmail.android.domain.entity.user.AddressKeys import ch.protonmail.android.utils.base64.Base64Encoder import ch.protonmail.android.utils.extensions.serialize +import io.mockk.coEvery +import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.InjectMockKs import io.mockk.impl.annotations.RelaxedMockK @@ -57,9 +62,11 @@ import io.mockk.mockk import io.mockk.slot import io.mockk.verify import io.mockk.verifyOrder +import io.mockk.verifySequence import kotlinx.coroutines.test.runBlockingTest import me.proton.core.test.kotlin.CoroutinesTest import org.junit.Assert.assertEquals +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -90,9 +97,16 @@ class CreateDraftWorkerTest : CoroutinesTest { @RelaxedMockK private lateinit var base64: Base64Encoder + @RelaxedMockK + private lateinit var apiManager: ProtonMailApiManager + @InjectMockKs private lateinit var worker: CreateDraftWorker + @BeforeEach + fun setUp() { + coEvery { apiManager.createDraft(any()) } returns mockk(relaxed = true) + } @Test fun workerEnqueuerCreatesOneTimeRequestWorkerWithParams() { @@ -463,6 +477,97 @@ class CreateDraftWorkerTest : CoroutinesTest { } } + @Test + fun workerPerformsCreateDraftRequestAndBuildsMessageFromResponseWhenSucceding() { + runBlockingTest { + // Given + val parentId = "89345" + val messageDbId = 345L + val message = Message().apply { + dbId = messageDbId + addressID = "addressId835" + messageBody = "messageBody" + sender = MessageSender("sender2342", "senderEmail@2340.com") + setLabelIDs(listOf("label", "label1", "label2")) + parsedHeaders = ParsedHeaders("recEncryption", "recAuth") + numAttachments = 3 + } + + val apiDraftRequest = mockk(relaxed = true) + val responseMessage = mockk(relaxed = true) + val apiDraftResponse = mockk { + every { messageId } returns "response_message_id" + every { this@mockk.message } returns responseMessage + } + givenMessageIdInput(messageDbId) + givenParentIdInput(parentId) + givenActionTypeInput(NONE) + givenPreviousSenderAddress("") + every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message + every { messageFactory.createDraftApiRequest(message) } returns apiDraftRequest + coEvery { apiManager.createDraft(apiDraftRequest) } returns apiDraftResponse + + // When + worker.doWork() + + // Then + coVerify { apiManager.createDraft(apiDraftRequest) } + verifySequence { + responseMessage.dbId = messageDbId + responseMessage.toList = listOf() + responseMessage.ccList = listOf() + responseMessage.bccList = listOf() + responseMessage.replyTos = listOf() + responseMessage.sender = message.sender + responseMessage.setLabelIDs(message.getEventLabelIDs()) + responseMessage.parsedHeaders = message.parsedHeaders + responseMessage.isDownloaded = true + responseMessage.setIsRead(true) + responseMessage.numAttachments = message.numAttachments + responseMessage.localId = message.messageId + } + } + } + + @Test + fun workerSavesCreatedDraftToDbAndReturnsSuccessWhenRequestSucceeds() { + runBlockingTest { + // Given + val parentId = "89345" + val messageDbId = 345L + val message = Message().apply { + dbId = messageDbId + addressID = "addressId835" + messageBody = "messageBody" + sender = MessageSender("sender2342", "senderEmail@2340.com") + setLabelIDs(listOf("label", "label1", "label2")) + parsedHeaders = ParsedHeaders("recEncryption", "recAuth") + numAttachments = 3 + } + + val apiDraftRequest = mockk(relaxed = true) + val responseMessage = mockk(relaxed = true) + val apiDraftResponse = mockk { + every { messageId } returns "response_message_id" + every { this@mockk.message } returns responseMessage + } + givenMessageIdInput(messageDbId) + givenParentIdInput(parentId) + givenActionTypeInput(NONE) + givenPreviousSenderAddress("") + every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message + every { messageFactory.createDraftApiRequest(message) } returns apiDraftRequest + coEvery { apiManager.createDraft(apiDraftRequest) } returns apiDraftResponse + + // When + val result = worker.doWork() + + // Then + verify { messageDetailsRepository.saveMessageInDB(responseMessage) } + assertEquals(ListenableWorker.Result.success(), result) + } + } + private fun givenPreviousSenderAddress(address: String) { every { parameters.inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_PREVIOUS_SENDER_ADDRESS_ID) } answers { address } } From 0b6a920e3acd2650f4291854bdd4c957413a9aca Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Mon, 14 Dec 2020 13:51:51 +0100 Subject: [PATCH 018/145] SaveDraft updates pending for send message and deletes local draft when API draft creation succeeds MAILAND-1018 --- .../models/room/pendingActions/PendingSend.kt | 28 ++--- .../android/usecase/compose/SaveDraft.kt | 51 ++++++++- .../android/worker/CreateDraftWorker.kt | 12 +- .../android/usecase/compose/SaveDraftTest.kt | 105 +++++++++++++++++- .../android/worker/CreateDraftWorkerTest.kt | 5 +- 5 files changed, 175 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/api/models/room/pendingActions/PendingSend.kt b/app/src/main/java/ch/protonmail/android/api/models/room/pendingActions/PendingSend.kt index 59304a16a..c5897435c 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/room/pendingActions/PendingSend.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/room/pendingActions/PendingSend.kt @@ -31,20 +31,16 @@ const val COLUMN_PENDING_SEND_SENT = "sent" const val COLUMN_PENDING_SEND_LOCAL_DB_ID = "local_database_id" // endregion -/** - * Created by dkadrikj on 1.10.15. - */ - @Entity(tableName = TABLE_PENDING_SEND) -class PendingSend @JvmOverloads constructor( - @PrimaryKey - @ColumnInfo(name = COLUMN_PENDING_SEND_ID) - var id: String = "", - @ColumnInfo(name = COLUMN_PENDING_SEND_MESSAGE_ID) - var messageId: String? = null, - @ColumnInfo(name = COLUMN_PENDING_SEND_OFFLINE_MESSAGE_ID) - var offlineMessageId: String? = null, - @ColumnInfo(name = COLUMN_PENDING_SEND_SENT) - var sent: Boolean? = null, - @ColumnInfo(name = COLUMN_PENDING_SEND_LOCAL_DB_ID) - var localDatabaseId: Long = 0) +data class PendingSend @JvmOverloads constructor( + @PrimaryKey + @ColumnInfo(name = COLUMN_PENDING_SEND_ID) + var id: String = "", + @ColumnInfo(name = COLUMN_PENDING_SEND_MESSAGE_ID) + var messageId: String? = null, + @ColumnInfo(name = COLUMN_PENDING_SEND_OFFLINE_MESSAGE_ID) + var offlineMessageId: String? = null, + @ColumnInfo(name = COLUMN_PENDING_SEND_SENT) + var sent: Boolean? = null, + @ColumnInfo(name = COLUMN_PENDING_SEND_LOCAL_DB_ID) + var localDatabaseId: Long = 0) diff --git a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt index 444e339a9..ff54a7369 100644 --- a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt +++ b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt @@ -19,6 +19,7 @@ package ch.protonmail.android.usecase.compose +import androidx.work.WorkInfo import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDao @@ -32,6 +33,11 @@ import ch.protonmail.android.di.CurrentUsername import ch.protonmail.android.domain.entity.Id import ch.protonmail.android.domain.entity.Name import ch.protonmail.android.worker.CreateDraftWorker +import ch.protonmail.android.worker.KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import me.proton.core.util.kotlin.DispatcherProvider import javax.inject.Inject @@ -47,7 +53,7 @@ class SaveDraft @Inject constructor( suspend operator fun invoke( params: SaveDraftParameters - ): Result = withContext(dispatchers.Io) { + ): Flow = withContext(dispatchers.Io) { val message = params.message val messageId = requireNotNull(message.messageId) val addressId = requireNotNull(message.addressID) @@ -73,12 +79,49 @@ class SaveDraft @Inject constructor( pendingActionsDao.findPendingSendByDbId(messageDbId)?.let { // TODO allow draft to be saved in this case when starting to use SaveDraft use case in PostMessageJob too - return@withContext Result.SendingInProgressError + return@withContext flowOf(Result.SendingInProgressError) } - createDraftWorker.enqueue(message, params.parentId, params.actionType, params.previousSenderAddressId) + return@withContext createDraftWorker.enqueue( + message, + params.parentId, + params.actionType, + params.previousSenderAddressId + ) + .filter { it.state.isFinished } + .map { workInfo -> + return@map handleWorkResult(workInfo, messageId) + } + + } + + private fun handleWorkResult(workInfo: WorkInfo, localDraftId: String): Result { + val workSucceeded = workInfo.state == WorkInfo.State.SUCCEEDED + + if (workSucceeded) { + val createdDraftId = workInfo.outputData.getString(KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID) + updatePendingForSendMessage(createdDraftId, localDraftId) + deleteOfflineDraft(localDraftId) + + return Result.Success + } - return@withContext Result.Success + return Result.Success + } + + private fun deleteOfflineDraft(localDraftId: String) { + val offlineDraft = messageDetailsRepository.findMessageById(localDraftId) + offlineDraft?.let { + messageDetailsRepository.deleteMessage(offlineDraft) + } + } + + private fun updatePendingForSendMessage(createdDraftId: String?, messageId: String) { + val pendingForSending = pendingActionsDao.findPendingSendByOfflineMessageId(messageId) + pendingForSending?.let { + pendingForSending.messageId = createdDraftId + pendingActionsDao.insertPendingForSend(pendingForSending) + } } sealed class Result { diff --git a/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt b/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt index 9e9f13c61..a60bc42f2 100644 --- a/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt +++ b/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt @@ -22,7 +22,7 @@ package ch.protonmail.android.worker import android.content.Context import androidx.hilt.Assisted import androidx.hilt.work.WorkerInject -import androidx.lifecycle.LiveData +import androidx.lifecycle.asFlow import androidx.work.Constraints import androidx.work.CoroutineWorker import androidx.work.NetworkType @@ -51,6 +51,7 @@ import ch.protonmail.android.domain.entity.user.Address import ch.protonmail.android.utils.base64.Base64Encoder import ch.protonmail.android.utils.extensions.deserialize import ch.protonmail.android.utils.extensions.serialize +import kotlinx.coroutines.flow.Flow import javax.inject.Inject internal const val KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_DB_ID = "keyCreateDraftMessageDbId" @@ -60,6 +61,7 @@ internal const val KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_ACTION_TYPE_SERIALIZED = internal const val KEY_INPUT_DATA_CREATE_DRAFT_PREVIOUS_SENDER_ADDRESS_ID = "keyCreateDraftPreviousSenderAddressId" internal const val KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_ERROR_ENUM = "keyCreateDraftErrorResult" +internal const val KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID = "keyCreateDraftSuccessResultDbId" private const val INPUT_MESSAGE_DB_ID_NOT_FOUND = -1L @@ -116,7 +118,9 @@ class CreateDraftWorker @WorkerInject constructor( messageDetailsRepository.saveMessageInDB(responseDraft) - return Result.success() + return Result.success( + workDataOf(KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID to response.messageId) + ) } private fun buildDraftRequestParentAttachments( @@ -215,7 +219,7 @@ class CreateDraftWorker @WorkerInject constructor( parentId: String?, actionType: Constants.MessageActionType, previousSenderAddressId: String - ): LiveData { + ): Flow { val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() @@ -232,7 +236,7 @@ class CreateDraftWorker @WorkerInject constructor( ).build() workManager.enqueue(createDraftRequest) - return workManager.getWorkInfoByIdLiveData(createDraftRequest.id) + return workManager.getWorkInfoByIdLiveData(createDraftRequest.id).asFlow() } } diff --git a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt index 2aeb50745..459a71368 100644 --- a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt +++ b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt @@ -19,6 +19,9 @@ package ch.protonmail.android.usecase.compose +import androidx.work.Data +import androidx.work.WorkInfo +import androidx.work.workDataOf import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDao @@ -36,6 +39,7 @@ import ch.protonmail.android.domain.entity.Name import ch.protonmail.android.usecase.compose.SaveDraft.Result import ch.protonmail.android.usecase.compose.SaveDraft.SaveDraftParameters import ch.protonmail.android.worker.CreateDraftWorker +import ch.protonmail.android.worker.KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every @@ -44,11 +48,15 @@ import io.mockk.impl.annotations.RelaxedMockK import io.mockk.junit5.MockKExtension import io.mockk.mockk import io.mockk.verify +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runBlockingTest import me.proton.core.test.kotlin.CoroutinesTest import org.junit.Assert import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith +import java.util.UUID @ExtendWith(MockKExtension::class) class SaveDraftTest : CoroutinesTest { @@ -197,7 +205,7 @@ class SaveDraftTest : CoroutinesTest { // Then val expectedError = Result.SendingInProgressError - Assert.assertEquals(expectedError, result) + Assert.assertEquals(expectedError, result.first()) verify(exactly = 0) { createDraftScheduler.enqueue(any(), any(), any(), any()) } } @@ -223,5 +231,100 @@ class SaveDraftTest : CoroutinesTest { verify { createDraftScheduler.enqueue(message, "parentId123", REPLY_ALL, "previousSenderId1273") } } + @Test + fun saveDraftsUpdatesPendingForSendingMessageIdWithNewApiDraftIdWhenWorkerSucceedsAndMessageIsPendingForSending() = + runBlockingTest { + // Given + val localDraftId = "8345" + val message = Message().apply { + dbId = 123L + this.messageId = "45623" + addressID = "addressId" + decryptedBody = "Message body in plain text" + localId = localDraftId + } + coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 9833L + every { pendingActionsDao.findPendingSendByDbId(9833L) } returns null + every { pendingActionsDao.findPendingSendByOfflineMessageId("45623") } answers { + PendingSend( + "234234", localDraftId, "offlineId", false, 834L + ) + } + val workOutputData = workDataOf( + KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID to "createdDraftMessageId" + ) + val workerStatusFlow = buildCreateDraftWorkerResponse(WorkInfo.State.SUCCEEDED, workOutputData) + every { + createDraftScheduler.enqueue( + message, + "parentId234", + REPLY_ALL, + "previousSenderId132423" + ) + } answers { workerStatusFlow } + + // When + saveDraft.invoke( + SaveDraftParameters(message, emptyList(), "parentId234", REPLY_ALL, "previousSenderId132423") + ).first() + + // Then + val expected = PendingSend("234234", "createdDraftMessageId", "offlineId", false, 834L) + verify { pendingActionsDao.insertPendingForSend(expected) } + } + + @Test + fun saveDraftsDeletesOfflineDraftWhenCreatingRemoteDraftThroughApiSucceds() = + runBlockingTest { + // Given + val localDraftId = "8345" + val message = Message().apply { + dbId = 123L + this.messageId = "45623" + addressID = "addressId" + decryptedBody = "Message body in plain text" + localId = localDraftId + } + coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 9833L + every { messageDetailsRepository.findMessageById("45623") } returns message + every { pendingActionsDao.findPendingSendByDbId(9833L) } returns null + every { pendingActionsDao.findPendingSendByOfflineMessageId(localDraftId) } returns PendingSend() + val workOutputData = workDataOf( + KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID to "createdDraftMessageId345" + ) + val workerStatusFlow = buildCreateDraftWorkerResponse(WorkInfo.State.SUCCEEDED, workOutputData) + every { + createDraftScheduler.enqueue( + message, + "parentId234", + REPLY_ALL, + "previousSenderId132423" + ) + } answers { workerStatusFlow } + + // When + saveDraft.invoke( + SaveDraftParameters(message, emptyList(), "parentId234", REPLY_ALL, "previousSenderId132423") + ).first() + + // Then + verify { messageDetailsRepository.deleteMessage(message) } + } + + private fun buildCreateDraftWorkerResponse( + endState: WorkInfo.State, + outputData: Data? = workDataOf() + ): Flow { + val workInfo = WorkInfo( + UUID.randomUUID(), + endState, + outputData!!, + emptyList(), + outputData, + 0 + ) + return MutableStateFlow(workInfo) + } + } diff --git a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt index b8c8118a6..23d37ef15 100644 --- a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt +++ b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt @@ -564,7 +564,10 @@ class CreateDraftWorkerTest : CoroutinesTest { // Then verify { messageDetailsRepository.saveMessageInDB(responseMessage) } - assertEquals(ListenableWorker.Result.success(), result) + val expected = ListenableWorker.Result.success( + Data.Builder().putString(KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID, "response_message_id").build() + ) + assertEquals(expected, result) } } From 6319af6bef33122b94266d28557c6cd57c7b5111 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Mon, 14 Dec 2020 17:11:10 +0100 Subject: [PATCH 019/145] SaveDraft use case uploads attachments through UploadAttachments use case This is not a complete commit, it's just to store the current state. As the existing `PostCreateDraftAttachmentsJob` does more than what `UploadAttachments` use case is doing, some changes are pending MAILAND-1018 --- .../android/usecase/compose/SaveDraft.kt | 27 ++++++----- .../android/worker/CreateDraftWorker.kt | 12 +++++ .../android/usecase/compose/SaveDraftTest.kt | 46 +++++++++++++++++++ 3 files changed, 71 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt index ff54a7369..66cc8ce7a 100644 --- a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt +++ b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt @@ -24,6 +24,7 @@ import ch.protonmail.android.activities.messageDetails.repository.MessageDetails import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDao import ch.protonmail.android.api.models.room.pendingActions.PendingUpload +import ch.protonmail.android.attachments.UploadAttachments import ch.protonmail.android.core.Constants import ch.protonmail.android.core.Constants.MessageLocationType.ALL_DRAFT import ch.protonmail.android.core.Constants.MessageLocationType.ALL_MAIL @@ -48,7 +49,8 @@ class SaveDraft @Inject constructor( private val dispatchers: DispatcherProvider, private val pendingActionsDao: PendingActionsDao, private val createDraftWorker: CreateDraftWorker.Enqueuer, - @CurrentUsername private val username: String + @CurrentUsername private val username: String, + val uploadAttachments: UploadAttachments ) { suspend operator fun invoke( @@ -90,23 +92,20 @@ class SaveDraft @Inject constructor( ) .filter { it.state.isFinished } .map { workInfo -> - return@map handleWorkResult(workInfo, messageId) - } - - } + val workSucceeded = workInfo.state == WorkInfo.State.SUCCEEDED - private fun handleWorkResult(workInfo: WorkInfo, localDraftId: String): Result { - val workSucceeded = workInfo.state == WorkInfo.State.SUCCEEDED + if (workSucceeded) { + val createdDraftId = workInfo.outputData.getString(KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID) + updatePendingForSendMessage(createdDraftId, messageId) + deleteOfflineDraft(messageId) + uploadAttachments(params.newAttachmentIds, message, addressCrypto) - if (workSucceeded) { - val createdDraftId = workInfo.outputData.getString(KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID) - updatePendingForSendMessage(createdDraftId, localDraftId) - deleteOfflineDraft(localDraftId) + return@map Result.Success + } - return Result.Success - } + return@map Result.Success + } - return Result.Success } private fun deleteOfflineDraft(localDraftId: String) { diff --git a/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt b/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt index a60bc42f2..c2c6ca2d5 100644 --- a/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt +++ b/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt @@ -121,6 +121,18 @@ class CreateDraftWorker @WorkerInject constructor( return Result.success( workDataOf(KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID to response.messageId) ) + + // TODO test whether this is needed, drop otherwise +// set inline attachments from parent message that were inline previously +// for (Attachment atta : draftMessage.getAttachments()) { +// if (parentAttachmentList != null && !parentAttachmentList.isEmpty()) { +// for (Attachment parentAtta : parentAttachmentList) { +// if (parentAtta.getKeyPackets().equals(atta.getKeyPackets())) { +// atta.setInline(parentAtta.getInline()); +// } +// } +// } +// } } private fun buildDraftRequestParentAttachments( diff --git a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt index 459a71368..a3be217d6 100644 --- a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt +++ b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt @@ -27,6 +27,7 @@ import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDao import ch.protonmail.android.api.models.room.pendingActions.PendingSend import ch.protonmail.android.api.models.room.pendingActions.PendingUpload +import ch.protonmail.android.attachments.UploadAttachments import ch.protonmail.android.core.Constants.MessageActionType.FORWARD import ch.protonmail.android.core.Constants.MessageActionType.REPLY import ch.protonmail.android.core.Constants.MessageActionType.REPLY_ALL @@ -61,6 +62,9 @@ import java.util.UUID @ExtendWith(MockKExtension::class) class SaveDraftTest : CoroutinesTest { + @RelaxedMockK + private lateinit var uploadAttachments: UploadAttachments + @RelaxedMockK private lateinit var createDraftScheduler: CreateDraftWorker.Enqueuer @@ -311,6 +315,48 @@ class SaveDraftTest : CoroutinesTest { verify { messageDetailsRepository.deleteMessage(message) } } + @Test + fun saveDraftsCallsUploadAttachmentsUseCaseToUploadNewAttachments() = + runBlockingTest { + // Given + val localDraftId = "8345" + val message = Message().apply { + dbId = 123L + this.messageId = "45623" + addressID = "addressId" + decryptedBody = "Message body in plain text" + localId = localDraftId + } + val workOutputData = workDataOf( + KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID to "createdDraftMessageId345" + ) + val workerStatusFlow = buildCreateDraftWorkerResponse(WorkInfo.State.SUCCEEDED, workOutputData) + val newAttachmentIds = listOf("2345", "453") + coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 9833L + every { messageDetailsRepository.findMessageById("45623") } returns message + every { pendingActionsDao.findPendingSendByDbId(9833L) } returns null + every { pendingActionsDao.findPendingSendByOfflineMessageId(localDraftId) } returns PendingSend() + every { + createDraftScheduler.enqueue( + message, + "parentId234", + REPLY_ALL, + "previousSenderId132423" + ) + } answers { workerStatusFlow } + val addressCrypto = mockk(relaxed = true) + every { addressCryptoFactory.create(Id("addressId"), Name(currentUsername)) } returns addressCrypto + + // When + saveDraft.invoke( + SaveDraftParameters(message, newAttachmentIds, "parentId234", REPLY_ALL, "previousSenderId132423") + ).first() + + // Then + coVerify { uploadAttachments(newAttachmentIds, message, addressCrypto) } + } + + private fun buildCreateDraftWorkerResponse( endState: WorkInfo.State, outputData: Data? = workDataOf() From 7ab2d61f2ed369d24c49c6b023f9669fe3555d02 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Mon, 14 Dec 2020 18:07:12 +0100 Subject: [PATCH 020/145] SaveDraftWorker retries saving when API call fails Wither by returing a bad status code or throwing exception. Backoff politcy is exponential with initial delay of 10 seconds, trying max 10 times MAILAND-1018 --- .../android/worker/CreateDraftWorker.kt | 74 +++++++++++------ .../android/worker/CreateDraftWorkerTest.kt | 79 ++++++++++++++++++- 2 files changed, 129 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt b/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt index c2c6ca2d5..d5ee47c0d 100644 --- a/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt +++ b/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt @@ -23,6 +23,7 @@ import android.content.Context import androidx.hilt.Assisted import androidx.hilt.work.WorkerInject import androidx.lifecycle.asFlow +import androidx.work.BackoffPolicy import androidx.work.Constraints import androidx.work.CoroutineWorker import androidx.work.NetworkType @@ -37,6 +38,7 @@ import ch.protonmail.android.api.models.messages.receive.MessageFactory import ch.protonmail.android.api.models.room.messages.Attachment import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.api.models.room.messages.MessageSender +import ch.protonmail.android.api.segments.TEN_SECONDS import ch.protonmail.android.api.utils.Fields import ch.protonmail.android.core.Constants import ch.protonmail.android.core.Constants.MessageActionType.FORWARD @@ -52,6 +54,7 @@ import ch.protonmail.android.utils.base64.Base64Encoder import ch.protonmail.android.utils.extensions.deserialize import ch.protonmail.android.utils.extensions.serialize import kotlinx.coroutines.flow.Flow +import java.time.Duration import javax.inject.Inject internal const val KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_DB_ID = "keyCreateDraftMessageDbId" @@ -64,6 +67,7 @@ internal const val KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_ERROR_ENUM = "keyCreateDr internal const val KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID = "keyCreateDraftSuccessResultDbId" private const val INPUT_MESSAGE_DB_ID_NOT_FOUND = -1L +private const val SAVE_DRAFT_MAX_RETRIES = 10 class CreateDraftWorker @WorkerInject constructor( @Assisted context: Context, @@ -76,6 +80,7 @@ class CreateDraftWorker @WorkerInject constructor( val apiManager: ProtonMailApiManager ) : CoroutineWorker(context, params) { + internal var retries: Int = 0 override suspend fun doWork(): Result { val message = messageDetailsRepository.findMessageByMessageDbId(getInputMessageDbId()) @@ -100,26 +105,23 @@ class CreateDraftWorker @WorkerInject constructor( createDraftRequest.addMessageBody(Fields.Message.SELF, encryptedMessage); createDraftRequest.setSender(buildMessageSender(message, senderAddress)) - val response = apiManager.createDraft(createDraftRequest) - - val responseDraft = response.message - responseDraft.dbId = message.dbId - responseDraft.toList = message.toList - responseDraft.ccList = message.ccList - responseDraft.bccList = message.bccList - responseDraft.replyTos = message.replyTos - responseDraft.sender = message.sender - responseDraft.setLabelIDs(message.getEventLabelIDs()) - responseDraft.parsedHeaders = message.parsedHeaders - responseDraft.isDownloaded = true - responseDraft.setIsRead(true) - responseDraft.numAttachments = message.numAttachments - responseDraft.localId = message.messageId - - messageDetailsRepository.saveMessageInDB(responseDraft) - - return Result.success( - workDataOf(KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID to response.messageId) + return runCatching { + apiManager.createDraft(createDraftRequest) + }.fold( + onSuccess = { response -> + if (response.code != Constants.RESPONSE_CODE_OK) { + return handleFailure() + } + + val responseDraft = response.message + updateStoredLocalDraft(responseDraft, message) + Result.success( + workDataOf(KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID to response.messageId) + ) + }, + onFailure = { + handleFailure() + } ) // TODO test whether this is needed, drop otherwise @@ -135,6 +137,31 @@ class CreateDraftWorker @WorkerInject constructor( // } } + private fun updateStoredLocalDraft(apiDraft: Message, localDraft: Message) { + apiDraft.dbId = localDraft.dbId + apiDraft.toList = localDraft.toList + apiDraft.ccList = localDraft.ccList + apiDraft.bccList = localDraft.bccList + apiDraft.replyTos = localDraft.replyTos + apiDraft.sender = localDraft.sender + apiDraft.setLabelIDs(localDraft.getEventLabelIDs()) + apiDraft.parsedHeaders = localDraft.parsedHeaders + apiDraft.isDownloaded = true + apiDraft.setIsRead(true) + apiDraft.numAttachments = localDraft.numAttachments + apiDraft.localId = localDraft.messageId + + messageDetailsRepository.saveMessageInDB(apiDraft) + } + + private fun handleFailure(): Result { + if (retries <= SAVE_DRAFT_MAX_RETRIES) { + retries++ + return Result.retry() + } + return failureWithError(CreateDraftWorkerErrors.ServerError) + } + private fun buildDraftRequestParentAttachments( attachments: List?, senderAddress: Address @@ -221,7 +248,8 @@ class CreateDraftWorker @WorkerInject constructor( inputData.getLong(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_DB_ID, INPUT_MESSAGE_DB_ID_NOT_FOUND) enum class CreateDraftWorkerErrors { - MessageNotFound + MessageNotFound, + ServerError } class Enqueuer @Inject constructor(private val workManager: WorkManager) { @@ -245,7 +273,9 @@ class CreateDraftWorker @WorkerInject constructor( KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_ACTION_TYPE_SERIALIZED to actionType.serialize(), KEY_INPUT_DATA_CREATE_DRAFT_PREVIOUS_SENDER_ADDRESS_ID to previousSenderAddressId ) - ).build() + ) + .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, Duration.ofSeconds(TEN_SECONDS)) + .build() workManager.enqueue(createDraftRequest) return workManager.getWorkInfoByIdLiveData(createDraftRequest.id).asFlow() diff --git a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt index 23d37ef15..8585862cc 100644 --- a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt +++ b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt @@ -20,6 +20,7 @@ package ch.protonmail.android.worker import android.content.Context +import androidx.work.BackoffPolicy import androidx.work.Data import androidx.work.ListenableWorker import androidx.work.NetworkType @@ -69,6 +70,7 @@ import org.junit.Assert.assertEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith +import java.io.IOException @ExtendWith(MockKExtension::class) class CreateDraftWorkerTest : CoroutinesTest { @@ -131,8 +133,9 @@ class CreateDraftWorkerTest : CoroutinesTest { ) // Then - val constraints = requestSlot.captured.workSpec.constraints - val inputData = requestSlot.captured.workSpec.input + val workSpec = requestSlot.captured.workSpec + val constraints = workSpec.constraints + val inputData = workSpec.input val actualMessageDbId = inputData.getLong(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_DB_ID, -1) val actualMessageLocalId = inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_LOCAL_ID) val actualMessageParentId = inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_PARENT_ID) @@ -144,6 +147,8 @@ class CreateDraftWorkerTest : CoroutinesTest { assertEquals(messageActionType.serialize(), actualMessageActionType) assertEquals(previousSenderAddressId, actualPreviousSenderAddress) assertEquals(NetworkType.CONNECTED, constraints.requiredNetworkType) + assertEquals(BackoffPolicy.EXPONENTIAL, workSpec.backoffPolicy) + assertEquals(10000, workSpec.backoffDelayDuration) verify { workManager.getWorkInfoByIdLiveData(any()) } } } @@ -496,6 +501,7 @@ class CreateDraftWorkerTest : CoroutinesTest { val apiDraftRequest = mockk(relaxed = true) val responseMessage = mockk(relaxed = true) val apiDraftResponse = mockk { + every { code } returns 1000 every { messageId } returns "response_message_id" every { this@mockk.message } returns responseMessage } @@ -548,6 +554,7 @@ class CreateDraftWorkerTest : CoroutinesTest { val apiDraftRequest = mockk(relaxed = true) val responseMessage = mockk(relaxed = true) val apiDraftResponse = mockk { + every { code } returns 1000 every { messageId } returns "response_message_id" every { this@mockk.message } returns responseMessage } @@ -571,6 +578,74 @@ class CreateDraftWorkerTest : CoroutinesTest { } } + @Test + fun workerRetriesSavingDraftWhenApiRequestFailsAndMaxTriesWereNotReached() { + runBlockingTest { + // Given + val parentId = "89345" + val messageDbId = 345L + val message = Message().apply { + dbId = messageDbId + addressID = "addressId835" + messageBody = "messageBody" + } + val errorAPIResponse = mockk { + every { code } returns 500 + every { error } returns "Internal Error: Draft not created" + } + givenMessageIdInput(messageDbId) + givenParentIdInput(parentId) + givenActionTypeInput(NONE) + givenPreviousSenderAddress("") + every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message + every { messageFactory.createDraftApiRequest(message) } returns mockk(relaxed = true) + coEvery { apiManager.createDraft(any()) } returns errorAPIResponse + worker.retries = 0 + + // When + val result = worker.doWork() + + // Then + val expected = ListenableWorker.Result.retry() + assertEquals(expected, result) + assertEquals(1, worker.retries) + } + } + + @Test + fun workerReturnsFailureWithErrorWhenAPIRequestFailsAndMaxTriesWereReached() { + runBlockingTest { + // Given + val parentId = "89345" + val messageDbId = 345L + val message = Message().apply { + dbId = messageDbId + addressID = "addressId835" + messageBody = "messageBody" + } + givenMessageIdInput(messageDbId) + givenParentIdInput(parentId) + givenActionTypeInput(NONE) + givenPreviousSenderAddress("") + every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message + every { messageFactory.createDraftApiRequest(message) } returns mockk(relaxed = true) + coEvery { apiManager.createDraft(any()) } throws IOException("Error performing request") + worker.retries = 11 + + // When + val result = worker.doWork() + + // Then + val expected = ListenableWorker.Result.failure( + Data.Builder().putString( + KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_ERROR_ENUM, + CreateDraftWorker.CreateDraftWorkerErrors.ServerError.name + ).build() + ) + assertEquals(expected, result) + } + } + private fun givenPreviousSenderAddress(address: String) { every { parameters.inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_PREVIOUS_SENDER_ADDRESS_ID) } answers { address } } From 3c5757d1b27668dbe886c8c8d2e4a6831837b742 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Tue, 15 Dec 2020 11:54:55 +0100 Subject: [PATCH 021/145] SaveDraft returns error when create draft / upload attachments fails - Use newly created draft instead of localDraft to upload attachments MAILAND-1018 --- .../android/usecase/compose/SaveDraft.kt | 38 +++++--- .../android/usecase/compose/SaveDraftTest.kt | 96 +++++++++++++++++-- 2 files changed, 117 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt index 66cc8ce7a..5e2769006 100644 --- a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt +++ b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt @@ -84,28 +84,42 @@ class SaveDraft @Inject constructor( return@withContext flowOf(Result.SendingInProgressError) } - return@withContext createDraftWorker.enqueue( - message, + return@withContext saveDraftOnline(message, params, messageId, addressCrypto) + + } + + private fun saveDraftOnline( + localDraft: Message, + params: SaveDraftParameters, + localDraftId: String, + addressCrypto: AddressCrypto + ): Flow { + return createDraftWorker.enqueue( + localDraft, params.parentId, params.actionType, params.previousSenderAddressId ) .filter { it.state.isFinished } .map { workInfo -> - val workSucceeded = workInfo.state == WorkInfo.State.SUCCEEDED - - if (workSucceeded) { + if (workInfo.state == WorkInfo.State.SUCCEEDED) { val createdDraftId = workInfo.outputData.getString(KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID) - updatePendingForSendMessage(createdDraftId, messageId) - deleteOfflineDraft(messageId) - uploadAttachments(params.newAttachmentIds, message, addressCrypto) + + updatePendingForSendMessage(createdDraftId, localDraftId) + deleteOfflineDraft(localDraftId) + + messageDetailsRepository.findMessageById(createdDraftId.orEmpty())?.let { + val uploadResult = uploadAttachments(params.newAttachmentIds, it, addressCrypto) + if (uploadResult is UploadAttachments.Result.Failure) { + return@map Result.UploadDraftAttachmentsFailed + } + } return@map Result.Success } - return@map Result.Success + return@map Result.OnlineDraftCreationFailed } - } private fun deleteOfflineDraft(localDraftId: String) { @@ -124,8 +138,10 @@ class SaveDraft @Inject constructor( } sealed class Result { - object SendingInProgressError : Result() object Success : Result() + object SendingInProgressError : Result() + object OnlineDraftCreationFailed : Result() + object UploadDraftAttachmentsFailed : Result() } data class SaveDraftParameters( diff --git a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt index a3be217d6..81a8bb3f4 100644 --- a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt +++ b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt @@ -28,6 +28,7 @@ import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDao import ch.protonmail.android.api.models.room.pendingActions.PendingSend import ch.protonmail.android.api.models.room.pendingActions.PendingUpload import ch.protonmail.android.attachments.UploadAttachments +import ch.protonmail.android.attachments.UploadAttachments.Result.Failure import ch.protonmail.android.core.Constants.MessageActionType.FORWARD import ch.protonmail.android.core.Constants.MessageActionType.REPLY import ch.protonmail.android.core.Constants.MessageActionType.REPLY_ALL @@ -39,7 +40,9 @@ import ch.protonmail.android.domain.entity.Id import ch.protonmail.android.domain.entity.Name import ch.protonmail.android.usecase.compose.SaveDraft.Result import ch.protonmail.android.usecase.compose.SaveDraft.SaveDraftParameters -import ch.protonmail.android.worker.CreateDraftWorker +import ch.protonmail.android.worker.CreateDraftWorker.CreateDraftWorkerErrors +import ch.protonmail.android.worker.CreateDraftWorker.Enqueuer +import ch.protonmail.android.worker.KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_ERROR_ENUM import ch.protonmail.android.worker.KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID import io.mockk.coEvery import io.mockk.coVerify @@ -54,7 +57,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runBlockingTest import me.proton.core.test.kotlin.CoroutinesTest -import org.junit.Assert +import org.junit.Assert.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import java.util.UUID @@ -66,7 +69,7 @@ class SaveDraftTest : CoroutinesTest { private lateinit var uploadAttachments: UploadAttachments @RelaxedMockK - private lateinit var createDraftScheduler: CreateDraftWorker.Enqueuer + private lateinit var createDraftScheduler: Enqueuer @RelaxedMockK private lateinit var pendingActionsDao: PendingActionsDao @@ -209,7 +212,7 @@ class SaveDraftTest : CoroutinesTest { // Then val expectedError = Result.SendingInProgressError - Assert.assertEquals(expectedError, result.first()) + assertEquals(expectedError, result.first()) verify(exactly = 0) { createDraftScheduler.enqueue(any(), any(), any(), any()) } } @@ -254,6 +257,7 @@ class SaveDraftTest : CoroutinesTest { "234234", localDraftId, "offlineId", false, 834L ) } + coEvery { uploadAttachments(any(), any(), any()) } returns UploadAttachments.Result.Success val workOutputData = workDataOf( KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID to "createdDraftMessageId" ) @@ -293,6 +297,7 @@ class SaveDraftTest : CoroutinesTest { every { messageDetailsRepository.findMessageById("45623") } returns message every { pendingActionsDao.findPendingSendByDbId(9833L) } returns null every { pendingActionsDao.findPendingSendByOfflineMessageId(localDraftId) } returns PendingSend() + coEvery { uploadAttachments(any(), any(), any()) } returns UploadAttachments.Result.Success val workOutputData = workDataOf( KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID to "createdDraftMessageId345" ) @@ -327,6 +332,7 @@ class SaveDraftTest : CoroutinesTest { decryptedBody = "Message body in plain text" localId = localDraftId } + val apiDraft = message.copy(messageId = "createdDraftMessageId345") val workOutputData = workDataOf( KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID to "createdDraftMessageId345" ) @@ -334,8 +340,10 @@ class SaveDraftTest : CoroutinesTest { val newAttachmentIds = listOf("2345", "453") coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 9833L every { messageDetailsRepository.findMessageById("45623") } returns message + every { messageDetailsRepository.findMessageById("createdDraftMessageId345") } returns apiDraft every { pendingActionsDao.findPendingSendByDbId(9833L) } returns null every { pendingActionsDao.findPendingSendByOfflineMessageId(localDraftId) } returns PendingSend() + coEvery { uploadAttachments(any(), apiDraft, any()) } returns UploadAttachments.Result.Success every { createDraftScheduler.enqueue( message, @@ -353,10 +361,86 @@ class SaveDraftTest : CoroutinesTest { ).first() // Then - coVerify { uploadAttachments(newAttachmentIds, message, addressCrypto) } + coVerify { uploadAttachments(newAttachmentIds, apiDraft, addressCrypto) } } - + @Test + fun saveDraftsReturnsFailureWhenWorkerFailsCreatingDraftOnAPI() = + runBlockingTest { + // Given + val localDraftId = "8345" + val message = Message().apply { + dbId = 123L + this.messageId = "45623" + addressID = "addressId" + decryptedBody = "Message body in plain text" + localId = localDraftId + } + val workOutputData = workDataOf( + KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_ERROR_ENUM to CreateDraftWorkerErrors.ServerError.name + ) + val workerStatusFlow = buildCreateDraftWorkerResponse(WorkInfo.State.FAILED, workOutputData) + coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 9833L + every { messageDetailsRepository.findMessageById("45623") } returns message + every { pendingActionsDao.findPendingSendByDbId(9833L) } returns null + every { + createDraftScheduler.enqueue( + message, + "parentId234", + REPLY_ALL, + "previousSenderId132423" + ) + } answers { workerStatusFlow } + + // When + val result = saveDraft.invoke( + SaveDraftParameters(message, emptyList(), "parentId234", REPLY_ALL, "previousSenderId132423") + ).first() + + // Then + assertEquals(Result.OnlineDraftCreationFailed, result) + } + + @Test + fun saveDraftsReturnsErrorWhenUploadingNewAttachmentsFails() = + runBlockingTest { + // Given + val localDraftId = "8345" + val message = Message().apply { + dbId = 123L + this.messageId = "45623" + addressID = "addressId" + decryptedBody = "Message body in plain text" + localId = localDraftId + } + val newAttachmentIds = listOf("2345", "453") + val workOutputData = workDataOf( + KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID to "newDraftId" + ) + val workerStatusFlow = buildCreateDraftWorkerResponse(WorkInfo.State.SUCCEEDED, workOutputData) + coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 9833L + every { messageDetailsRepository.findMessageById("45623") } returns message + every { pendingActionsDao.findPendingSendByDbId(9833L) } returns null + coEvery { uploadAttachments(newAttachmentIds, any(), any()) } returns Failure("Can't upload attachments") + every { + createDraftScheduler.enqueue( + message, + "parentId234", + REPLY, + "previousSenderId132423" + ) + } answers { workerStatusFlow } + + // When + val result = saveDraft.invoke( + SaveDraftParameters(message, newAttachmentIds, "parentId234", REPLY, "previousSenderId132423") + ).first() + + // Then + assertEquals(Result.UploadDraftAttachmentsFailed, result) + } + + private fun buildCreateDraftWorkerResponse( endState: WorkInfo.State, outputData: Data? = workDataOf() From 7b90d6cd481eff4a3ccc85eae247c533a7fcb062 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Tue, 15 Dec 2020 19:10:18 +0100 Subject: [PATCH 022/145] SaveDraft removes message from pending uoload when upload succeeds MAILAND-1018 --- .../compose/ComposeMessageViewModel.kt | 5 +- .../android/usecase/compose/SaveDraft.kt | 36 ++++++--- .../android/worker/CreateDraftWorker.kt | 13 ++- .../android/usecase/compose/SaveDraftTest.kt | 79 ++++++++++++++++--- 4 files changed, 104 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt index 2b741795e..f9e3a1312 100644 --- a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt +++ b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt @@ -67,6 +67,7 @@ import io.reactivex.Observable import io.reactivex.Single import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import me.proton.core.util.kotlin.DispatcherProvider @@ -464,7 +465,9 @@ class ComposeMessageViewModel @Inject constructor( _actionId, _oldSenderAddressId ) - ) + ).collect { + Timber.d("Saving draft result $it") + } _oldSenderAddressId = "" setIsDirty(false) diff --git a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt index 5e2769006..a39384757 100644 --- a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt +++ b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt @@ -41,6 +41,7 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import me.proton.core.util.kotlin.DispatcherProvider +import timber.log.Timber import javax.inject.Inject class SaveDraft @Inject constructor( @@ -56,6 +57,8 @@ class SaveDraft @Inject constructor( suspend operator fun invoke( params: SaveDraftParameters ): Flow = withContext(dispatchers.Io) { + Timber.i("Saving Draft for messageId ${params.message.messageId}") + val message = params.message val messageId = requireNotNull(message.messageId) val addressId = requireNotNull(message.addressID) @@ -73,6 +76,7 @@ class SaveDraft @Inject constructor( ) val messageDbId = messageDetailsRepository.saveMessageLocally(message) + // TODO this is likely uneeded as we never check pending drafts anywhere messageDetailsRepository.insertPendingDraft(messageDbId) if (params.newAttachmentIds.isNotEmpty()) { @@ -102,23 +106,29 @@ class SaveDraft @Inject constructor( ) .filter { it.state.isFinished } .map { workInfo -> - if (workInfo.state == WorkInfo.State.SUCCEEDED) { - val createdDraftId = workInfo.outputData.getString(KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID) - - updatePendingForSendMessage(createdDraftId, localDraftId) - deleteOfflineDraft(localDraftId) - - messageDetailsRepository.findMessageById(createdDraftId.orEmpty())?.let { - val uploadResult = uploadAttachments(params.newAttachmentIds, it, addressCrypto) - if (uploadResult is UploadAttachments.Result.Failure) { - return@map Result.UploadDraftAttachmentsFailed + withContext(dispatchers.Io) { + if (workInfo.state == WorkInfo.State.SUCCEEDED) { + val createdDraftId = workInfo.outputData.getString(KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID) + Timber.d( + "Saving Draft to API for messageId $localDraftId succeeded. Created draftId = $createdDraftId" + ) + + updatePendingForSendMessage(createdDraftId, localDraftId) + deleteOfflineDraft(localDraftId) + + messageDetailsRepository.findMessageById(createdDraftId.orEmpty())?.let { + val uploadResult = uploadAttachments(params.newAttachmentIds, it, addressCrypto) + if (uploadResult is UploadAttachments.Result.Failure) { + return@withContext Result.UploadDraftAttachmentsFailed + } + pendingActionsDao.deletePendingUploadByMessageId(localDraftId) + return@withContext Result.Success } } - return@map Result.Success + Timber.e("Saving Draft to API for messageId $localDraftId FAILED.") + return@withContext Result.OnlineDraftCreationFailed } - - return@map Result.OnlineDraftCreationFailed } } diff --git a/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt b/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt index d5ee47c0d..43f130b63 100644 --- a/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt +++ b/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt @@ -54,6 +54,7 @@ import ch.protonmail.android.utils.base64.Base64Encoder import ch.protonmail.android.utils.extensions.deserialize import ch.protonmail.android.utils.extensions.serialize import kotlinx.coroutines.flow.Flow +import timber.log.Timber import java.time.Duration import javax.inject.Inject @@ -110,17 +111,18 @@ class CreateDraftWorker @WorkerInject constructor( }.fold( onSuccess = { response -> if (response.code != Constants.RESPONSE_CODE_OK) { - return handleFailure() + return handleFailure(response.error) } val responseDraft = response.message updateStoredLocalDraft(responseDraft, message) + Timber.i("Create Draft Worker API call succeeded") Result.success( workDataOf(KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID to response.messageId) ) }, onFailure = { - handleFailure() + handleFailure(it.message) } ) @@ -151,14 +153,17 @@ class CreateDraftWorker @WorkerInject constructor( apiDraft.numAttachments = localDraft.numAttachments apiDraft.localId = localDraft.messageId - messageDetailsRepository.saveMessageInDB(apiDraft) + val id = messageDetailsRepository.saveMessageInDB(apiDraft) + Timber.d("Saved API draft in DB with DB id $id") } - private fun handleFailure(): Result { + private fun handleFailure(error: String?): Result { if (retries <= SAVE_DRAFT_MAX_RETRIES) { retries++ + Timber.w("Create Draft Worker API call FAILED with error = $error. Retrying...") return Result.retry() } + Timber.e("Create Draft Worker API call failed all the retries. error = $error. FAILING") return failureWithError(CreateDraftWorkerErrors.ServerError) } diff --git a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt index 81a8bb3f4..8b385a244 100644 --- a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt +++ b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt @@ -86,7 +86,7 @@ class SaveDraftTest : CoroutinesTest { private val currentUsername = "username" @Test - fun saveDraftSavesEncryptedDraftMessageToDb() = + fun saveDraftSavesEncryptedDraftMessageToDb() { runBlockingTest { // Given val message = Message().apply { @@ -117,9 +117,10 @@ class SaveDraftTest : CoroutinesTest { ) coVerify { messageDetailsRepository.saveMessageLocally(expectedMessage) } } + } @Test - fun saveDraftInsertsPendingDraftInPendingActionsDatabase() = + fun saveDraftInsertsPendingDraftInPendingActionsDatabase() { runBlockingTest { // Given val message = Message().apply { @@ -142,9 +143,10 @@ class SaveDraftTest : CoroutinesTest { // Then coVerify { messageDetailsRepository.insertPendingDraft(123L) } } + } @Test - fun saveDraftsInsertsPendingUploadWhenThereAreNewAttachments() = + fun saveDraftsInsertsPendingUploadWhenThereAreNewAttachments() { runBlockingTest { // Given val message = Message().apply { @@ -168,9 +170,10 @@ class SaveDraftTest : CoroutinesTest { // Then verify { pendingActionsDao.insertPendingForUpload(PendingUpload("456")) } } + } @Test - fun saveDraftsDoesNotInsertsPendingUploadWhenThereAreNoNewAttachments() = + fun saveDraftsDoesNotInsertsPendingUploadWhenThereAreNoNewAttachments() { runBlockingTest { // Given val message = Message().apply { @@ -189,9 +192,10 @@ class SaveDraftTest : CoroutinesTest { // Then verify(exactly = 0) { pendingActionsDao.insertPendingForUpload(any()) } } + } @Test - fun sendDraftReturnsSendingInProgressErrorWhenMessageIsAlreadyBeingSent() = + fun sendDraftReturnsSendingInProgressErrorWhenMessageIsAlreadyBeingSent() { runBlockingTest { // Given val messageDbId = 345L @@ -215,9 +219,10 @@ class SaveDraftTest : CoroutinesTest { assertEquals(expectedError, result.first()) verify(exactly = 0) { createDraftScheduler.enqueue(any(), any(), any(), any()) } } + } @Test - fun saveDraftsSchedulesCreateDraftWorker() = + fun saveDraftsSchedulesCreateDraftWorker() { runBlockingTest { // Given val message = Message().apply { @@ -237,9 +242,10 @@ class SaveDraftTest : CoroutinesTest { // Then verify { createDraftScheduler.enqueue(message, "parentId123", REPLY_ALL, "previousSenderId1273") } } + } @Test - fun saveDraftsUpdatesPendingForSendingMessageIdWithNewApiDraftIdWhenWorkerSucceedsAndMessageIsPendingForSending() = + fun saveDraftsUpdatesPendingForSendingMessageIdWithNewApiDraftIdWhenWorkerSucceedsAndMessageIsPendingForSending() { runBlockingTest { // Given val localDraftId = "8345" @@ -280,9 +286,10 @@ class SaveDraftTest : CoroutinesTest { val expected = PendingSend("234234", "createdDraftMessageId", "offlineId", false, 834L) verify { pendingActionsDao.insertPendingForSend(expected) } } + } @Test - fun saveDraftsDeletesOfflineDraftWhenCreatingRemoteDraftThroughApiSucceds() = + fun saveDraftsDeletesOfflineDraftWhenCreatingRemoteDraftThroughApiSucceds() { runBlockingTest { // Given val localDraftId = "8345" @@ -319,9 +326,10 @@ class SaveDraftTest : CoroutinesTest { // Then verify { messageDetailsRepository.deleteMessage(message) } } + } @Test - fun saveDraftsCallsUploadAttachmentsUseCaseToUploadNewAttachments() = + fun saveDraftsCallsUploadAttachmentsUseCaseToUploadNewAttachments() { runBlockingTest { // Given val localDraftId = "8345" @@ -363,9 +371,10 @@ class SaveDraftTest : CoroutinesTest { // Then coVerify { uploadAttachments(newAttachmentIds, apiDraft, addressCrypto) } } + } @Test - fun saveDraftsReturnsFailureWhenWorkerFailsCreatingDraftOnAPI() = + fun saveDraftsReturnsFailureWhenWorkerFailsCreatingDraftOnAPI() { runBlockingTest { // Given val localDraftId = "8345" @@ -392,6 +401,7 @@ class SaveDraftTest : CoroutinesTest { ) } answers { workerStatusFlow } + // When val result = saveDraft.invoke( SaveDraftParameters(message, emptyList(), "parentId234", REPLY_ALL, "previousSenderId132423") @@ -400,9 +410,10 @@ class SaveDraftTest : CoroutinesTest { // Then assertEquals(Result.OnlineDraftCreationFailed, result) } + } @Test - fun saveDraftsReturnsErrorWhenUploadingNewAttachmentsFails() = + fun saveDraftsReturnsErrorWhenUploadingNewAttachmentsFails() { runBlockingTest { // Given val localDraftId = "8345" @@ -439,6 +450,52 @@ class SaveDraftTest : CoroutinesTest { // Then assertEquals(Result.UploadDraftAttachmentsFailed, result) } + } + + @Test + fun saveDraftRemovesMessageFromPendingForUploadListWhenUploadSucceeds() { + runBlockingTest { + // Given + val localDraftId = "8345" + val message = Message().apply { + dbId = 123L + this.messageId = "45623" + addressID = "addressId" + decryptedBody = "Message body in plain text" + localId = localDraftId + } + val apiDraft = message.copy(messageId = "createdDraftMessageId345") + val workOutputData = workDataOf( + KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID to "createdDraftMessageId345" + ) + val workerStatusFlow = buildCreateDraftWorkerResponse(WorkInfo.State.SUCCEEDED, workOutputData) + val newAttachmentIds = listOf("2345", "453") + coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 9833L + every { messageDetailsRepository.findMessageById("45623") } returns message + every { messageDetailsRepository.findMessageById("createdDraftMessageId345") } returns apiDraft + every { pendingActionsDao.findPendingSendByDbId(9833L) } returns null + every { pendingActionsDao.findPendingSendByOfflineMessageId(localDraftId) } returns PendingSend() + coEvery { uploadAttachments(any(), apiDraft, any()) } returns UploadAttachments.Result.Success + every { + createDraftScheduler.enqueue( + message, + "parentId234", + REPLY_ALL, + "previousSenderId132423" + ) + } answers { workerStatusFlow } + val addressCrypto = mockk(relaxed = true) + every { addressCryptoFactory.create(Id("addressId"), Name(currentUsername)) } returns addressCrypto + + // When + saveDraft.invoke( + SaveDraftParameters(message, newAttachmentIds, "parentId234", REPLY_ALL, "previousSenderId132423") + ).first() + + // Then + verify { pendingActionsDao.deletePendingUploadByMessageId("45623") } + } + } private fun buildCreateDraftWorkerResponse( From 474621d7cbb8fa2ad1655c52406e70d8f730508a Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Wed, 16 Dec 2020 17:00:32 +0100 Subject: [PATCH 023/145] ComposeMessageViewModel observes SaveDraft result instead of listening to the DraftCreatedEvent on the Bus - Removed some validations that are not needed anymore MAILAND-1018 --- .../ComposeMessageActivity.java | 28 +++--------- .../compose/ComposeMessageViewModel.kt | 36 ++++++++------- .../android/usecase/compose/SaveDraft.kt | 12 ++--- .../attachments/UploadAttachmentsTest.kt | 4 -- .../compose/ComposeMessageViewModelTest.kt | 34 +++++++++++++- .../android/usecase/compose/SaveDraftTest.kt | 5 ++- .../extensions/InstantExecutorExtension.kt | 45 +++++++++++++++++++ 7 files changed, 113 insertions(+), 51 deletions(-) create mode 100644 app/src/test/java/ch/protonmail/android/utils/extensions/InstantExecutorExtension.kt diff --git a/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java b/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java index 513934e27..cb1a7152a 100644 --- a/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java +++ b/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java @@ -136,7 +136,6 @@ import ch.protonmail.android.events.AttachmentFailedEvent; import ch.protonmail.android.events.ContactEvent; import ch.protonmail.android.events.DownloadEmbeddedImagesEvent; -import ch.protonmail.android.events.DraftCreatedEvent; import ch.protonmail.android.events.FetchDraftDetailEvent; import ch.protonmail.android.events.FetchMessageDetailEvent; import ch.protonmail.android.events.HumanVerifyOptionsEvent; @@ -187,7 +186,6 @@ import static ch.protonmail.android.attachments.ImportAttachmentsWorkerKt.KEY_INPUT_DATA_COMPOSER_INSTANCE_ID; import static ch.protonmail.android.attachments.ImportAttachmentsWorkerKt.KEY_INPUT_DATA_FILE_URIS_STRING_ARRAY; import static ch.protonmail.android.settings.pin.ValidatePinActivityKt.EXTRA_ATTACHMENT_IMPORT_EVENT; -import static ch.protonmail.android.settings.pin.ValidatePinActivityKt.EXTRA_DRAFT_CREATED_EVENT; import static ch.protonmail.android.settings.pin.ValidatePinActivityKt.EXTRA_DRAFT_DETAILS_EVENT; import static ch.protonmail.android.settings.pin.ValidatePinActivityKt.EXTRA_MESSAGE_DETAIL_EVENT; @@ -469,10 +467,11 @@ private void observeSetup() { composeMessageViewModel.getOpenAttachmentsScreenResult().observe(ComposeMessageActivity.this, new AddAttachmentsObserver()); composeMessageViewModel.getMessageDraftResult().observe(ComposeMessageActivity.this, new OnDraftCreatedObserver(TextUtils.isEmpty(mAction))); composeMessageViewModel.getSavingDraftComplete().observe(this, event -> { - if (event != null) { - DraftCreatedEvent draftEvent = event.getContentIfNotHandled(); - onDraftCreatedEvent(draftEvent); + if (mUpdateDraftPmMeChanged) { + composeMessageViewModel.setBeforeSaveDraft(true, mComposeBodyEditText.getText().toString()); + mUpdateDraftPmMeChanged = false; } + disableSendButton(false); }); composeMessageViewModel.getDbIdWatcher().observe(ComposeMessageActivity.this, new SendMessageObserver()); @@ -995,18 +994,6 @@ public void onPostImportAttachmentEvent(PostImportAttachmentEvent event) { } } - private void onDraftCreatedEvent(final DraftCreatedEvent event) { - String draftId = composeMessageViewModel.getDraftId(); - if (event == null || !draftId.equals(event.getOldMessageId())) { - return; - } - composeMessageViewModel.onDraftCreated(event); - if (mUpdateDraftPmMeChanged) { - composeMessageViewModel.setBeforeSaveDraft(true, mComposeBodyEditText.getText().toString()); - mUpdateDraftPmMeChanged = false; - } - } - @Override protected void onStart() { super.onStart(); @@ -1018,7 +1005,6 @@ protected void onStart() { contactsPermissionHelper.checkPermission(); } composeMessageViewModel.insertPendingDraft(); -// mToRecipientsView.invalidateRecipients(); } @Override @@ -1492,19 +1478,15 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { onPostImportAttachmentEvent((PostImportAttachmentEvent) attachmentExtra); } composeMessageViewModel.setBeforeSaveDraft(false, mComposeBodyEditText.getText().toString()); - } else if (data.hasExtra(EXTRA_MESSAGE_DETAIL_EVENT) || data.hasExtra(EXTRA_DRAFT_DETAILS_EVENT) || data.hasExtra(EXTRA_DRAFT_CREATED_EVENT)) { + } else if (data.hasExtra(EXTRA_MESSAGE_DETAIL_EVENT) || data.hasExtra(EXTRA_DRAFT_DETAILS_EVENT)) { FetchMessageDetailEvent messageDetailEvent = (FetchMessageDetailEvent) data.getSerializableExtra(EXTRA_MESSAGE_DETAIL_EVENT); FetchDraftDetailEvent draftDetailEvent = (FetchDraftDetailEvent) data.getSerializableExtra(EXTRA_DRAFT_DETAILS_EVENT); - DraftCreatedEvent draftCreatedEvent = (DraftCreatedEvent) data.getSerializableExtra(EXTRA_DRAFT_CREATED_EVENT); if (messageDetailEvent != null) { composeMessageViewModel.onFetchMessageDetailEvent(messageDetailEvent); } if (draftDetailEvent != null) { onFetchDraftDetailEvent(draftDetailEvent); } - if (draftCreatedEvent != null) { - onDraftCreatedEvent(draftCreatedEvent); - } } mToRecipientsView.requestFocus(); UiUtil.toggleKeyboard(this, mToRecipientsView); diff --git a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt index f9e3a1312..88787e787 100644 --- a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt +++ b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt @@ -74,7 +74,6 @@ import me.proton.core.util.kotlin.DispatcherProvider import timber.log.Timber import java.util.HashMap import java.util.UUID -import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject import kotlin.collections.set @@ -105,8 +104,7 @@ class ComposeMessageViewModel @Inject constructor( private val _setupComplete: MutableLiveData> = MutableLiveData() private val _closeComposer: MutableLiveData> = MutableLiveData() private var _setupCompleteValue = false - private val _savingDraftComplete: MutableLiveData> = MutableLiveData() - private var _savingDraftInProcess: AtomicBoolean = AtomicBoolean(false) + private val _savingDraftComplete: MutableLiveData = MutableLiveData() private val _deleteResult: MutableLiveData> = MutableLiveData() private val _loadingDraftResult: MutableLiveData = MutableLiveData() private val _messageResultError: MutableLiveData> = MutableLiveData() @@ -163,7 +161,7 @@ class ComposeMessageViewModel @Inject constructor( get() = _closeComposer val setupCompleteValue: Boolean get() = _setupCompleteValue - val savingDraftComplete: LiveData> + val savingDraftComplete: LiveData get() = _savingDraftComplete val senderAddresses: List get() = _senderAddresses @@ -364,12 +362,6 @@ class ComposeMessageViewModel @Inject constructor( return result } - @Subscribe - fun onDraftCreatedEvent(event: DraftCreatedEvent) { - _savingDraftInProcess.set(false) - _savingDraftComplete.postValue(Event(event)) - } - @Subscribe fun onFetchMessageDetailEvent(event: FetchMessageDetailEvent) { if (event.success) { @@ -437,7 +429,6 @@ class ComposeMessageViewModel @Inject constructor( //endregion } else { //region new draft here - _savingDraftInProcess.set(true) setOfflineDraftSaved(true) if (draftId.isEmpty() && message.messageId.isNullOrEmpty()) { val newDraftId = UUID.randomUUID().toString() @@ -466,7 +457,12 @@ class ComposeMessageViewModel @Inject constructor( _oldSenderAddressId ) ).collect { - Timber.d("Saving draft result $it") + when (it) { + is SaveDraft.Result.Success -> onDraftSaved(it.draftId, message) + SaveDraft.Result.SendingInProgressError -> TODO() + SaveDraft.Result.OnlineDraftCreationFailed -> TODO() + SaveDraft.Result.UploadDraftAttachmentsFailed -> TODO() + } } _oldSenderAddressId = "" @@ -478,6 +474,16 @@ class ComposeMessageViewModel @Inject constructor( } } + private suspend fun onDraftSaved(savedDraftId: String, message: Message) { + withContext(dispatchers.Io) { + val draft = messageDetailsRepository.findMessageById(savedDraftId) + + val draftCreatedEvent = DraftCreatedEvent(message.messageId, message.messageId, draft) + onDraftCreated(draftCreatedEvent) + _savingDraftComplete.postValue(draft) + } + } + private suspend fun calculateNewAttachments(uploadAttachments: Boolean): List { var newAttachments: List = ArrayList() val localAttachmentsList = _messageDataResult.attachmentList.filter { !it.isUploaded } // these are composer attachments @@ -672,7 +678,7 @@ class ComposeMessageViewModel @Inject constructor( } } - fun onDraftCreated(event: DraftCreatedEvent) { + private fun onDraftCreated(event: DraftCreatedEvent) { val newMessageId: String? val eventMessage = event.message @@ -686,7 +692,7 @@ class ComposeMessageViewModel @Inject constructor( eventMessage.messageId } - viewModelScope.launch { + viewModelScope.launch(dispatchers.Main) { val isOfflineDraftSaved: Boolean isOfflineDraftSaved = if (event.status == Status.NO_NETWORK) { @@ -1284,7 +1290,7 @@ class ComposeMessageViewModel @Inject constructor( viewModelScope.launch { draftId = messageId!! message.isDownloaded = true - val attachments = message.Attachments // composeMessageRepository.getAttachments(message, IO) + val attachments = message.Attachments message.setAttachmentList(attachments) setAttachmentList(ArrayList(LocalAttachment.createLocalAttachmentList(attachments))) _dbId = message.dbId diff --git a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt index a39384757..f50460b2b 100644 --- a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt +++ b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt @@ -108,9 +108,11 @@ class SaveDraft @Inject constructor( .map { workInfo -> withContext(dispatchers.Io) { if (workInfo.state == WorkInfo.State.SUCCEEDED) { - val createdDraftId = workInfo.outputData.getString(KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID) + val createdDraftId = requireNotNull( + workInfo.outputData.getString(KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID) + ) Timber.d( - "Saving Draft to API for messageId $localDraftId succeeded. Created draftId = $createdDraftId" + "Save Draft to API for messageId $localDraftId succeeded. Created draftId = $createdDraftId" ) updatePendingForSendMessage(createdDraftId, localDraftId) @@ -122,7 +124,7 @@ class SaveDraft @Inject constructor( return@withContext Result.UploadDraftAttachmentsFailed } pendingActionsDao.deletePendingUploadByMessageId(localDraftId) - return@withContext Result.Success + return@withContext Result.Success(createdDraftId) } } @@ -139,7 +141,7 @@ class SaveDraft @Inject constructor( } } - private fun updatePendingForSendMessage(createdDraftId: String?, messageId: String) { + private fun updatePendingForSendMessage(createdDraftId: String, messageId: String) { val pendingForSending = pendingActionsDao.findPendingSendByOfflineMessageId(messageId) pendingForSending?.let { pendingForSending.messageId = createdDraftId @@ -148,7 +150,7 @@ class SaveDraft @Inject constructor( } sealed class Result { - object Success : Result() + data class Success(val draftId: String) : Result() object SendingInProgressError : Result() object OnlineDraftCreationFailed : Result() object UploadDraftAttachmentsFailed : Result() diff --git a/app/src/test/java/ch/protonmail/android/attachments/UploadAttachmentsTest.kt b/app/src/test/java/ch/protonmail/android/attachments/UploadAttachmentsTest.kt index a694af4e5..39d3a43f4 100644 --- a/app/src/test/java/ch/protonmail/android/attachments/UploadAttachmentsTest.kt +++ b/app/src/test/java/ch/protonmail/android/attachments/UploadAttachmentsTest.kt @@ -19,7 +19,6 @@ package ch.protonmail.android.attachments -import androidx.arch.core.executor.testing.InstantTaskExecutorRule import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository import ch.protonmail.android.api.models.room.messages.Attachment import ch.protonmail.android.api.models.room.messages.Message @@ -44,9 +43,6 @@ import kotlin.test.Test class UploadAttachmentsTest : CoroutinesTest { - @get:Rule - val instantTaskExecutorRule = InstantTaskExecutorRule() - @RelaxedMockK private lateinit var attachmentsRepository: AttachmentsRepository diff --git a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt index 1c82329fb..853702e2a 100644 --- a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt +++ b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt @@ -25,23 +25,30 @@ import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.api.services.PostMessageServiceFactory import ch.protonmail.android.core.Constants import ch.protonmail.android.core.UserManager +import ch.protonmail.android.testAndroid.lifecycle.testObserver import ch.protonmail.android.testAndroid.rx.TrampolineScheduler import ch.protonmail.android.usecase.VerifyConnection import ch.protonmail.android.usecase.compose.SaveDraft import ch.protonmail.android.usecase.delete.DeleteMessage import ch.protonmail.android.usecase.fetch.FetchPublicKeys +import ch.protonmail.android.utils.extensions.InstantExecutorExtension +import io.mockk.coEvery import io.mockk.coVerify +import io.mockk.every import io.mockk.impl.annotations.InjectMockKs import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.RelaxedMockK import io.mockk.junit5.MockKExtension +import io.mockk.verify +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runBlockingTest import me.proton.core.test.kotlin.CoroutinesTest +import org.junit.Assert.assertEquals import org.junit.Rule import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith -@ExtendWith(MockKExtension::class) +@ExtendWith(MockKExtension::class, InstantExecutorExtension::class) class ComposeMessageViewModelTest : CoroutinesTest { @Rule @@ -78,7 +85,7 @@ class ComposeMessageViewModelTest : CoroutinesTest { lateinit var viewModel: ComposeMessageViewModel @Test - fun saveDraftCallsSaveDraftUseCaseWhenTheDraftIsNew() = + fun saveDraftCallsSaveDraftUseCaseWhenTheDraftIsNew() { runBlockingTest { val message = Message() // Needed to set class fields to the right value @@ -97,4 +104,27 @@ class ComposeMessageViewModelTest : CoroutinesTest { ) coVerify { saveDraft(parameters) } } + } + + @Test + fun saveDraftReadsNewlyCreatedDraftFromRepositoryAndPostsItToLiveDataWhenSaveDraftUseCaseSucceeds() { + runBlockingTest { + val message = Message() + val createdDraftId = "newDraftId" + val createdDraft = Message(messageId = createdDraftId, localId = "local28348") + // Needed to set class fields to the right value + viewModel.prepareMessageData(false, "addressId", "mail-alias", false) + viewModel.setupComposingNewMessage(false, Constants.MessageActionType.FORWARD, "parentId823", "") + viewModel.oldSenderAddressId = "previousSenderAddressId" + coEvery { saveDraft(any()) } returns flowOf(SaveDraft.Result.Success(createdDraftId)) + val testObserver = viewModel.savingDraftComplete.testObserver() + every { messageDetailsRepository.findMessageById(createdDraftId) } returns createdDraft + + viewModel.saveDraft(message, hasConnectivity = false) + + verify { messageDetailsRepository.findMessageById(createdDraftId) } + assertEquals(createdDraft, testObserver.observedValues[0]) + } + } + } diff --git a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt index 8b385a244..41aa17edd 100644 --- a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt +++ b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt @@ -453,7 +453,7 @@ class SaveDraftTest : CoroutinesTest { } @Test - fun saveDraftRemovesMessageFromPendingForUploadListWhenUploadSucceeds() { + fun saveDraftRemovesMessageFromPendingForUploadListAndReturnsSuccessWhenUploadSucceeds() { runBlockingTest { // Given val localDraftId = "8345" @@ -488,12 +488,13 @@ class SaveDraftTest : CoroutinesTest { every { addressCryptoFactory.create(Id("addressId"), Name(currentUsername)) } returns addressCrypto // When - saveDraft.invoke( + val result = saveDraft.invoke( SaveDraftParameters(message, newAttachmentIds, "parentId234", REPLY_ALL, "previousSenderId132423") ).first() // Then verify { pendingActionsDao.deletePendingUploadByMessageId("45623") } + assertEquals(Result.Success("createdDraftMessageId345"), result) } } diff --git a/app/src/test/java/ch/protonmail/android/utils/extensions/InstantExecutorExtension.kt b/app/src/test/java/ch/protonmail/android/utils/extensions/InstantExecutorExtension.kt new file mode 100644 index 000000000..5b5d5b3fd --- /dev/null +++ b/app/src/test/java/ch/protonmail/android/utils/extensions/InstantExecutorExtension.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * + * This file is part of ProtonMail. + * + * ProtonMail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonMail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonMail. If not, see https://www.gnu.org/licenses/. + */ + +package ch.protonmail.android.utils.extensions + +import androidx.arch.core.executor.ArchTaskExecutor +import androidx.arch.core.executor.TaskExecutor +import org.junit.jupiter.api.extension.AfterEachCallback +import org.junit.jupiter.api.extension.BeforeEachCallback +import org.junit.jupiter.api.extension.ExtensionContext + +class InstantExecutorExtension : BeforeEachCallback, AfterEachCallback { + + override fun beforeEach(context: ExtensionContext?) { + ArchTaskExecutor.getInstance() + .setDelegate(object : TaskExecutor() { + override fun executeOnDiskIO(runnable: Runnable) = runnable.run() + + override fun postToMainThread(runnable: Runnable) = runnable.run() + + override fun isMainThread(): Boolean = true + }) + } + + override fun afterEach(context: ExtensionContext?) { + ArchTaskExecutor.getInstance().setDelegate(null) + } + +} From c089f6713115e1197307529d47a50e2c351ddcd7 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Thu, 17 Dec 2020 15:52:12 +0100 Subject: [PATCH 024/145] Test and simplify view's draft created logic - Remove OnDraftCreatedObserver and MessageDraftResult liveData as the same value is posted right before on `savingDraftComplete` LiveData.We can execute all the logic there as the source doesn't change meanwhile - Simplify onDraftCreated method to only do: * Delete local draft from composeMessageRepository when it's saved online * Sets _draftId property val and observes message sent event We could remove all the rest of the logic based on the following reasons: - isOfflineDraftSaved --> deleted as it becomes constant (method is only called when saving to API succeeds) - iterate on attachments to set ID and name --> based on my understanding of the code and some tests, this will never be executed. That's because the first draft creation is done immediately when opening up the composer, so no attachments are there - Setting message inline --> same as point above MAILAND-1018 --- .../ComposeMessageActivity.java | 20 ++--- .../compose/ComposeMessageRepository.kt | 12 +-- .../compose/ComposeMessageViewModel.kt | 73 +++---------------- .../compose/ComposeMessageViewModelTest.kt | 52 ++++++++++--- 4 files changed, 68 insertions(+), 89 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java b/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java index cb1a7152a..91e6e0246 100644 --- a/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java +++ b/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java @@ -465,13 +465,18 @@ private void observeSetup() { composeMessageViewModel.getDeleteResult().observe(ComposeMessageActivity.this, new CheckLocalMessageObserver()); composeMessageViewModel.getOpenAttachmentsScreenResult().observe(ComposeMessageActivity.this, new AddAttachmentsObserver()); - composeMessageViewModel.getMessageDraftResult().observe(ComposeMessageActivity.this, new OnDraftCreatedObserver(TextUtils.isEmpty(mAction))); composeMessageViewModel.getSavingDraftComplete().observe(this, event -> { if (mUpdateDraftPmMeChanged) { composeMessageViewModel.setBeforeSaveDraft(true, mComposeBodyEditText.getText().toString()); mUpdateDraftPmMeChanged = false; } disableSendButton(false); + onMessageLoaded( + event, + false, + TextUtils.isEmpty(mAction) && + composeMessageViewModel.getMessageDataResult().getAttachmentList().isEmpty() + ); }); composeMessageViewModel.getDbIdWatcher().observe(ComposeMessageActivity.this, new SendMessageObserver()); @@ -2145,19 +2150,6 @@ public void onChanged(@Nullable Long dbId) { } } - private class OnDraftCreatedObserver implements Observer { - private final boolean updateAttachments; - - OnDraftCreatedObserver(boolean updateAttachments) { - this.updateAttachments = updateAttachments; - } - - @Override - public void onChanged(@Nullable Message message) { - onMessageLoaded(message, false, updateAttachments && composeMessageViewModel.getMessageDataResult().getAttachmentList().isEmpty()); - } - } - private class AddAttachmentsObserver implements Observer> { @Override diff --git a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageRepository.kt b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageRepository.kt index 1499783cd..76619e098 100644 --- a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageRepository.kt +++ b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageRepository.kt @@ -53,6 +53,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import me.proton.core.util.kotlin.DispatcherProvider import javax.inject.Inject import javax.inject.Named @@ -60,9 +61,10 @@ class ComposeMessageRepository @Inject constructor( val jobManager: JobManager, val api: ProtonMailApiManager, val databaseProvider: DatabaseProvider, - @Named("messages")var messagesDatabase: MessagesDatabase, - @Named("messages_search")val searchDatabase: MessagesDatabase, - val messageDetailsRepository: MessageDetailsRepository // FIXME: this should be removed){} + @Named("messages") var messagesDatabase: MessagesDatabase, + @Named("messages_search") val searchDatabase: MessagesDatabase, + val messageDetailsRepository: MessageDetailsRepository, // FIXME: this should be removed){} + val dispatchers: DispatcherProvider ) { val lazyManager = resettableManager() @@ -154,8 +156,8 @@ class ComposeMessageRepository @Inject constructor( } - suspend fun deleteMessageById(messageId: String, dispatcher: CoroutineDispatcher) = - withContext(dispatcher) { + suspend fun deleteMessageById(messageId: String) = + withContext(dispatchers.Io) { messagesDatabase.deleteMessageById(messageId) } diff --git a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt index 88787e787..9bceb71e2 100644 --- a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt +++ b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt @@ -48,7 +48,6 @@ import ch.protonmail.android.contacts.PostResult import ch.protonmail.android.core.Constants import ch.protonmail.android.core.ProtonMailApplication import ch.protonmail.android.core.UserManager -import ch.protonmail.android.events.DraftCreatedEvent import ch.protonmail.android.events.FetchMessageDetailEvent import ch.protonmail.android.events.Status import ch.protonmail.android.jobs.contacts.GetSendPreferenceJob @@ -458,7 +457,7 @@ class ComposeMessageViewModel @Inject constructor( ) ).collect { when (it) { - is SaveDraft.Result.Success -> onDraftSaved(it.draftId, message) + is SaveDraft.Result.Success -> onDraftSaved(it.draftId) SaveDraft.Result.SendingInProgressError -> TODO() SaveDraft.Result.OnlineDraftCreationFailed -> TODO() SaveDraft.Result.UploadDraftAttachmentsFailed -> TODO() @@ -474,12 +473,19 @@ class ComposeMessageViewModel @Inject constructor( } } - private suspend fun onDraftSaved(savedDraftId: String, message: Message) { + private suspend fun onDraftSaved(savedDraftId: String) { withContext(dispatchers.Io) { - val draft = messageDetailsRepository.findMessageById(savedDraftId) + val draft = requireNotNull(messageDetailsRepository.findMessageById(savedDraftId)) - val draftCreatedEvent = DraftCreatedEvent(message.messageId, message.messageId, draft) - onDraftCreated(draftCreatedEvent) + viewModelScope.launch(dispatchers.Main) { + if (_draftId.get().isNotEmpty() && draft.messageId.isNullOrEmpty().not()) { + draft.localId?.let { + composeMessageRepository.deleteMessageById(it) + } + } + _draftId.set(draft.messageId) + watchForMessageSent() + } _savingDraftComplete.postValue(draft) } } @@ -678,61 +684,6 @@ class ComposeMessageViewModel @Inject constructor( } } - private fun onDraftCreated(event: DraftCreatedEvent) { - val newMessageId: String? - val eventMessage = event.message - - if (_draftId.get() != event.oldMessageId) { - return - } - - newMessageId = if (eventMessage == null) { - event.messageId - } else { - eventMessage.messageId - } - - viewModelScope.launch(dispatchers.Main) { - val isOfflineDraftSaved: Boolean - isOfflineDraftSaved = - if (event.status == Status.NO_NETWORK) { - true - } else { - val draftId = _draftId.get() - if (!TextUtils.isEmpty(draftId) && !TextUtils.isEmpty(newMessageId)) { - composeMessageRepository.deleteMessageById(draftId, IO) - } - false - } - - setOfflineDraftSaved(isOfflineDraftSaved) - var draftMessage: Message? = null - if (eventMessage != null) { - val eventMessageAttachmentList = - composeMessageRepository.getAttachments(eventMessage, _messageDataResult.isTransient, IO) - - for (localAttachment in _messageDataResult.attachmentList) { - for (attachment in eventMessageAttachmentList) { - if (localAttachment.displayName == attachment.fileName) { - localAttachment.attachmentId = attachment.attachmentId ?: "" - } - } - } - _draftId.set(newMessageId) - draftMessage = eventMessage - watchForMessageSent() - } - val draftId = _draftId.get() - if (draftMessage != null && draftId != null) { - val storedMessage = composeMessageRepository.findMessage(draftId, IO) - if (storedMessage != null) { - draftMessage.isInline = storedMessage.isInline - } - } - _messageDraftResult.postValue(draftMessage) - } - } - fun deleteDraft() { viewModelScope.launch { deleteMessage(listOf(_draftId.get())) diff --git a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt index 853702e2a..ad99496c6 100644 --- a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt +++ b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt @@ -88,10 +88,7 @@ class ComposeMessageViewModelTest : CoroutinesTest { fun saveDraftCallsSaveDraftUseCaseWhenTheDraftIsNew() { runBlockingTest { val message = Message() - // Needed to set class fields to the right value - viewModel.prepareMessageData(false, "addressId", "mail-alias", false) - viewModel.setupComposingNewMessage(false, Constants.MessageActionType.FORWARD, "parentId823", "") - viewModel.oldSenderAddressId = "previousSenderAddressId" + givenViewModelPropertiesAreInitialised() viewModel.saveDraft(message, hasConnectivity = false) @@ -112,12 +109,9 @@ class ComposeMessageViewModelTest : CoroutinesTest { val message = Message() val createdDraftId = "newDraftId" val createdDraft = Message(messageId = createdDraftId, localId = "local28348") - // Needed to set class fields to the right value - viewModel.prepareMessageData(false, "addressId", "mail-alias", false) - viewModel.setupComposingNewMessage(false, Constants.MessageActionType.FORWARD, "parentId823", "") - viewModel.oldSenderAddressId = "previousSenderAddressId" - coEvery { saveDraft(any()) } returns flowOf(SaveDraft.Result.Success(createdDraftId)) val testObserver = viewModel.savingDraftComplete.testObserver() + givenViewModelPropertiesAreInitialised() + coEvery { saveDraft(any()) } returns flowOf(SaveDraft.Result.Success(createdDraftId)) every { messageDetailsRepository.findMessageById(createdDraftId) } returns createdDraft viewModel.saveDraft(message, hasConnectivity = false) @@ -127,4 +121,44 @@ class ComposeMessageViewModelTest : CoroutinesTest { } } + @Test + fun saveDraftDeletesLocalMessageFromComposerRepositoryWhenSaveDraftUseCaseIsSuccessful() { + runBlockingTest { + val createdDraftId = "newDraftId" + val localDraftId = "localDraftId" + val createdDraft = Message(messageId = createdDraftId, localId = localDraftId) + givenViewModelPropertiesAreInitialised() + coEvery { saveDraft(any()) } returns flowOf(SaveDraft.Result.Success(createdDraftId)) + every { messageDetailsRepository.findMessageById(createdDraftId) } returns createdDraft + + viewModel.saveDraft(Message(), hasConnectivity = false) + + coVerify { composeMessageRepository.deleteMessageById(localDraftId) } + } + } + + @Test + fun saveDraftObservesMessageInComposeRepositoryToGetNotifiedWhenMessageIsSent() { + runBlockingTest { + val createdDraftId = "newDraftId" + val localDraftId = "localDraftId" + val createdDraft = Message(messageId = createdDraftId, localId = localDraftId) + givenViewModelPropertiesAreInitialised() + coEvery { saveDraft(any()) } returns flowOf(SaveDraft.Result.Success(createdDraftId)) + every { messageDetailsRepository.findMessageById(createdDraftId) } returns createdDraft + + viewModel.saveDraft(Message(), hasConnectivity = false) + + assertEquals(createdDraftId, viewModel.draftId) + coVerify { composeMessageRepository.findMessageByIdObservable(createdDraftId) } + } + } + + private fun givenViewModelPropertiesAreInitialised() { + // Needed to set class fields to the right value and allow code under test to get executed + viewModel.prepareMessageData(false, "addressId", "mail-alias", false) + viewModel.setupComposingNewMessage(false, Constants.MessageActionType.FORWARD, "parentId823", "") + viewModel.oldSenderAddressId = "previousSenderAddressId" + } + } From 5380a01bc105a0c13a9e3ca66ee9304d59568fcc Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Thu, 17 Dec 2020 16:12:39 +0100 Subject: [PATCH 025/145] Cleanup - Methods renaming and removing unused code - Removed messageDraftResult LiveData as not used anymore - Removed setOfflineDraftSaved as value was set but never read - Rename findMessageByIdBlocking method to change non-blocking method to using dispatcher from constructor --- .../android/activities/MailboxViewModel.kt | 2 +- .../ComposeMessageActivity.java | 1 - .../composeMessage/MessageBuilderData.kt | 7 ----- .../activities/mailbox/MailboxActivity.kt | 2 +- .../activities/mailbox/OnParentEventTask.kt | 3 +- .../mailbox/ShowLabelsManagerDialogTask.kt | 6 ++-- .../repository/MessageDetailsRepository.kt | 26 ++++++++-------- .../viewmodel/MessageDetailsViewModel.kt | 2 +- .../api/segments/event/EventHandler.kt | 8 ++--- .../android/api/services/MessagesService.kt | 4 +-- .../DownloadEmbeddedAttachmentsWorker.kt | 2 +- .../compose/ComposeMessageRepository.kt | 4 +-- .../compose/ComposeMessageViewModel.kt | 30 +++++-------------- .../fcm/ProcessPushNotificationDataWorker.kt | 8 ++--- .../android/jobs/ApplyLabelJob.java | 2 +- .../android/jobs/CreateAndPostDraftJob.java | 8 ++--- .../android/jobs/FetchDraftDetailJob.java | 2 +- .../android/jobs/FetchMessageDetailJob.java | 2 +- .../android/jobs/MoveToFolderJob.java | 2 +- .../android/jobs/PostArchiveJob.java | 2 +- .../protonmail/android/jobs/PostDraftJob.java | 2 +- .../protonmail/android/jobs/PostInboxJob.java | 2 +- .../protonmail/android/jobs/PostReadJob.java | 2 +- .../protonmail/android/jobs/PostSpamJob.java | 2 +- .../protonmail/android/jobs/PostTrashJob.java | 2 +- .../android/jobs/PostTrashJobV2.java | 2 +- .../android/jobs/PostUnreadJob.java | 2 +- .../android/jobs/PostUnstarJob.java | 2 +- .../android/jobs/ProtonMailCounterJob.java | 2 +- .../android/jobs/RemoveLabelJob.java | 4 +-- .../android/receivers/NotificationReceiver.kt | 2 +- .../android/usecase/compose/SaveDraft.kt | 4 +-- .../android/usecase/delete/DeleteMessage.kt | 2 +- .../android/worker/CreateDraftWorker.kt | 2 +- .../compose/ComposeMessageViewModelTest.kt | 8 ++--- .../android/usecase/compose/SaveDraftTest.kt | 14 ++++----- .../usecase/delete/DeleteMessageTest.kt | 8 ++--- .../android/worker/CreateDraftWorkerTest.kt | 10 +++---- 38 files changed, 87 insertions(+), 108 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/activities/MailboxViewModel.kt b/app/src/main/java/ch/protonmail/android/activities/MailboxViewModel.kt index 3c32c74ac..978ebfa3f 100644 --- a/app/src/main/java/ch/protonmail/android/activities/MailboxViewModel.kt +++ b/app/src/main/java/ch/protonmail/android/activities/MailboxViewModel.kt @@ -142,7 +142,7 @@ class MailboxViewModel @ViewModelInject constructor( withContext(Dispatchers.Default) { while (iterator.hasNext()) { val messageId = iterator.next() - val message = messageDetailsRepository.findMessageById(messageId, Dispatchers.Default) + val message = messageDetailsRepository.findMessageById(messageId) if (message != null) { val currentLabelsIds = message.labelIDsNotIncludingLocations diff --git a/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java b/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java index 91e6e0246..866c7d600 100644 --- a/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java +++ b/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java @@ -2217,7 +2217,6 @@ public void onChanged(@Nullable Event messageEvent) { Message localMessage = messageEvent.getContentIfNotHandled(); if (localMessage != null) { - composeMessageViewModel.setOfflineDraftSaved(false); String aliasAddress = composeMessageViewModel.getMessageDataResult().getAddressEmailAlias(); MessageSender messageSender; diff --git a/app/src/main/java/ch/protonmail/android/activities/composeMessage/MessageBuilderData.kt b/app/src/main/java/ch/protonmail/android/activities/composeMessage/MessageBuilderData.kt index 52195a5b3..1a979f080 100644 --- a/app/src/main/java/ch/protonmail/android/activities/composeMessage/MessageBuilderData.kt +++ b/app/src/main/java/ch/protonmail/android/activities/composeMessage/MessageBuilderData.kt @@ -54,7 +54,6 @@ class MessageBuilderData( val isRespondInlineChecked: Boolean, val showImages: Boolean, val showRemoteContent: Boolean, - val offlineDraftSaved: Boolean, val initialMessageContent: String, val decryptedMessage: String, val isMessageBodyVisible: Boolean, @@ -90,7 +89,6 @@ class MessageBuilderData( private var isRespondInlineChecked: Boolean = false private var showImages: Boolean = false private var showRemoteContent: Boolean = false - private var offlineDraftSaved: Boolean = false private var initialMessageContent: String = "" private var decryptedMessage: String = "" private var isMessageBodyVisible: Boolean = false @@ -129,7 +127,6 @@ class MessageBuilderData( this.isRespondInlineChecked = oldObject.isRespondInlineChecked this.showImages = oldObject.showImages this.showRemoteContent = oldObject.showRemoteContent - this.offlineDraftSaved = oldObject.offlineDraftSaved this.initialMessageContent = oldObject.initialMessageContent this.isMessageBodyVisible = oldObject.isMessageBodyVisible this.quotedHeader = oldObject.quotedHeader @@ -218,9 +215,6 @@ class MessageBuilderData( fun showRemoteContent(showRemoteContent: Boolean) = apply { this.showRemoteContent = showRemoteContent } - fun offlineDraftSaved(offlineDraftSaved: Boolean) = - apply { this.offlineDraftSaved = offlineDraftSaved } - fun initialMessageContent(initialMessageContent: String) = apply { this.initialMessageContent = initialMessageContent } @@ -265,7 +259,6 @@ class MessageBuilderData( isRespondInlineChecked, showImages, showRemoteContent, - offlineDraftSaved, initialMessageContent, decryptedMessage, isMessageBodyVisible, diff --git a/app/src/main/java/ch/protonmail/android/activities/mailbox/MailboxActivity.kt b/app/src/main/java/ch/protonmail/android/activities/mailbox/MailboxActivity.kt index d3eae206e..495d93688 100644 --- a/app/src/main/java/ch/protonmail/android/activities/mailbox/MailboxActivity.kt +++ b/app/src/main/java/ch/protonmail/android/activities/mailbox/MailboxActivity.kt @@ -1771,7 +1771,7 @@ class MailboxActivity : ) : AsyncTask() { override fun doInBackground(vararg params: Unit): Message? = - messageDetailsRepository.findMessageById(message.messageId!!) + messageDetailsRepository.findMessageByIdBlocking(message.messageId!!) public override fun onPostExecute(savedMessage: Message?) { val mailboxActivity = mailboxActivity.get() diff --git a/app/src/main/java/ch/protonmail/android/activities/mailbox/OnParentEventTask.kt b/app/src/main/java/ch/protonmail/android/activities/mailbox/OnParentEventTask.kt index de37d1c29..e2c643fbe 100644 --- a/app/src/main/java/ch/protonmail/android/activities/mailbox/OnParentEventTask.kt +++ b/app/src/main/java/ch/protonmail/android/activities/mailbox/OnParentEventTask.kt @@ -22,7 +22,6 @@ import android.os.AsyncTask import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository import ch.protonmail.android.adapters.messages.MessagesRecyclerViewAdapter import ch.protonmail.android.events.ParentEvent -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking internal class OnParentEventTask(private val messageDetailsRepository: MessageDetailsRepository, @@ -33,7 +32,7 @@ internal class OnParentEventTask(private val messageDetailsRepository: MessageDe runBlocking { val messageId = event.parentId - messageDetailsRepository.findMessageById(messageId, Dispatchers.IO)?.apply { + messageDetailsRepository.findMessageById(messageId)?.apply { isReplied = event.isReplied == 1 isRepliedAll = event.isRepliedAll == 1 isForwarded = event.isForwarded == 1 diff --git a/app/src/main/java/ch/protonmail/android/activities/mailbox/ShowLabelsManagerDialogTask.kt b/app/src/main/java/ch/protonmail/android/activities/mailbox/ShowLabelsManagerDialogTask.kt index 0a59b0783..fe773d2a6 100644 --- a/app/src/main/java/ch/protonmail/android/activities/mailbox/ShowLabelsManagerDialogTask.kt +++ b/app/src/main/java/ch/protonmail/android/activities/mailbox/ShowLabelsManagerDialogTask.kt @@ -23,7 +23,9 @@ import androidx.fragment.app.FragmentManager import ch.protonmail.android.activities.dialogs.ManageLabelsDialogFragment import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository import ch.protonmail.android.api.models.room.messages.Message -import java.util.* +import java.util.ArrayList +import java.util.HashMap +import java.util.HashSet /** * Created by Kamil Rajtar on 24.07.18. @@ -33,7 +35,7 @@ internal class ShowLabelsManagerDialogTask(private val fragmentManager: Fragment private val messageIds:List):AsyncTask>() { override fun doInBackground(vararg voids:Void):List { - return messageIds.filter {!it.isEmpty()}.mapNotNull(messageDetailsRepository::findMessageById) + return messageIds.filter { !it.isEmpty() }.mapNotNull(messageDetailsRepository::findMessageByIdBlocking) } override fun onPostExecute(messages:List) { diff --git a/app/src/main/java/ch/protonmail/android/activities/messageDetails/repository/MessageDetailsRepository.kt b/app/src/main/java/ch/protonmail/android/activities/messageDetails/repository/MessageDetailsRepository.kt index 79b7b23d3..b0bf60e53 100644 --- a/app/src/main/java/ch/protonmail/android/activities/messageDetails/repository/MessageDetailsRepository.kt +++ b/app/src/main/java/ch/protonmail/android/activities/messageDetails/repository/MessageDetailsRepository.kt @@ -88,29 +88,29 @@ class MessageDetailsRepository @Inject constructor( } fun findMessageByIdAsync(messageId: String): LiveData = - messagesDao.findMessageByIdAsync(messageId).asyncMap(readMessageBodyFromFileIfNeeded) + messagesDao.findMessageByIdAsync(messageId).asyncMap(readMessageBodyFromFileIfNeeded) fun findSearchMessageByIdAsync(messageId: String): LiveData = - searchDatabaseDao.findMessageByIdAsync(messageId).asyncMap(readMessageBodyFromFileIfNeeded) + searchDatabaseDao.findMessageByIdAsync(messageId).asyncMap(readMessageBodyFromFileIfNeeded) - suspend fun findMessageById(messageId: String, dispatcher: CoroutineDispatcher) = - withContext(dispatcher) { - findMessageById(messageId) - } + suspend fun findMessageById(messageId: String) = + withContext(dispatchers.Io) { + findMessageByIdBlocking(messageId) + } suspend fun findSearchMessageById(messageId: String, dispatcher: CoroutineDispatcher) = - withContext(dispatcher) { - searchDatabaseDao.findMessageById(messageId)?.apply { readMessageBodyFromFileIfNeeded(this) } - } + withContext(dispatcher) { + searchDatabaseDao.findMessageById(messageId)?.apply { readMessageBodyFromFileIfNeeded(this) } + } - suspend fun findMessageByMessageDbId(dbId: Long, dispatcher: CoroutineDispatcher) : Message? = - withContext(dispatcher) { + suspend fun findMessageByMessageDbId(dbId: Long, dispatcher: CoroutineDispatcher): Message? = + withContext(dispatcher) { findMessageByMessageDbId(dbId) } - fun findMessageById(messageId: String): Message? = - messagesDao.findMessageById(messageId)?.apply { readMessageBodyFromFileIfNeeded(this) } + fun findMessageByIdBlocking(messageId: String): Message? = + messagesDao.findMessageById(messageId)?.apply { readMessageBodyFromFileIfNeeded(this) } fun findSearchMessageById(messageId: String): Message? = searchDatabaseDao.findMessageById(messageId)?.apply { readMessageBodyFromFileIfNeeded(this) } diff --git a/app/src/main/java/ch/protonmail/android/activities/messageDetails/viewmodel/MessageDetailsViewModel.kt b/app/src/main/java/ch/protonmail/android/activities/messageDetails/viewmodel/MessageDetailsViewModel.kt index 92fd8d535..e030afa6c 100644 --- a/app/src/main/java/ch/protonmail/android/activities/messageDetails/viewmodel/MessageDetailsViewModel.kt +++ b/app/src/main/java/ch/protonmail/android/activities/messageDetails/viewmodel/MessageDetailsViewModel.kt @@ -425,7 +425,7 @@ internal class MessageDetailsViewModel @ViewModelInject constructor( } } else { - val savedMessage = findMessageById(messageId, dispatchers.Io) + val savedMessage = findMessageById(messageId) if (savedMessage != null) { messageResponse.message.writeTo(savedMessage) saveMessageInDB(savedMessage) diff --git a/app/src/main/java/ch/protonmail/android/api/segments/event/EventHandler.kt b/app/src/main/java/ch/protonmail/android/api/segments/event/EventHandler.kt index dd50459a4..6a0635c90 100644 --- a/app/src/main/java/ch/protonmail/android/api/segments/event/EventHandler.kt +++ b/app/src/main/java/ch/protonmail/android/api/segments/event/EventHandler.kt @@ -360,7 +360,7 @@ class EventHandler @AssistedInject constructor( when (type) { EventType.CREATE -> { try { - val savedMessage = messageDetailsRepository.findMessageById(messageID) + val savedMessage = messageDetailsRepository.findMessageByIdBlocking(messageID) savedMessage.ifNullElse( { messageDetailsRepository.saveMessageInDB(messageFactory.createMessage(event.message)) @@ -375,7 +375,7 @@ class EventHandler @AssistedInject constructor( } EventType.DELETE -> { - val message = messageDetailsRepository.findMessageById(messageID) + val message = messageDetailsRepository.findMessageByIdBlocking(messageID) if (message != null) { messagesDatabase.deleteMessage(message) } @@ -383,7 +383,7 @@ class EventHandler @AssistedInject constructor( EventType.UPDATE -> { // update Message body - val message = messageDetailsRepository.findMessageById(messageID) + val message = messageDetailsRepository.findMessageByIdBlocking(messageID) stagedMessages[messageID]?.let { val dbTime = message?.time ?: 0 val serverTime = it.time @@ -414,7 +414,7 @@ class EventHandler @AssistedInject constructor( messageID: String, item: EventResponse.MessageEventBody ) { - val message = messageDetailsRepository.findMessageById(messageID) + val message = messageDetailsRepository.findMessageByIdBlocking(messageID) val newMessage = item.message if (message != null) { diff --git a/app/src/main/java/ch/protonmail/android/api/services/MessagesService.kt b/app/src/main/java/ch/protonmail/android/api/services/MessagesService.kt index dd935547f..294f88d59 100644 --- a/app/src/main/java/ch/protonmail/android/api/services/MessagesService.kt +++ b/app/src/main/java/ch/protonmail/android/api/services/MessagesService.kt @@ -240,7 +240,7 @@ class MessagesService : JobIntentService() { if (refreshMessages) messageDetailsRepository.deleteMessagesByLocation(location) messageList.asSequence().map { msg -> unixTime = msg.time - val savedMessage = messageDetailsRepository.findMessageById(msg.messageId!!) + val savedMessage = messageDetailsRepository.findMessageByIdBlocking(msg.messageId!!) msg.setLabelIDs(msg.getEventLabelIDs()) msg.location = location.messageLocationTypeValue msg.setFolderLocation(messagesDb) @@ -312,7 +312,7 @@ class MessagesService : JobIntentService() { if (refreshMessages) messageDetailsRepository.deleteMessagesByLabel(labelId) messageList.asSequence().map { msg -> unixTime = msg.time - val savedMessage = messageDetailsRepository.findMessageById(msg.messageId!!) + val savedMessage = messageDetailsRepository.findMessageByIdBlocking(msg.messageId!!) msg.setLabelIDs(msg.getEventLabelIDs()) msg.location = location.messageLocationTypeValue msg.setFolderLocation(messagesDb) diff --git a/app/src/main/java/ch/protonmail/android/attachments/DownloadEmbeddedAttachmentsWorker.kt b/app/src/main/java/ch/protonmail/android/attachments/DownloadEmbeddedAttachmentsWorker.kt index 39fadb55e..6db921d8d 100644 --- a/app/src/main/java/ch/protonmail/android/attachments/DownloadEmbeddedAttachmentsWorker.kt +++ b/app/src/main/java/ch/protonmail/android/attachments/DownloadEmbeddedAttachmentsWorker.kt @@ -109,7 +109,7 @@ class DownloadEmbeddedAttachmentsWorker @WorkerInject constructor( if (message != null) { // use search or standard message database, if Message comes from search attachments = messageDetailsRepository.findSearchAttachmentsByMessageId(messageId) } else { - message = messageDetailsRepository.findMessageById(messageId) + message = messageDetailsRepository.findMessageByIdBlocking(messageId) attachments = messageDetailsRepository.findAttachmentsByMessageId(messageId) } diff --git a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageRepository.kt b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageRepository.kt index 76619e098..34eda5b95 100644 --- a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageRepository.kt +++ b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageRepository.kt @@ -150,7 +150,7 @@ class ComposeMessageRepository @Inject constructor( withContext(dispatcher) { var message: Message? = null if (!TextUtils.isEmpty(draftId)) { - message = messageDetailsRepository.findMessageById(draftId) + message = messageDetailsRepository.findMessageByIdBlocking(draftId) } message } @@ -216,7 +216,7 @@ class ComposeMessageRepository @Inject constructor( fun markMessageRead(messageId: String) { GlobalScope.launch(Dispatchers.IO) { - messageDetailsRepository.findMessageById(messageId)?.let { savedMessage -> + messageDetailsRepository.findMessageByIdBlocking(messageId)?.let { savedMessage -> val read = savedMessage.isRead if (!read) { jobManager.addJobInBackground(PostReadJob(listOf(savedMessage.messageId))) diff --git a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt index 9bceb71e2..b197fd07f 100644 --- a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt +++ b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt @@ -108,7 +108,6 @@ class ComposeMessageViewModel @Inject constructor( private val _loadingDraftResult: MutableLiveData = MutableLiveData() private val _messageResultError: MutableLiveData> = MutableLiveData() private val _openAttachmentsScreenResult: MutableLiveData> = MutableLiveData() - private val _messageDraftResult: MutableLiveData = MutableLiveData() private val _buildingMessageCompleted: MutableLiveData> = MutableLiveData() private val _dbIdWatcher: MutableLiveData = MutableLiveData() private val _fetchMessageDetailsEvent: MutableLiveData> = MutableLiveData() @@ -212,9 +211,6 @@ class ComposeMessageViewModel @Inject constructor( get() = _parentId // endregion - val messageDraftResult: LiveData - get() = _messageDraftResult - private val loggedInUsernames = if (userManager.user.combinedContacts) { AccountManager.getInstance(ProtonMailApplication.getApplication().applicationContext).getLoggedInUsers() } else { @@ -428,7 +424,6 @@ class ComposeMessageViewModel @Inject constructor( //endregion } else { //region new draft here - setOfflineDraftSaved(true) if (draftId.isEmpty() && message.messageId.isNullOrEmpty()) { val newDraftId = UUID.randomUUID().toString() _draftId.set(newDraftId) @@ -474,20 +469,18 @@ class ComposeMessageViewModel @Inject constructor( } private suspend fun onDraftSaved(savedDraftId: String) { - withContext(dispatchers.Io) { - val draft = requireNotNull(messageDetailsRepository.findMessageById(savedDraftId)) + val draft = requireNotNull(messageDetailsRepository.findMessageById(savedDraftId)) - viewModelScope.launch(dispatchers.Main) { - if (_draftId.get().isNotEmpty() && draft.messageId.isNullOrEmpty().not()) { - draft.localId?.let { - composeMessageRepository.deleteMessageById(it) - } + viewModelScope.launch(dispatchers.Main) { + if (_draftId.get().isNotEmpty() && draft.messageId.isNullOrEmpty().not()) { + draft.localId?.let { + composeMessageRepository.deleteMessageById(it) } - _draftId.set(draft.messageId) - watchForMessageSent() } - _savingDraftComplete.postValue(draft) + _draftId.set(draft.messageId) + watchForMessageSent() } + _savingDraftComplete.postValue(draft) } private suspend fun calculateNewAttachments(uploadAttachments: Boolean): List { @@ -1100,13 +1093,6 @@ class ComposeMessageViewModel @Inject constructor( .build() } - fun setOfflineDraftSaved(offlineDraftSaved: Boolean) { - _messageDataResult = MessageBuilderData.Builder() - .fromOld(_messageDataResult) - .offlineDraftSaved(offlineDraftSaved) - .build() - } - fun setInitialMessageContent(initialMessageContent: String) { _messageDataResult = MessageBuilderData.Builder() .fromOld(_messageDataResult) diff --git a/app/src/main/java/ch/protonmail/android/fcm/ProcessPushNotificationDataWorker.kt b/app/src/main/java/ch/protonmail/android/fcm/ProcessPushNotificationDataWorker.kt index a0ed53338..dd02affd2 100644 --- a/app/src/main/java/ch/protonmail/android/fcm/ProcessPushNotificationDataWorker.kt +++ b/app/src/main/java/ch/protonmail/android/fcm/ProcessPushNotificationDataWorker.kt @@ -167,7 +167,7 @@ class ProcessPushNotificationDataWorker @WorkerInject constructor( } else { fetchMessageMetadata(messageId) } - ?: messageDetailsRepository.findMessageById(messageId) + ?: messageDetailsRepository.findMessageByIdBlocking(messageId) } private fun fetchMessageMetadata(messageId: String): Message? { @@ -180,7 +180,7 @@ class ProcessPushNotificationDataWorker @WorkerInject constructor( message = messages[0] } if (message != null) { - val savedMessage = messageDetailsRepository.findMessageById(message.messageId!!) + val savedMessage = messageDetailsRepository.findMessageByIdBlocking(message.messageId!!) if (savedMessage != null) { message.isInline = savedMessage.isInline } @@ -188,7 +188,7 @@ class ProcessPushNotificationDataWorker @WorkerInject constructor( messageDetailsRepository.saveMessageInDB(message) } else { // check if the message is already in local store - message = messageDetailsRepository.findMessageById(messageId) + message = messageDetailsRepository.findMessageByIdBlocking(messageId) } } } catch (error: Exception) { @@ -202,7 +202,7 @@ class ProcessPushNotificationDataWorker @WorkerInject constructor( try { val messageResponse: MessageResponse = protonMailApiManager.messageDetail(messageId) message = messageResponse.message - val savedMessage = messageDetailsRepository.findMessageById(messageId) + val savedMessage = messageDetailsRepository.findMessageByIdBlocking(messageId) if (savedMessage != null) { message.isInline = savedMessage.isInline } diff --git a/app/src/main/java/ch/protonmail/android/jobs/ApplyLabelJob.java b/app/src/main/java/ch/protonmail/android/jobs/ApplyLabelJob.java index 9b5520665..8252606d0 100644 --- a/app/src/main/java/ch/protonmail/android/jobs/ApplyLabelJob.java +++ b/app/src/main/java/ch/protonmail/android/jobs/ApplyLabelJob.java @@ -67,7 +67,7 @@ private void countUnread(@NonNull ModificationMethod modificationMethod) { .getDatabase(); int totalUnread = 0; for (String messageId : messageIds) { - Message message = getMessageDetailsRepository().findMessageById(messageId); + Message message = getMessageDetailsRepository().findMessageByIdBlocking(messageId); if (message == null) { continue; } diff --git a/app/src/main/java/ch/protonmail/android/jobs/CreateAndPostDraftJob.java b/app/src/main/java/ch/protonmail/android/jobs/CreateAndPostDraftJob.java index 2a023a274..871ced0c4 100644 --- a/app/src/main/java/ch/protonmail/android/jobs/CreateAndPostDraftJob.java +++ b/app/src/main/java/ch/protonmail/android/jobs/CreateAndPostDraftJob.java @@ -128,7 +128,7 @@ public void onRun() throws Throwable { newDraft.setParentID(mParentId); newDraft.setAction(mActionType.getMessageActionTypeValue()); if(!isTransient) { - parentMessage = getMessageDetailsRepository().findMessageById(mParentId); + parentMessage = getMessageDetailsRepository().findMessageByIdBlocking(mParentId); } else { parentMessage = getMessageDetailsRepository().findSearchMessageById(mParentId); } @@ -136,7 +136,7 @@ public void onRun() throws Throwable { String addressId = message.getAddressID(); String encryptedMessage = message.getMessageBody(); if (!TextUtils.isEmpty(message.getMessageId())) { - Message savedMessage = getMessageDetailsRepository().findMessageById(message.getMessageId()); + Message savedMessage = getMessageDetailsRepository().findMessageByIdBlocking(message.getMessageId()); if (savedMessage != null) { encryptedMessage = savedMessage.getMessageBody(); } @@ -195,7 +195,7 @@ public void onRun() throws Throwable { pendingForSending.setMessageId(newId); pendingActionsDatabase.insertPendingForSend(pendingForSending); } - Message offlineDraft = getMessageDetailsRepository().findMessageById(oldId); + Message offlineDraft = getMessageDetailsRepository().findMessageByIdBlocking(oldId); if (offlineDraft != null) { getMessageDetailsRepository().deleteMessage(offlineDraft); } @@ -266,7 +266,7 @@ private static class PostCreateDraftAttachmentsJob extends ProtonMailBaseJob { @Override public void onRun() throws Throwable { PendingActionsDatabase pendingActionsDatabase = PendingActionsDatabaseFactory.Companion.getInstance(getApplicationContext()).getDatabase(); - Message message = getMessageDetailsRepository().findMessageById(mMessageId); + Message message = getMessageDetailsRepository().findMessageByIdBlocking(mMessageId); User user = getUserManager().getUser(mUsername); if (user == null) { pendingActionsDatabase.deletePendingUploadByMessageId(mMessageId, mOldMessageId); diff --git a/app/src/main/java/ch/protonmail/android/jobs/FetchDraftDetailJob.java b/app/src/main/java/ch/protonmail/android/jobs/FetchDraftDetailJob.java index a54b12d36..5f44cc9a5 100644 --- a/app/src/main/java/ch/protonmail/android/jobs/FetchDraftDetailJob.java +++ b/app/src/main/java/ch/protonmail/android/jobs/FetchDraftDetailJob.java @@ -47,7 +47,7 @@ public void onRun() throws Throwable { try { final Message message = getApi().messageDetail(mMessageId).getMessage(); - Message savedMessage = getMessageDetailsRepository().findMessageById(message.getMessageId()); + Message savedMessage = getMessageDetailsRepository().findMessageByIdBlocking(message.getMessageId()); if (savedMessage != null) { message.setInline(savedMessage.isInline()); } diff --git a/app/src/main/java/ch/protonmail/android/jobs/FetchMessageDetailJob.java b/app/src/main/java/ch/protonmail/android/jobs/FetchMessageDetailJob.java index e801e5f93..d94bf2e40 100644 --- a/app/src/main/java/ch/protonmail/android/jobs/FetchMessageDetailJob.java +++ b/app/src/main/java/ch/protonmail/android/jobs/FetchMessageDetailJob.java @@ -53,7 +53,7 @@ public void onRun() throws Throwable { try { final MessageResponse messageResponse = getApi().messageDetail(mMessageId); final Message message = messageResponse.getMessage(); - Message savedMessage = getMessageDetailsRepository().findMessageById(message.getMessageId()); + Message savedMessage = getMessageDetailsRepository().findMessageByIdBlocking(message.getMessageId()); final FetchMessageDetailEvent event = new FetchMessageDetailEvent(true, mMessageId); if (savedMessage != null) { message.writeTo(savedMessage); diff --git a/app/src/main/java/ch/protonmail/android/jobs/MoveToFolderJob.java b/app/src/main/java/ch/protonmail/android/jobs/MoveToFolderJob.java index f74adfc3f..f75eaf9d9 100644 --- a/app/src/main/java/ch/protonmail/android/jobs/MoveToFolderJob.java +++ b/app/src/main/java/ch/protonmail/android/jobs/MoveToFolderJob.java @@ -58,7 +58,7 @@ public void onAdded() { .getDatabase(); int totalUnread = 0; for (String id : mMessageIds) { - final Message message = getMessageDetailsRepository().findMessageById(id); + final Message message = getMessageDetailsRepository().findMessageByIdBlocking(id); if (message != null) { if (!TextUtils.isEmpty(mLabelId)) { int location = message.getLocation(); diff --git a/app/src/main/java/ch/protonmail/android/jobs/PostArchiveJob.java b/app/src/main/java/ch/protonmail/android/jobs/PostArchiveJob.java index 89bf6d938..2016cbdf6 100644 --- a/app/src/main/java/ch/protonmail/android/jobs/PostArchiveJob.java +++ b/app/src/main/java/ch/protonmail/android/jobs/PostArchiveJob.java @@ -58,7 +58,7 @@ public void onAdded() { .getDatabase(); int totalUnread = 0; for (String id : mMessageIds) { - final Message message = getMessageDetailsRepository().findMessageById(id); + final Message message = getMessageDetailsRepository().findMessageByIdBlocking(id); if (message != null) { if (markMessageLocally(countersDatabase, message)) { totalUnread++; diff --git a/app/src/main/java/ch/protonmail/android/jobs/PostDraftJob.java b/app/src/main/java/ch/protonmail/android/jobs/PostDraftJob.java index f6302130b..66d5a1e89 100644 --- a/app/src/main/java/ch/protonmail/android/jobs/PostDraftJob.java +++ b/app/src/main/java/ch/protonmail/android/jobs/PostDraftJob.java @@ -39,7 +39,7 @@ public PostDraftJob(final List messageIds) { public void onAdded() { //TODO make a bulk operation for (String id : mMessageIds) { - final Message message = getMessageDetailsRepository().findMessageById(id); + final Message message = getMessageDetailsRepository().findMessageByIdBlocking(id); if (message != null) { message.setLocation(Constants.MessageLocationType.DRAFT.getMessageLocationTypeValue()); getMessageDetailsRepository().saveMessageInDB(message); diff --git a/app/src/main/java/ch/protonmail/android/jobs/PostInboxJob.java b/app/src/main/java/ch/protonmail/android/jobs/PostInboxJob.java index 15d350eaa..0f60cb673 100644 --- a/app/src/main/java/ch/protonmail/android/jobs/PostInboxJob.java +++ b/app/src/main/java/ch/protonmail/android/jobs/PostInboxJob.java @@ -58,7 +58,7 @@ public void onAdded() { int totalUnread = 0; for (String id : mMessageIds) { - final Message message = getMessageDetailsRepository().findMessageById(id); + final Message message = getMessageDetailsRepository().findMessageByIdBlocking(id); if (message != null) { if (!message.isRead()) { UnreadLocationCounter unreadLocationCounter = countersDatabase.findUnreadLocationById(message.getLocation()); diff --git a/app/src/main/java/ch/protonmail/android/jobs/PostReadJob.java b/app/src/main/java/ch/protonmail/android/jobs/PostReadJob.java index 7ede1e491..c5e38e5d3 100644 --- a/app/src/main/java/ch/protonmail/android/jobs/PostReadJob.java +++ b/app/src/main/java/ch/protonmail/android/jobs/PostReadJob.java @@ -49,7 +49,7 @@ public void onAdded() { Constants.MessageLocationType messageLocation = Constants.MessageLocationType.INVALID; boolean starred = false; for (String id : mMessageIds) { - final Message message = getMessageDetailsRepository().findMessageById(id); + final Message message = getMessageDetailsRepository().findMessageByIdBlocking(id); if (message != null) { starred = message.isStarred() != null && message.isStarred(); messageLocation = Constants.MessageLocationType.Companion.fromInt(message.getLocation()); diff --git a/app/src/main/java/ch/protonmail/android/jobs/PostSpamJob.java b/app/src/main/java/ch/protonmail/android/jobs/PostSpamJob.java index b4f11e9d1..b39875ed4 100644 --- a/app/src/main/java/ch/protonmail/android/jobs/PostSpamJob.java +++ b/app/src/main/java/ch/protonmail/android/jobs/PostSpamJob.java @@ -58,7 +58,7 @@ public void onAdded() { .getDatabase(); int totalUnread = 0; for (String id : mMessageIds) { - final Message message = getMessageDetailsRepository().findMessageById(id); + final Message message = getMessageDetailsRepository().findMessageByIdBlocking(id); if (message != null) { if (markMessageLocally(countersDatabase,message)) { totalUnread++; diff --git a/app/src/main/java/ch/protonmail/android/jobs/PostTrashJob.java b/app/src/main/java/ch/protonmail/android/jobs/PostTrashJob.java index e888fd357..b0a4449ba 100644 --- a/app/src/main/java/ch/protonmail/android/jobs/PostTrashJob.java +++ b/app/src/main/java/ch/protonmail/android/jobs/PostTrashJob.java @@ -53,7 +53,7 @@ public void onAdded() { int totalUnread = 0; for (String id : mMessageIds) { - Message message = getMessageDetailsRepository().findMessageById(id); + Message message = getMessageDetailsRepository().findMessageByIdBlocking(id); if (message != null) { if (!message.isRead()) { UnreadLocationCounter unreadLocationCounter = countersDatabase.findUnreadLocationById(message.getLocation()); diff --git a/app/src/main/java/ch/protonmail/android/jobs/PostTrashJobV2.java b/app/src/main/java/ch/protonmail/android/jobs/PostTrashJobV2.java index 6cb02aa3a..b6e221572 100644 --- a/app/src/main/java/ch/protonmail/android/jobs/PostTrashJobV2.java +++ b/app/src/main/java/ch/protonmail/android/jobs/PostTrashJobV2.java @@ -62,7 +62,7 @@ public void onAdded() { .getDatabase(); int totalUnread = 0; for (String id : mMessageIds) { - Message message = getMessageDetailsRepository().findMessageById(id); + Message message = getMessageDetailsRepository().findMessageByIdBlocking(id); if (message != null) { if (!message.isRead()) { UnreadLocationCounter unreadLocationCounter = countersDatabase.findUnreadLocationById(message.getLocation()); diff --git a/app/src/main/java/ch/protonmail/android/jobs/PostUnreadJob.java b/app/src/main/java/ch/protonmail/android/jobs/PostUnreadJob.java index 0bf8fadd2..85c13d7ab 100644 --- a/app/src/main/java/ch/protonmail/android/jobs/PostUnreadJob.java +++ b/app/src/main/java/ch/protonmail/android/jobs/PostUnreadJob.java @@ -49,7 +49,7 @@ public void onAdded() { Constants.MessageLocationType messageLocation = Constants.MessageLocationType.INVALID; boolean starred = false; for (String id : mMessageIds) { - final Message message = getMessageDetailsRepository().findMessageById(id); + final Message message = getMessageDetailsRepository().findMessageByIdBlocking(id); if (message != null) { starred = message.isStarred() !=null && message.isStarred(); messageLocation = Constants.MessageLocationType.Companion.fromInt(message.getLocation()); diff --git a/app/src/main/java/ch/protonmail/android/jobs/PostUnstarJob.java b/app/src/main/java/ch/protonmail/android/jobs/PostUnstarJob.java index 94f9e1fd8..df4225a77 100644 --- a/app/src/main/java/ch/protonmail/android/jobs/PostUnstarJob.java +++ b/app/src/main/java/ch/protonmail/android/jobs/PostUnstarJob.java @@ -50,7 +50,7 @@ public void onAdded() { Constants.MessageLocationType messageLocation = Constants.MessageLocationType.INVALID; boolean isUnread = false; - final Message message = getMessageDetailsRepository().findMessageById(id); + final Message message = getMessageDetailsRepository().findMessageByIdBlocking(id); if (message != null) { messageLocation = Constants.MessageLocationType.Companion.fromInt(message.getLocation()); isUnread = !message.isRead(); diff --git a/app/src/main/java/ch/protonmail/android/jobs/ProtonMailCounterJob.java b/app/src/main/java/ch/protonmail/android/jobs/ProtonMailCounterJob.java index 948202cd5..1fae90862 100644 --- a/app/src/main/java/ch/protonmail/android/jobs/ProtonMailCounterJob.java +++ b/app/src/main/java/ch/protonmail/android/jobs/ProtonMailCounterJob.java @@ -53,7 +53,7 @@ protected void onProtonCancel(int cancelReason, @Nullable Throwable throwable) { int totalUnread = 0; List messageIds = getMessageIds(); for (String id : messageIds) { - final Message message = getMessageDetailsRepository().findMessageById(id); + final Message message = getMessageDetailsRepository().findMessageByIdBlocking(id); if (message != null) { if ( !message.isRead() ) { UnreadLocationCounter unreadLocationCounter = countersDatabase.findUnreadLocationById(message.getLocation()); diff --git a/app/src/main/java/ch/protonmail/android/jobs/RemoveLabelJob.java b/app/src/main/java/ch/protonmail/android/jobs/RemoveLabelJob.java index f26bb297e..3e6c92456 100644 --- a/app/src/main/java/ch/protonmail/android/jobs/RemoveLabelJob.java +++ b/app/src/main/java/ch/protonmail/android/jobs/RemoveLabelJob.java @@ -51,7 +51,7 @@ protected void onProtonCancel(int cancelReason, @Nullable Throwable throwable) { .getDatabase(); int totalUnread = 0; for (String messageId : messageIds) { - Message message = getMessageDetailsRepository().findMessageById(messageId); + Message message = getMessageDetailsRepository().findMessageByIdBlocking(messageId); if (message == null) { continue; } @@ -76,7 +76,7 @@ public void onAdded() { int totalUnread = 0; for (String messageId : messageIds) { - Message message = getMessageDetailsRepository().findMessageById(messageId); + Message message = getMessageDetailsRepository().findMessageByIdBlocking(messageId); if (message == null) { continue; } diff --git a/app/src/main/java/ch/protonmail/android/receivers/NotificationReceiver.kt b/app/src/main/java/ch/protonmail/android/receivers/NotificationReceiver.kt index 0fcfd08aa..e7c204814 100644 --- a/app/src/main/java/ch/protonmail/android/receivers/NotificationReceiver.kt +++ b/app/src/main/java/ch/protonmail/android/receivers/NotificationReceiver.kt @@ -82,7 +82,7 @@ class NotificationReceiver : BroadcastReceiver() { messageId: String ) { withContext(Dispatchers.Default) { - val message = messageDetailsRepository.findMessageById(messageId) + val message = messageDetailsRepository.findMessageByIdBlocking(messageId) if (message != null) { val job: Job = PostTrashJobV2(listOf(message.messageId), null) jobManager.addJobInBackground(job) diff --git a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt index f50460b2b..ae2a1cd67 100644 --- a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt +++ b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt @@ -118,7 +118,7 @@ class SaveDraft @Inject constructor( updatePendingForSendMessage(createdDraftId, localDraftId) deleteOfflineDraft(localDraftId) - messageDetailsRepository.findMessageById(createdDraftId.orEmpty())?.let { + messageDetailsRepository.findMessageByIdBlocking(createdDraftId.orEmpty())?.let { val uploadResult = uploadAttachments(params.newAttachmentIds, it, addressCrypto) if (uploadResult is UploadAttachments.Result.Failure) { return@withContext Result.UploadDraftAttachmentsFailed @@ -135,7 +135,7 @@ class SaveDraft @Inject constructor( } private fun deleteOfflineDraft(localDraftId: String) { - val offlineDraft = messageDetailsRepository.findMessageById(localDraftId) + val offlineDraft = messageDetailsRepository.findMessageByIdBlocking(localDraftId) offlineDraft?.let { messageDetailsRepository.deleteMessage(offlineDraft) } diff --git a/app/src/main/java/ch/protonmail/android/usecase/delete/DeleteMessage.kt b/app/src/main/java/ch/protonmail/android/usecase/delete/DeleteMessage.kt index dc474b04f..2e520003b 100644 --- a/app/src/main/java/ch/protonmail/android/usecase/delete/DeleteMessage.kt +++ b/app/src/main/java/ch/protonmail/android/usecase/delete/DeleteMessage.kt @@ -52,7 +52,7 @@ class DeleteMessage @Inject constructor( for (id in validMessageIdList) { ensureActive() - messageDetailsRepository.findMessageById(id)?.let { message -> + messageDetailsRepository.findMessageByIdBlocking(id)?.let { message -> message.deleted = true messagesToSave.add(message) } diff --git a/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt b/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt index 43f130b63..ae095128e 100644 --- a/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt +++ b/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt @@ -94,7 +94,7 @@ class CreateDraftWorker @WorkerInject constructor( parentId?.let { createDraftRequest.setParentID(parentId) createDraftRequest.action = getInputActionType().messageActionTypeValue - val parentMessage = messageDetailsRepository.findMessageById(parentId) + val parentMessage = messageDetailsRepository.findMessageByIdBlocking(parentId) val attachments = parentMessage?.attachments(messageDetailsRepository.databaseProvider.provideMessagesDao()) buildDraftRequestParentAttachments(attachments, senderAddress).forEach { diff --git a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt index ad99496c6..a6e2ad3cf 100644 --- a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt +++ b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt @@ -112,11 +112,11 @@ class ComposeMessageViewModelTest : CoroutinesTest { val testObserver = viewModel.savingDraftComplete.testObserver() givenViewModelPropertiesAreInitialised() coEvery { saveDraft(any()) } returns flowOf(SaveDraft.Result.Success(createdDraftId)) - every { messageDetailsRepository.findMessageById(createdDraftId) } returns createdDraft + every { messageDetailsRepository.findMessageByIdBlocking(createdDraftId) } returns createdDraft viewModel.saveDraft(message, hasConnectivity = false) - verify { messageDetailsRepository.findMessageById(createdDraftId) } + verify { messageDetailsRepository.findMessageByIdBlocking(createdDraftId) } assertEquals(createdDraft, testObserver.observedValues[0]) } } @@ -129,7 +129,7 @@ class ComposeMessageViewModelTest : CoroutinesTest { val createdDraft = Message(messageId = createdDraftId, localId = localDraftId) givenViewModelPropertiesAreInitialised() coEvery { saveDraft(any()) } returns flowOf(SaveDraft.Result.Success(createdDraftId)) - every { messageDetailsRepository.findMessageById(createdDraftId) } returns createdDraft + every { messageDetailsRepository.findMessageByIdBlocking(createdDraftId) } returns createdDraft viewModel.saveDraft(Message(), hasConnectivity = false) @@ -145,7 +145,7 @@ class ComposeMessageViewModelTest : CoroutinesTest { val createdDraft = Message(messageId = createdDraftId, localId = localDraftId) givenViewModelPropertiesAreInitialised() coEvery { saveDraft(any()) } returns flowOf(SaveDraft.Result.Success(createdDraftId)) - every { messageDetailsRepository.findMessageById(createdDraftId) } returns createdDraft + every { messageDetailsRepository.findMessageByIdBlocking(createdDraftId) } returns createdDraft viewModel.saveDraft(Message(), hasConnectivity = false) diff --git a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt index 41aa17edd..49d595d87 100644 --- a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt +++ b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt @@ -301,7 +301,7 @@ class SaveDraftTest : CoroutinesTest { localId = localDraftId } coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 9833L - every { messageDetailsRepository.findMessageById("45623") } returns message + every { messageDetailsRepository.findMessageByIdBlocking("45623") } returns message every { pendingActionsDao.findPendingSendByDbId(9833L) } returns null every { pendingActionsDao.findPendingSendByOfflineMessageId(localDraftId) } returns PendingSend() coEvery { uploadAttachments(any(), any(), any()) } returns UploadAttachments.Result.Success @@ -347,8 +347,8 @@ class SaveDraftTest : CoroutinesTest { val workerStatusFlow = buildCreateDraftWorkerResponse(WorkInfo.State.SUCCEEDED, workOutputData) val newAttachmentIds = listOf("2345", "453") coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 9833L - every { messageDetailsRepository.findMessageById("45623") } returns message - every { messageDetailsRepository.findMessageById("createdDraftMessageId345") } returns apiDraft + every { messageDetailsRepository.findMessageByIdBlocking("45623") } returns message + every { messageDetailsRepository.findMessageByIdBlocking("createdDraftMessageId345") } returns apiDraft every { pendingActionsDao.findPendingSendByDbId(9833L) } returns null every { pendingActionsDao.findPendingSendByOfflineMessageId(localDraftId) } returns PendingSend() coEvery { uploadAttachments(any(), apiDraft, any()) } returns UploadAttachments.Result.Success @@ -390,7 +390,7 @@ class SaveDraftTest : CoroutinesTest { ) val workerStatusFlow = buildCreateDraftWorkerResponse(WorkInfo.State.FAILED, workOutputData) coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 9833L - every { messageDetailsRepository.findMessageById("45623") } returns message + every { messageDetailsRepository.findMessageByIdBlocking("45623") } returns message every { pendingActionsDao.findPendingSendByDbId(9833L) } returns null every { createDraftScheduler.enqueue( @@ -430,7 +430,7 @@ class SaveDraftTest : CoroutinesTest { ) val workerStatusFlow = buildCreateDraftWorkerResponse(WorkInfo.State.SUCCEEDED, workOutputData) coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 9833L - every { messageDetailsRepository.findMessageById("45623") } returns message + every { messageDetailsRepository.findMessageByIdBlocking("45623") } returns message every { pendingActionsDao.findPendingSendByDbId(9833L) } returns null coEvery { uploadAttachments(newAttachmentIds, any(), any()) } returns Failure("Can't upload attachments") every { @@ -471,8 +471,8 @@ class SaveDraftTest : CoroutinesTest { val workerStatusFlow = buildCreateDraftWorkerResponse(WorkInfo.State.SUCCEEDED, workOutputData) val newAttachmentIds = listOf("2345", "453") coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 9833L - every { messageDetailsRepository.findMessageById("45623") } returns message - every { messageDetailsRepository.findMessageById("createdDraftMessageId345") } returns apiDraft + every { messageDetailsRepository.findMessageByIdBlocking("45623") } returns message + every { messageDetailsRepository.findMessageByIdBlocking("createdDraftMessageId345") } returns apiDraft every { pendingActionsDao.findPendingSendByDbId(9833L) } returns null every { pendingActionsDao.findPendingSendByOfflineMessageId(localDraftId) } returns PendingSend() coEvery { uploadAttachments(any(), apiDraft, any()) } returns UploadAttachments.Result.Success diff --git a/app/src/test/java/ch/protonmail/android/usecase/delete/DeleteMessageTest.kt b/app/src/test/java/ch/protonmail/android/usecase/delete/DeleteMessageTest.kt index 9412dd433..a7188efd7 100644 --- a/app/src/test/java/ch/protonmail/android/usecase/delete/DeleteMessageTest.kt +++ b/app/src/test/java/ch/protonmail/android/usecase/delete/DeleteMessageTest.kt @@ -66,7 +66,7 @@ class DeleteMessageTest { val operation = mockk(relaxed = true) every { db.findPendingUploadByMessageId(any()) } returns null every { db.findPendingSendByMessageId(any()) } returns null - every { repository.findMessageById(messId) } returns message + every { repository.findMessageByIdBlocking(messId) } returns message every { repository.saveMessageInDB(message) } returns 1L every { repository.findSearchMessageById(messId) } returns null every { repository.saveMessagesInOneTransaction(any()) } returns Unit @@ -93,7 +93,7 @@ class DeleteMessageTest { val searchMessage = mockk(relaxed = true) every { db.findPendingUploadByMessageId(any()) } returns null every { db.findPendingSendByMessageId(any()) } returns null - every { repository.findMessageById(messId) } returns null + every { repository.findMessageByIdBlocking(messId) } returns null every { repository.findSearchMessageById(messId) } returns searchMessage every { repository.saveSearchMessageInDB(searchMessage) } returns Unit every { repository.saveMessagesInOneTransaction(any()) } returns Unit @@ -121,7 +121,7 @@ class DeleteMessageTest { val operation = mockk(relaxed = true) every { db.findPendingUploadByMessageId(any()) } returns pendingUpload every { db.findPendingSendByMessageId(any()) } returns null - every { repository.findMessageById(messId) } returns message + every { repository.findMessageByIdBlocking(messId) } returns message every { repository.saveMessageInDB(message) } returns 1L every { repository.findSearchMessageById(messId) } returns null every { repository.saveMessagesInOneTransaction(any()) } returns Unit @@ -151,7 +151,7 @@ class DeleteMessageTest { val operation = mockk(relaxed = true) every { db.findPendingUploadByMessageId(any()) } returns null every { db.findPendingSendByMessageId(any()) } returns pendingSend - every { repository.findMessageById(messId) } returns null + every { repository.findMessageByIdBlocking(messId) } returns null every { repository.findSearchMessageById(messId) } returns message every { repository.saveMessageInDB(message) } returns 1L every { repository.saveSearchMessageInDB(message) } returns Unit diff --git a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt index 8585862cc..72345d1cc 100644 --- a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt +++ b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt @@ -200,7 +200,7 @@ class CreateDraftWorkerTest : CoroutinesTest { verify { apiDraftMessage.action = 2 } // Always get parent message from messageDetailsDB, never from searchDB // ignoring isTransient property as the values in the two DB appears to be the same - verify { messageDetailsRepository.findMessageById(parentId) } + verify { messageDetailsRepository.findMessageByIdBlocking(parentId) } } } @@ -312,7 +312,7 @@ class CreateDraftWorkerTest : CoroutinesTest { givenPreviousSenderAddress(previousSenderAddressId) every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message every { messageFactory.createDraftApiRequest(message) } answers { apiDraftMessage } - every { messageDetailsRepository.findMessageById(parentId) } returns parentMessage + every { messageDetailsRepository.findMessageByIdBlocking(parentId) } returns parentMessage every { userManager.username } returns username every { userManager.getUser(username).loadNew(username) } returns mockk { every { findAddressById(Id("addressId835")) } returns senderAddress @@ -358,7 +358,7 @@ class CreateDraftWorkerTest : CoroutinesTest { givenPreviousSenderAddress(previousSenderAddressId) every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message every { messageFactory.createDraftApiRequest(message) } answers { apiDraftMessage } - every { messageDetailsRepository.findMessageById(parentId) } returns parentMessage + every { messageDetailsRepository.findMessageByIdBlocking(parentId) } returns parentMessage every { userManager.username } returns "username93w" // When @@ -394,7 +394,7 @@ class CreateDraftWorkerTest : CoroutinesTest { givenPreviousSenderAddress("") every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message every { messageFactory.createDraftApiRequest(message) } answers { apiDraftMessage } - every { messageDetailsRepository.findMessageById(parentId) } returns parentMessage + every { messageDetailsRepository.findMessageByIdBlocking(parentId) } returns parentMessage // When worker.doWork() @@ -434,7 +434,7 @@ class CreateDraftWorkerTest : CoroutinesTest { givenPreviousSenderAddress("") every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message every { messageFactory.createDraftApiRequest(message) } answers { apiDraftMessage } - every { messageDetailsRepository.findMessageById(parentId) } returns parentMessage + every { messageDetailsRepository.findMessageByIdBlocking(parentId) } returns parentMessage // When worker.doWork() From 4ca172ca5895f6f5c4da88f15452ccb86c04bcdb Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Thu, 17 Dec 2020 16:44:51 +0100 Subject: [PATCH 026/145] Delete DraftCreatedEvent and CreateAndPostDraftJob The listeners of DraftCreatedEvent have been deleted too, as the actions they are taking when receiving back the draft's creation result should not cause any actual changes locally that wouldn't be done anyways by updating the draft --- .../activities/AddAttachmentsActivity.java | 85 ----- .../ComposeMessageActivity.java | 2 - .../android/core/ProtonMailApplication.java | 16 - .../android/events/DraftCreatedEvent.java | 61 ---- .../android/jobs/CreateAndPostDraftJob.java | 320 ------------------ .../settings/pin/ValidatePinActivity.kt | 11 - 6 files changed, 495 deletions(-) delete mode 100644 app/src/main/java/ch/protonmail/android/events/DraftCreatedEvent.java delete mode 100644 app/src/main/java/ch/protonmail/android/jobs/CreateAndPostDraftJob.java diff --git a/app/src/main/java/ch/protonmail/android/activities/AddAttachmentsActivity.java b/app/src/main/java/ch/protonmail/android/activities/AddAttachmentsActivity.java index de7ebd1cb..e5f63e9a1 100644 --- a/app/src/main/java/ch/protonmail/android/activities/AddAttachmentsActivity.java +++ b/app/src/main/java/ch/protonmail/android/activities/AddAttachmentsActivity.java @@ -24,7 +24,6 @@ import android.content.pm.PackageManager; import android.content.res.Configuration; import android.net.Uri; -import android.os.AsyncTask; import android.os.Bundle; import android.os.Environment; import android.os.ParcelFileDescriptor; @@ -49,7 +48,6 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; -import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; import java.util.Random; @@ -61,16 +59,13 @@ import ch.protonmail.android.R; import ch.protonmail.android.activities.guest.LoginActivity; import ch.protonmail.android.adapters.AttachmentListAdapter; -import ch.protonmail.android.api.models.room.messages.Attachment; import ch.protonmail.android.api.models.room.messages.LocalAttachment; -import ch.protonmail.android.api.models.room.messages.Message; import ch.protonmail.android.api.models.room.messages.MessagesDatabase; import ch.protonmail.android.api.models.room.messages.MessagesDatabaseFactory; import ch.protonmail.android.attachments.ImportAttachmentsWorker; import ch.protonmail.android.core.Constants; import ch.protonmail.android.core.ProtonMailApplication; import ch.protonmail.android.events.DownloadedAttachmentEvent; -import ch.protonmail.android.events.DraftCreatedEvent; import ch.protonmail.android.events.LogoutEvent; import ch.protonmail.android.events.PostImportAttachmentEvent; import ch.protonmail.android.events.PostImportAttachmentFailureEvent; @@ -83,7 +78,6 @@ import ch.protonmail.android.utils.extensions.TextExtensions; import ch.protonmail.android.utils.ui.dialogs.DialogUtils; import dagger.hilt.android.AndroidEntryPoint; -import kotlin.Unit; import kotlin.collections.ArraysKt; import kotlin.collections.CollectionsKt; import timber.log.Timber; @@ -350,31 +344,6 @@ public void onPostImportAttachmentFailureEvent(PostImportAttachmentFailureEvent TextExtensions.showToast(this, R.string.problem_selecting_file); } - @Subscribe - public void onDraftCreatedEvent(DraftCreatedEvent event) { - if (event == null) { - return; - } - if (mDraftId == null || !mDraftId.equals(event.getOldMessageId())) { - return; - } - mDraftCreated = true; - invalidateOptionsMenu(); - mProgressLayout.setVisibility(View.GONE); - String newMessageId = event.getMessageId(); - if (event.getStatus() != Status.NO_NETWORK) { - if (!TextUtils.isEmpty(mDraftId) && !TextUtils.isEmpty(newMessageId)) { - new DeleteMessageByIdTask(messagesDatabase, mDraftId).execute(); - } - } - mDraftId = event.getMessageId(); - if (event.getStatus() == Status.SUCCESS) { - final Message eventMessage = event.getMessage(); - new UpdateAttachmentsAdapterTask(new WeakReference<>(this), eventMessage, - AddAttachmentsActivity.this.messagesDatabase, mAdapter).execute(); - } - } - @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); @@ -571,60 +540,6 @@ private int countEmbeddedImages(List attachments) { return embeddedImages; } - private static class DeleteMessageByIdTask extends AsyncTask { - - private final MessagesDatabase messagesDatabase; - private final String draftId; - - private DeleteMessageByIdTask(MessagesDatabase messagesDatabase, String draftId) { - this.messagesDatabase = messagesDatabase; - this.draftId = draftId; - } - - @Override - protected Unit doInBackground(Unit... units) { - messagesDatabase.deleteMessageById(draftId); - return Unit.INSTANCE; - } - } - - private static class UpdateAttachmentsAdapterTask extends AsyncTask> { - private final WeakReference addAttachmentsActivityWeakReference; - private final MessagesDatabase messagesDatabase; - private final AttachmentListAdapter adapter; - private final Message eventMessage; - - UpdateAttachmentsAdapterTask( - WeakReference addAttachmentsActivityWeakReference, Message eventMessage, MessagesDatabase messagesDatabase, - AttachmentListAdapter adapter) { - this.addAttachmentsActivityWeakReference = addAttachmentsActivityWeakReference; - this.messagesDatabase = messagesDatabase; - this.adapter = adapter; - this.eventMessage = eventMessage; - } - - @Override - protected List doInBackground(Unit... units) { - return eventMessage.attachments(messagesDatabase); - } - - @Override - protected void onPostExecute(List messageAttachments) { - AddAttachmentsActivity addAttachmentsActivity = addAttachmentsActivityWeakReference.get(); - if (addAttachmentsActivity == null) { - return; - } - int attachmentsCount = messageAttachments.size(); - if (adapter.getData().size() <= attachmentsCount) { - - ArrayList attachments = new ArrayList<>(LocalAttachment.Companion.createLocalAttachmentList(messageAttachments)); - int totalEmbeddedImages = addAttachmentsActivity.countEmbeddedImages(attachments); - addAttachmentsActivity.updateAttachmentsCount(attachmentsCount, totalEmbeddedImages); - adapter.updateData(attachments, totalEmbeddedImages); - } - } - } - private boolean openGallery() { if (!isAttachmentsCountAllowed()) { TextExtensions.showToast(this, R.string.max_attachments_reached); diff --git a/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java b/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java index 866c7d600..037e796f1 100644 --- a/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java +++ b/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java @@ -1198,13 +1198,11 @@ private void sendMessage(boolean sendAnyway) { unit -> { UiUtil.hideKeyboard(this); composeMessageViewModel.finishBuildingMessage(mComposeBodyEditText.getText().toString()); - ProtonMailApplication.getApplication().resetDraftCreated(); return unit; }, true); } else { UiUtil.hideKeyboard(this); composeMessageViewModel.finishBuildingMessage(mComposeBodyEditText.getText().toString()); - ProtonMailApplication.getApplication().resetDraftCreated(); } } } diff --git a/app/src/main/java/ch/protonmail/android/core/ProtonMailApplication.java b/app/src/main/java/ch/protonmail/android/core/ProtonMailApplication.java index 7bd5985cf..7541c6d4a 100644 --- a/app/src/main/java/ch/protonmail/android/core/ProtonMailApplication.java +++ b/app/src/main/java/ch/protonmail/android/core/ProtonMailApplication.java @@ -87,7 +87,6 @@ import ch.protonmail.android.events.ApiOfflineEvent; import ch.protonmail.android.events.AuthStatus; import ch.protonmail.android.events.DownloadedAttachmentEvent; -import ch.protonmail.android.events.DraftCreatedEvent; import ch.protonmail.android.events.ForceUpgradeEvent; import ch.protonmail.android.events.InvalidAccessTokenEvent; import ch.protonmail.android.events.Login2FAEvent; @@ -161,7 +160,6 @@ public class ProtonMailApplication extends Application implements androidx.work. private Snackbar apiOfflineSnackBar; @Nullable private StorageLimitEvent mLastStorageLimitEvent; - private DraftCreatedEvent mLastDraftCreatedEvent; private WeakReference mCurrentActivity; private boolean mUpdateOccurred; private AllCurrencyPlans mAllCurrencyPlans; @@ -302,20 +300,6 @@ public StorageLimitEvent produceStorageLimitEvent() { return latestEvent; } - @Produce - public DraftCreatedEvent produceDraftCreatedEvent() { - return mLastDraftCreatedEvent; - } - - @Subscribe - public void onDraftCreatedEvent(DraftCreatedEvent event) { - mLastDraftCreatedEvent = event; - } - - public void resetDraftCreated() { - mLastDraftCreatedEvent = null; - } - @Subscribe public void onOrganizationEvent(OrganizationEvent event) { if (event.getStatus() == Status.SUCCESS) { diff --git a/app/src/main/java/ch/protonmail/android/events/DraftCreatedEvent.java b/app/src/main/java/ch/protonmail/android/events/DraftCreatedEvent.java deleted file mode 100644 index 341da5456..000000000 --- a/app/src/main/java/ch/protonmail/android/events/DraftCreatedEvent.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2020 Proton Technologies AG - * - * This file is part of ProtonMail. - * - * ProtonMail is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ProtonMail is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ProtonMail. If not, see https://www.gnu.org/licenses/. - */ -package ch.protonmail.android.events; - -import java.io.Serializable; - -import ch.protonmail.android.api.models.room.messages.Message; - -/** - * Created by sunny on 8/12/15. - */ -public class DraftCreatedEvent implements Serializable { - private String messageId; - private String oldMessageId; - private Status status; - private Message message; - - public DraftCreatedEvent(String messageId, String oldMessageId, Message message){ - this.messageId = messageId; - this.oldMessageId = oldMessageId; - this.message = message; - this.status = Status.SUCCESS; - } - - public DraftCreatedEvent(String messageId, String oldMessageId, Message message, Status status) { - this(messageId, oldMessageId, message); - this.status = status; - } - - public String getMessageId(){ - return messageId; - } - - public Status getStatus() { - return status; - } - - public String getOldMessageId() { - return oldMessageId; - } - - public Message getMessage() { - return message; - } -} diff --git a/app/src/main/java/ch/protonmail/android/jobs/CreateAndPostDraftJob.java b/app/src/main/java/ch/protonmail/android/jobs/CreateAndPostDraftJob.java deleted file mode 100644 index 871ced0c4..000000000 --- a/app/src/main/java/ch/protonmail/android/jobs/CreateAndPostDraftJob.java +++ /dev/null @@ -1,320 +0,0 @@ -/* - * Copyright (c) 2020 Proton Technologies AG - * - * This file is part of ProtonMail. - * - * ProtonMail is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ProtonMail is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ProtonMail. If not, see https://www.gnu.org/licenses/. - */ -package ch.protonmail.android.jobs; - -import android.text.TextUtils; -import android.util.Base64; -import android.webkit.URLUtil; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.birbit.android.jobqueue.Params; -import com.birbit.android.jobqueue.RetryConstraint; - -import java.io.File; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import ch.protonmail.android.api.models.IDList; -import ch.protonmail.android.api.models.DraftBody; -import ch.protonmail.android.api.models.User; -import ch.protonmail.android.api.models.address.Address; -import ch.protonmail.android.api.models.messages.receive.AttachmentFactory; -import ch.protonmail.android.api.models.messages.receive.MessageFactory; -import ch.protonmail.android.api.models.messages.receive.MessageResponse; -import ch.protonmail.android.api.models.messages.receive.MessageSenderFactory; -import ch.protonmail.android.api.models.messages.receive.ServerMessage; -import ch.protonmail.android.api.models.messages.receive.ServerMessageSender; -import ch.protonmail.android.api.models.room.messages.Attachment; -import ch.protonmail.android.api.models.room.messages.Message; -import ch.protonmail.android.api.models.room.messages.MessageSender; -import ch.protonmail.android.api.models.room.messages.MessagesDatabase; -import ch.protonmail.android.api.models.room.messages.MessagesDatabaseFactory; -import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDatabase; -import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDatabaseFactory; -import ch.protonmail.android.api.models.room.pendingActions.PendingSend; -import ch.protonmail.android.api.utils.Fields; -import ch.protonmail.android.core.Constants; -import ch.protonmail.android.crypto.AddressCrypto; -import ch.protonmail.android.crypto.Crypto; -import ch.protonmail.android.domain.entity.user.AddressKeys; -import ch.protonmail.android.events.AttachmentFailedEvent; -import ch.protonmail.android.events.DraftCreatedEvent; -import ch.protonmail.android.utils.AppUtil; -import ch.protonmail.android.utils.Logger; - -public class CreateAndPostDraftJob extends ProtonMailBaseJob { - - private static final String TAG_CREATE_AND_POST_DRAFT_JOB = "CreateAndPostDraftJob"; - private static final int CREATE_DRAFT_RETRY_LIMIT = 10; - - private Long mDbMessageId; - private final String mParentId; - private final Constants.MessageActionType mActionType; - private final boolean mUploadAttachments; - private final List mNewAttachments; - private final String mOldSenderAddressID; - private final String oldId; - private final boolean isTransient; - private final String mUsername; - - public CreateAndPostDraftJob(@NonNull Long dbMessageId, String localMessageId, String parentId, Constants.MessageActionType actionType, - boolean uploadAttachments, @NonNull List newAttachments, String oldSenderId, boolean isTransient, String username) { - super(new Params(Priority.HIGH).requireNetwork().persist().groupBy(Constants.JOB_GROUP_SENDING)); - mDbMessageId = dbMessageId; - oldId = localMessageId; - mParentId = parentId; - mActionType = actionType; - mUploadAttachments = uploadAttachments; - mNewAttachments = newAttachments; - mOldSenderAddressID = oldSenderId; - this.isTransient = isTransient; - mUsername = username; - } - - @Override - protected int getRetryLimit() { - return CREATE_DRAFT_RETRY_LIMIT; - } - - @Override - protected void onProtonCancel(int cancelReason, @Nullable Throwable throwable) { - PendingActionsDatabase pendingActionsDatabase = PendingActionsDatabaseFactory.Companion.getInstance(getApplicationContext()).getDatabase(); - pendingActionsDatabase.deletePendingDraftById(mDbMessageId); - } - - @Override - public void onRun() throws Throwable { - getMessageDetailsRepository().reloadDependenciesForUser(mUsername); - // first save draft with -ve messageId so it won't overwrite any message - MessagesDatabase messagesDatabase = MessagesDatabaseFactory.Companion.getInstance(getApplicationContext()).getDatabase(); - MessagesDatabase searchDatabase = MessagesDatabaseFactory.Companion.getSearchDatabase(getApplicationContext()).getDatabase(); - PendingActionsDatabase pendingActionsDatabase = PendingActionsDatabaseFactory.Companion.getInstance(getApplicationContext()).getDatabase(); - - Message message = getMessageDetailsRepository().findMessageByMessageDbId(mDbMessageId); - PendingSend pendingForSending = pendingActionsDatabase.findPendingSendByDbId(message.getDbId()); - - if (pendingForSending != null) { - return; // sending already pressed and in process, so no need to create draft, it will be created from the post send job - } - - message.setLocation(Constants.MessageLocationType.DRAFT.getMessageLocationTypeValue()); - AttachmentFactory attachmentFactory = new AttachmentFactory(); - MessageSenderFactory messageSenderFactory = new MessageSenderFactory(); - MessageFactory messageFactory = new MessageFactory(attachmentFactory, messageSenderFactory); - - final ServerMessage serverMessage = messageFactory.createServerMessage(message); - final DraftBody newDraft = new DraftBody(serverMessage); - Message parentMessage = null; - if (mParentId != null) { - newDraft.setParentID(mParentId); - newDraft.setAction(mActionType.getMessageActionTypeValue()); - if(!isTransient) { - parentMessage = getMessageDetailsRepository().findMessageByIdBlocking(mParentId); - } else { - parentMessage = getMessageDetailsRepository().findSearchMessageById(mParentId); - } - } - String addressId = message.getAddressID(); - String encryptedMessage = message.getMessageBody(); - if (!TextUtils.isEmpty(message.getMessageId())) { - Message savedMessage = getMessageDetailsRepository().findMessageByIdBlocking(message.getMessageId()); - if (savedMessage != null) { - encryptedMessage = savedMessage.getMessageBody(); - } - } - User user = getUserManager().getUser(mUsername); - Address senderAddress = user.getAddressById(addressId); - newDraft.getMessage().setSender(new ServerMessageSender(senderAddress.getDisplayName(), senderAddress.getEmail())); - AddressCrypto crypto = Crypto.forAddress(getUserManager(), mUsername, message.getAddressID()); - newDraft.getMessage().setBody(encryptedMessage); - List parentAttachmentList = null; - if (parentMessage != null) { - if(!isTransient) { - parentAttachmentList = parentMessage.attachments(messagesDatabase); - } else { - parentAttachmentList = parentMessage.attachments(searchDatabase); - } - } - if (parentAttachmentList != null) { - updateAttachmentKeyPackets(parentAttachmentList, newDraft, mOldSenderAddressID, senderAddress); - } - if (message.getSenderEmail().contains("+")) { // it's being sent by alias - newDraft.getMessage().setSender(new ServerMessageSender(message.getSenderName(), message.getSenderEmail())); - } - final MessageResponse draftResponse = getApi().createDraftBlocking(newDraft); - // on success update draft with messageId - - String newId = draftResponse.getMessageId(); - Message draftMessage = draftResponse.getMessage(); - getApi().markMessageAsRead(new IDList(Collections.singletonList(newId))); - draftMessage.setDbId(mDbMessageId); - draftMessage.setToList(message.getToList()); - draftMessage.setCcList(message.getCcList()); - draftMessage.setBccList(message.getBccList()); - draftMessage.setReplyTos(message.getReplyTos()); - draftMessage.setSender(message.getSender()); - draftMessage.setLabelIDs(message.getEventLabelIDs()); - draftMessage.setParsedHeaders(message.getParsedHeaders()); - draftMessage.setDownloaded(true); - draftMessage.setIsRead(true); - draftMessage.setNumAttachments(message.getNumAttachments()); - draftMessage.setLocalId(oldId); - - for (Attachment atta : draftMessage.getAttachments()) { - if (parentAttachmentList != null && !parentAttachmentList.isEmpty()) { - for (Attachment parentAtta : parentAttachmentList) { - if (parentAtta.getKeyPackets().equals(atta.getKeyPackets())) { - atta.setInline(parentAtta.getInline()); - } - } - } - } - getMessageDetailsRepository().saveMessageInDB(draftMessage); - - pendingForSending = pendingActionsDatabase.findPendingSendByOfflineMessageId(oldId); - if (pendingForSending != null) { - pendingForSending.setMessageId(newId); - pendingActionsDatabase.insertPendingForSend(pendingForSending); - } - Message offlineDraft = getMessageDetailsRepository().findMessageByIdBlocking(oldId); - if (offlineDraft != null) { - getMessageDetailsRepository().deleteMessage(offlineDraft); - } - - if (message.getNumAttachments() >= 1 && mUploadAttachments && !mNewAttachments.isEmpty()) { - List listOfAttachments = new ArrayList<>(); - for (String attachmentId : mNewAttachments) { - listOfAttachments.add(messagesDatabase.findAttachmentById(attachmentId)); - } - getJobManager().addJob(new PostCreateDraftAttachmentsJob(newId, oldId, mUploadAttachments, listOfAttachments, crypto, mUsername)); - } else { - DraftCreatedEvent draftCreatedEvent = new DraftCreatedEvent(message.getMessageId(), oldId, draftMessage); - AppUtil.postEventOnUi(draftCreatedEvent); - } - } - - private void updateAttachmentKeyPackets(List attachmentList, DraftBody draftBody, String oldSenderAddress, Address newSenderAddress) throws Exception { - if (!TextUtils.isEmpty(oldSenderAddress)) { - AddressCrypto oldCrypto = Crypto.forAddress(getUserManager(), mUsername, oldSenderAddress); - AddressKeys newAddressKeys = newSenderAddress.toNewAddress().getKeys(); - String newPublicKey = oldCrypto.buildArmoredPublicKey(newAddressKeys.getPrimaryKey().getPrivateKey()); - for (Attachment attachment : attachmentList) { - if (mActionType == Constants.MessageActionType.FORWARD || - ((mActionType == Constants.MessageActionType.REPLY || mActionType == Constants.MessageActionType.REPLY_ALL) && attachment.getInline())) { - String AttachmentID = attachment.getAttachmentId(); - String keyPackets = attachment.getKeyPackets(); - byte[] keyPackage = Base64.decode(keyPackets, Base64.DEFAULT); - byte[] sessionKey = oldCrypto.decryptKeyPacket(keyPackage); - byte[] newKeyPackage = oldCrypto.encryptKeyPacket(sessionKey, newPublicKey); - String newKeyPackets = Base64.encodeToString(newKeyPackage, Base64.NO_WRAP); - if (!TextUtils.isEmpty(keyPackets)) { - draftBody.getAttachmentKeyPackets().put(AttachmentID, newKeyPackets); - } - } - } - } else { - for (Attachment attachment : attachmentList) { - if (mActionType == Constants.MessageActionType.FORWARD || - ((mActionType == Constants.MessageActionType.REPLY || mActionType == Constants.MessageActionType.REPLY_ALL) && attachment.getInline())) { - String AttachmentID = attachment.getAttachmentId(); - draftBody.getAttachmentKeyPackets().put(AttachmentID, attachment.getKeyPackets()); - } - } - } - } - - /** - * @deprecated replace with UploadAttachments use case - */ - private static class PostCreateDraftAttachmentsJob extends ProtonMailBaseJob { - private final String mMessageId; - private final String mOldMessageId; - private final boolean mUploadAttachments; - private final List mAttachments; - private final AddressCrypto mCrypto; - private final String mUsername; - - PostCreateDraftAttachmentsJob(String messageId, String oldMessageId, boolean uploadAttachments, List attachments, AddressCrypto crypto, String username) { - super(new Params(Priority.MEDIUM).requireNetwork().persist().groupBy(Constants.JOB_GROUP_MESSAGE)); - mMessageId = messageId; - mOldMessageId = oldMessageId; - mUploadAttachments = uploadAttachments; - mAttachments = attachments; - mCrypto = crypto; - mUsername = username; - } - - @Override - public void onRun() throws Throwable { - PendingActionsDatabase pendingActionsDatabase = PendingActionsDatabaseFactory.Companion.getInstance(getApplicationContext()).getDatabase(); - Message message = getMessageDetailsRepository().findMessageByIdBlocking(mMessageId); - User user = getUserManager().getUser(mUsername); - if (user == null) { - pendingActionsDatabase.deletePendingUploadByMessageId(mMessageId, mOldMessageId); - return; - } - if (message != null && mUploadAttachments && (mAttachments != null && mAttachments.size() > 0)) { - //upload all attachments - List messageAttachments = message.getAttachments(); - if (messageAttachments != null && mAttachments != null && mAttachments.size() > messageAttachments.size()) { - messageAttachments = mAttachments; - } - for (Attachment attachment : messageAttachments) { - try { - String filePath = attachment.getFilePath(); - if (TextUtils.isEmpty(filePath)) { - // TODO: inform user that the attachment is not saved properly - continue; - } - final File file = new File(filePath); - if (!URLUtil.isDataUrl(filePath) && !file.exists()) { - continue; - } - if (attachment.isUploaded()) { - continue; - } - attachment.uploadAndSave(getMessageDetailsRepository(), getApi(), mCrypto); - } catch (Exception e) { - Logger.doLogException(TAG_CREATE_AND_POST_DRAFT_JOB, "error while attaching file: " + attachment.getFilePath(), e); - AppUtil.postEventOnUi(new AttachmentFailedEvent(message.getMessageId(), message.getSubject(), attachment.getFileName())); - } - } - } - message.setNumAttachments(mAttachments.size()); - PendingSend pendingForSending = pendingActionsDatabase.findPendingSendByDbId(message.getDbId()); - - if (pendingForSending == null) { - getMessageDetailsRepository().saveMessageInDB(message); - } - getJobManager().addJob(new FetchMessageDetailJob(message.getMessageId())); - pendingActionsDatabase.deletePendingUploadByMessageId(mMessageId, mOldMessageId); - DraftCreatedEvent draftCreatedEvent = new DraftCreatedEvent(message.getMessageId(), mOldMessageId, message); - AppUtil.postEventOnUi(draftCreatedEvent); - } - - } - - @Override - protected RetryConstraint shouldReRunOnThrowable(@NonNull Throwable throwable, int runCount, int maxRunCount) { - return RetryConstraint.createExponentialBackoff(runCount, 500); - } -} diff --git a/app/src/main/java/ch/protonmail/android/settings/pin/ValidatePinActivity.kt b/app/src/main/java/ch/protonmail/android/settings/pin/ValidatePinActivity.kt index 209aca7c3..f22afd2d4 100644 --- a/app/src/main/java/ch/protonmail/android/settings/pin/ValidatePinActivity.kt +++ b/app/src/main/java/ch/protonmail/android/settings/pin/ValidatePinActivity.kt @@ -28,7 +28,6 @@ import ch.protonmail.android.R import ch.protonmail.android.activities.BaseActivity import ch.protonmail.android.core.Constants import ch.protonmail.android.core.ProtonMailApplication -import ch.protonmail.android.events.DraftCreatedEvent import ch.protonmail.android.events.FetchDraftDetailEvent import ch.protonmail.android.events.FetchMessageDetailEvent import ch.protonmail.android.events.LogoutEvent @@ -52,7 +51,6 @@ const val EXTRA_ATTACHMENT_IMPORT_EVENT = "extra_attachment_import_event" const val EXTRA_TOTAL_COUNT_EVENT = "extra_total_count_event" const val EXTRA_MESSAGE_DETAIL_EVENT = "extra_message_details_event" const val EXTRA_DRAFT_DETAILS_EVENT = "extra_draft_details_event" -const val EXTRA_DRAFT_CREATED_EVENT = "extra_draft_created_event" // endregion /* @@ -68,7 +66,6 @@ class ValidatePinActivity : BaseActivity(), private var messageCountsEvent: MessageCountsEvent? = null private var messageDetailEvent: FetchMessageDetailEvent? = null private var draftDetailEvent: FetchDraftDetailEvent? = null - private var draftCreatedEvent: DraftCreatedEvent? = null private lateinit var biometricPrompt: BiometricPrompt private lateinit var promptInfo: BiometricPrompt.PromptInfo @@ -125,11 +122,6 @@ class ValidatePinActivity : BaseActivity(), fun onFetchDraftDetailEvent(event: FetchDraftDetailEvent) { draftDetailEvent = event } - - @Subscribe - fun onDraftCreatedEvent(event: DraftCreatedEvent) { - draftCreatedEvent = event - } // endregion override fun onPinCreated(pin: String) { @@ -242,9 +234,6 @@ class ValidatePinActivity : BaseActivity(), if (messageDetailEvent != null) { putExtra(EXTRA_MESSAGE_DETAIL_EVENT, messageDetailEvent) } - if (draftCreatedEvent != null) { - putExtra(EXTRA_DRAFT_CREATED_EVENT, draftCreatedEvent) - } } } From 308d88dee51d2f2ea049748690c93981f2c8f1e9 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Fri, 18 Dec 2020 12:16:29 +0100 Subject: [PATCH 027/145] ComposeMessageViewModel show error when creating draft fails - Do not listen for AttachmentFailedEvent in VM (as we're now handling error) and in some other places where it didn't really make sense * AttachmentFailedEvent will be deleted when migrating UpdateAndPostDraftJob MAILAND-1018 --- .../ComposeMessageActivity.java | 8 ++----- .../activities/mailbox/MailboxActivity.kt | 2 +- .../messageDetails/MessageDetailsActivity.kt | 2 +- .../settings/BaseSettingsActivity.kt | 7 ------ .../compose/ComposeMessageViewModel.kt | 24 ++++++++++++++++--- .../android/contacts/ContactsActivity.kt | 7 ------ .../android/events/AttachmentFailedEvent.java | 11 +-------- .../android/jobs/UpdateAndPostDraftJob.java | 2 +- .../android/usecase/compose/SaveDraft.kt | 2 +- app/src/main/res/values/strings.xml | 3 +-- 10 files changed, 29 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java b/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java index 037e796f1..1d2a94835 100644 --- a/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java +++ b/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java @@ -133,7 +133,6 @@ import ch.protonmail.android.crypto.AddressCrypto; import ch.protonmail.android.crypto.CipherText; import ch.protonmail.android.crypto.Crypto; -import ch.protonmail.android.events.AttachmentFailedEvent; import ch.protonmail.android.events.ContactEvent; import ch.protonmail.android.events.DownloadEmbeddedImagesEvent; import ch.protonmail.android.events.FetchDraftDetailEvent; @@ -465,6 +464,8 @@ private void observeSetup() { composeMessageViewModel.getDeleteResult().observe(ComposeMessageActivity.this, new CheckLocalMessageObserver()); composeMessageViewModel.getOpenAttachmentsScreenResult().observe(ComposeMessageActivity.this, new AddAttachmentsObserver()); + composeMessageViewModel.getSavingDraftError().observe(this, errorMessage -> + Toast.makeText(this, errorMessage, Toast.LENGTH_SHORT).show()); composeMessageViewModel.getSavingDraftComplete().observe(this, event -> { if (mUpdateDraftPmMeChanged) { composeMessageViewModel.setBeforeSaveDraft(true, mComposeBodyEditText.getText().toString()); @@ -976,11 +977,6 @@ public void onLogoutEvent(LogoutEvent event) { finishActivity(); } - @Subscribe - public void onAttachmentFailedEvent(AttachmentFailedEvent event) { - TextExtensions.showToast(this, getString(R.string.attachment_failed) + " " + event.getMessageSubject() + " " + event.getAttachmentName(), Toast.LENGTH_SHORT); - } - @Subscribe public void onPostImportAttachmentEvent(PostImportAttachmentEvent event) { if (event == null || event.composerInstanceId == null || !event.composerInstanceId.equals(composerInstanceId)) { diff --git a/app/src/main/java/ch/protonmail/android/activities/mailbox/MailboxActivity.kt b/app/src/main/java/ch/protonmail/android/activities/mailbox/MailboxActivity.kt index 495d93688..467c66552 100644 --- a/app/src/main/java/ch/protonmail/android/activities/mailbox/MailboxActivity.kt +++ b/app/src/main/java/ch/protonmail/android/activities/mailbox/MailboxActivity.kt @@ -1055,7 +1055,7 @@ class MailboxActivity : @Subscribe fun onAttachmentFailedEvent(event: AttachmentFailedEvent) { showToast( - "${getString(R.string.attachment_failed)} ${event.messageSubject} ${event.attachmentName}", + "${getString(R.string.attachment_failed)} ${event.messageSubject}", Toast.LENGTH_SHORT ) } diff --git a/app/src/main/java/ch/protonmail/android/activities/messageDetails/MessageDetailsActivity.kt b/app/src/main/java/ch/protonmail/android/activities/messageDetails/MessageDetailsActivity.kt index 1a0a89568..7e0f1f768 100644 --- a/app/src/main/java/ch/protonmail/android/activities/messageDetails/MessageDetailsActivity.kt +++ b/app/src/main/java/ch/protonmail/android/activities/messageDetails/MessageDetailsActivity.kt @@ -713,7 +713,7 @@ internal class MessageDetailsActivity : @Suppress("unused") fun onAttachmentFailedEvent(event: AttachmentFailedEvent) { showToast( - "${getString(R.string.attachment_failed)} ${event.messageSubject} ${event.attachmentName}", + "${getString(R.string.attachment_failed)} ${event.messageSubject}", Toast.LENGTH_SHORT ) } diff --git a/app/src/main/java/ch/protonmail/android/activities/settings/BaseSettingsActivity.kt b/app/src/main/java/ch/protonmail/android/activities/settings/BaseSettingsActivity.kt index 220467989..7b86143fb 100644 --- a/app/src/main/java/ch/protonmail/android/activities/settings/BaseSettingsActivity.kt +++ b/app/src/main/java/ch/protonmail/android/activities/settings/BaseSettingsActivity.kt @@ -30,7 +30,6 @@ import android.provider.Settings.EXTRA_CHANNEL_ID import android.view.Gravity import android.view.MenuItem import android.view.View -import android.widget.Toast import androidx.activity.viewModels import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.Toolbar @@ -71,7 +70,6 @@ import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDataba import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDatabaseFactory import ch.protonmail.android.core.Constants import ch.protonmail.android.core.ProtonMailApplication -import ch.protonmail.android.events.AttachmentFailedEvent import ch.protonmail.android.events.FetchLabelsEvent import ch.protonmail.android.events.user.MailSettingsEvent import ch.protonmail.android.jobs.FetchByLocationJob @@ -498,11 +496,6 @@ abstract class BaseSettingsActivity : BaseConnectivityActivity() { loadMailSettings() } - @Subscribe - fun onAttachmentFailedEvent(event: AttachmentFailedEvent) { - showToast(getString(R.string.attachment_failed) + " " + event.messageSubject + " " + event.attachmentName, Toast.LENGTH_SHORT) - } - open fun onLabelsLoadedEvent(event: FetchLabelsEvent) { if (!canClick.get()) { showToast(R.string.cache_cleared, gravity = Gravity.CENTER) diff --git a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt index b197fd07f..710ea96aa 100644 --- a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt +++ b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt @@ -104,6 +104,7 @@ class ComposeMessageViewModel @Inject constructor( private val _closeComposer: MutableLiveData> = MutableLiveData() private var _setupCompleteValue = false private val _savingDraftComplete: MutableLiveData = MutableLiveData() + private val _savingDraftError: MutableLiveData = MutableLiveData() private val _deleteResult: MutableLiveData> = MutableLiveData() private val _loadingDraftResult: MutableLiveData = MutableLiveData() private val _messageResultError: MutableLiveData> = MutableLiveData() @@ -161,6 +162,8 @@ class ComposeMessageViewModel @Inject constructor( get() = _setupCompleteValue val savingDraftComplete: LiveData get() = _savingDraftComplete + val savingDraftError: LiveData + get() = _savingDraftError val senderAddresses: List get() = _senderAddresses val deleteResult: LiveData> @@ -453,9 +456,19 @@ class ComposeMessageViewModel @Inject constructor( ).collect { when (it) { is SaveDraft.Result.Success -> onDraftSaved(it.draftId) - SaveDraft.Result.SendingInProgressError -> TODO() - SaveDraft.Result.OnlineDraftCreationFailed -> TODO() - SaveDraft.Result.UploadDraftAttachmentsFailed -> TODO() + SaveDraft.Result.OnlineDraftCreationFailed -> { + val errorMessage = getStringResource( + R.string.failed_saving_draft_online + ).format(message.subject) + _savingDraftError.postValue(errorMessage) + } + SaveDraft.Result.UploadDraftAttachmentsFailed -> { + val attachmentFailed = R.string.attachment_failed + val errorMessage = getStringResource(attachmentFailed) + message.subject + _savingDraftError.postValue(errorMessage) + } + SaveDraft.Result.SendingInProgressError -> { + } } } @@ -468,6 +481,11 @@ class ComposeMessageViewModel @Inject constructor( } } + private fun getStringResource(stringId: Int): String { + val context = ProtonMailApplication.getApplication().applicationContext + return context.getString(stringId) + } + private suspend fun onDraftSaved(savedDraftId: String) { val draft = requireNotNull(messageDetailsRepository.findMessageById(savedDraftId)) diff --git a/app/src/main/java/ch/protonmail/android/contacts/ContactsActivity.kt b/app/src/main/java/ch/protonmail/android/contacts/ContactsActivity.kt index 3382dc00b..997ba9bd7 100644 --- a/app/src/main/java/ch/protonmail/android/contacts/ContactsActivity.kt +++ b/app/src/main/java/ch/protonmail/android/contacts/ContactsActivity.kt @@ -45,7 +45,6 @@ import ch.protonmail.android.contacts.list.search.OnSearchClose import ch.protonmail.android.contacts.list.search.SearchExpandListener import ch.protonmail.android.contacts.list.search.SearchViewQueryListener import ch.protonmail.android.core.Constants -import ch.protonmail.android.events.AttachmentFailedEvent import ch.protonmail.android.events.LogoutEvent import ch.protonmail.android.events.user.MailSettingsEvent import ch.protonmail.android.permissions.PermissionHelper @@ -252,12 +251,6 @@ class ContactsActivity : moveToLogin() } - @Subscribe - @Suppress("unused") - fun onAttachmentFailedEvent(event: AttachmentFailedEvent) { - showToast(getString(R.string.attachment_failed, event.messageSubject, event.attachmentName)) - } - private fun onContactsFetchedEvent(isSuccessful: Boolean) { Timber.v("onContactsFetchedEvent isSuccessful:$isSuccessful") progressLayoutView?.isVisible = false diff --git a/app/src/main/java/ch/protonmail/android/events/AttachmentFailedEvent.java b/app/src/main/java/ch/protonmail/android/events/AttachmentFailedEvent.java index 2d0bf058e..9873cdc7e 100644 --- a/app/src/main/java/ch/protonmail/android/events/AttachmentFailedEvent.java +++ b/app/src/main/java/ch/protonmail/android/events/AttachmentFailedEvent.java @@ -18,17 +18,12 @@ */ package ch.protonmail.android.events; -/** - * Created by sunny on 8/12/15. - */ public class AttachmentFailedEvent { private final String messageId; private final String messageSubject; - private final String attachmentName; - public AttachmentFailedEvent(String messageId, String messageSubject, String attachmentName){ + public AttachmentFailedEvent(String messageId, String messageSubject) { this.messageId = messageId; - this.attachmentName = attachmentName; this.messageSubject = messageSubject; } @@ -39,8 +34,4 @@ public String getMessageId(){ public String getMessageSubject() { return messageSubject; } - - public String getAttachmentName() { - return attachmentName; - } } diff --git a/app/src/main/java/ch/protonmail/android/jobs/UpdateAndPostDraftJob.java b/app/src/main/java/ch/protonmail/android/jobs/UpdateAndPostDraftJob.java index e62458f7f..7172fb7e2 100644 --- a/app/src/main/java/ch/protonmail/android/jobs/UpdateAndPostDraftJob.java +++ b/app/src/main/java/ch/protonmail/android/jobs/UpdateAndPostDraftJob.java @@ -234,7 +234,7 @@ private ArrayList updateDraft(PendingActionsDatabase pendingActionsD } catch (Exception e) { Logger.doLogException(TAG_UPDATE_AND_POST_DRAFT_JOB, "error while attaching file: " + attachment.getFilePath(), e); AppUtil.postEventOnUi(new AttachmentFailedEvent(message.getMessageId(), - message.getSubject(), attachment.getFileName())); + message.getSubject())); } } pendingActionsDatabase.deletePendingUploadByMessageId(mMessageId); diff --git a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt index ae2a1cd67..c4cfd96e8 100644 --- a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt +++ b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt @@ -118,7 +118,7 @@ class SaveDraft @Inject constructor( updatePendingForSendMessage(createdDraftId, localDraftId) deleteOfflineDraft(localDraftId) - messageDetailsRepository.findMessageByIdBlocking(createdDraftId.orEmpty())?.let { + messageDetailsRepository.findMessageByIdBlocking(createdDraftId)?.let { val uploadResult = uploadAttachments(params.newAttachmentIds, it, addressCrypto) if (uploadResult is UploadAttachments.Result.Failure) { return@withContext Result.UploadDraftAttachmentsFailed diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f3b6385d0..63a09bd38 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1117,6 +1117,5 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Invalid Firebase API key. Push notifications will not work. - - + Failed saving online draft for message %s From 0c782da1ad0a2077b1c2494b1796d0f0ddfed86a Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Fri, 18 Dec 2020 12:51:01 +0100 Subject: [PATCH 028/145] Delete PendingDraft object and all usages as it was never read PendingDraft was inserted and deleted in multiple places, but never read - Bump PendingActionsDatabase version to 4 MAILAND-1018 --- .../ComposeMessageActivity.java | 7 +-- .../repository/MessageDetailsRepository.kt | 11 ---- .../pendingActions/PendingActionsDatabase.kt | 9 --- .../PendingActionsDatabaseFactory.kt | 2 +- .../room/pendingActions/PendingDraft.kt | 34 ----------- .../api/services/PostMessageServiceFactory.kt | 8 --- .../compose/ComposeMessageViewModel.kt | 58 +++++-------------- .../android/jobs/UpdateAndPostDraftJob.java | 4 -- .../android/usecase/compose/SaveDraft.kt | 6 +- .../android/usecase/compose/SaveDraftTest.kt | 26 --------- 10 files changed, 18 insertions(+), 147 deletions(-) delete mode 100644 app/src/main/java/ch/protonmail/android/api/models/room/pendingActions/PendingDraft.kt diff --git a/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java b/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java index 1d2a94835..84b49dccb 100644 --- a/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java +++ b/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java @@ -1005,7 +1005,6 @@ protected void onStart() { if (askForPermission) { contactsPermissionHelper.checkPermission(); } - composeMessageViewModel.insertPendingDraft(); } @Override @@ -1056,7 +1055,6 @@ private void onStoragePermissionGranted() { @Override protected void onStop() { super.onStop(); - composeMessageViewModel.removePendingDraft(); askForPermission = true; ProtonMailApplication.getApplication().getBus().unregister(this); ProtonMailApplication.getApplication().getBus().unregister(composeMessageViewModel); @@ -1109,10 +1107,7 @@ private void showDraftDialog() { getString(R.string.yes), getString(R.string.cancel), unit -> { - String draftId = composeMessageViewModel.getDraftId(); - if (!TextUtils.isEmpty(draftId)) { - composeMessageViewModel.deleteDraft(); - } + composeMessageViewModel.deleteDraft(); mComposeBodyEditText.setIsDirty(false); finishActivity(); return unit; diff --git a/app/src/main/java/ch/protonmail/android/activities/messageDetails/repository/MessageDetailsRepository.kt b/app/src/main/java/ch/protonmail/android/activities/messageDetails/repository/MessageDetailsRepository.kt index b0bf60e53..c038c49f1 100644 --- a/app/src/main/java/ch/protonmail/android/activities/messageDetails/repository/MessageDetailsRepository.kt +++ b/app/src/main/java/ch/protonmail/android/activities/messageDetails/repository/MessageDetailsRepository.kt @@ -34,7 +34,6 @@ import ch.protonmail.android.api.models.room.messages.LocalAttachment import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.api.models.room.messages.MessagesDao import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDao -import ch.protonmail.android.api.models.room.pendingActions.PendingDraft import ch.protonmail.android.api.models.room.pendingActions.PendingSend import ch.protonmail.android.api.models.room.pendingActions.PendingUpload import ch.protonmail.android.attachments.DownloadEmbeddedAttachmentsWorker @@ -463,16 +462,6 @@ class MessageDetailsRepository @Inject constructor( jobManager.addJobInBackground(PostReadJob(listOf(messageId))) } - /** - * TODO this have nothing to do with MessageDetails, extract to PendingActionsRepository - */ - suspend fun insertPendingDraft(messageDbId: Long) = - withContext(dispatchers.Io) { - pendingActionsDatabase.insertPendingDraft(PendingDraft(messageDbId)) - } - - fun deletePendingDraft(messageDbId: Long) = pendingActionsDatabase.deletePendingDraftById(messageDbId) - fun findAllPendingSendsAsync(): LiveData> { return pendingActionsDatabase.findAllPendingSendsAsync() } diff --git a/app/src/main/java/ch/protonmail/android/api/models/room/pendingActions/PendingActionsDatabase.kt b/app/src/main/java/ch/protonmail/android/api/models/room/pendingActions/PendingActionsDatabase.kt index b5e53e88f..dbe0a1b14 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/room/pendingActions/PendingActionsDatabase.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/room/pendingActions/PendingActionsDatabase.kt @@ -73,13 +73,4 @@ abstract class PendingActionsDatabase { @Query("DELETE FROM $TABLE_PENDING_UPLOADS") abstract fun clearPendingUploadCache() - - @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun insertPendingDraft(pendingDraft: PendingDraft) - - @Query("DELETE FROM $TABLE_PENDING_DRAFT WHERE ${COLUMN_PENDING_DRAFT_MESSAGE_ID}=:messageDbId") - abstract fun deletePendingDraftById(messageDbId: Long) - - @Query("SELECT * FROM $TABLE_PENDING_DRAFT WHERE ${COLUMN_PENDING_DRAFT_MESSAGE_ID}=:messageDbId") - abstract fun findPendingDraftByDbId(messageDbId: Long): PendingDraft? } diff --git a/app/src/main/java/ch/protonmail/android/api/models/room/pendingActions/PendingActionsDatabaseFactory.kt b/app/src/main/java/ch/protonmail/android/api/models/room/pendingActions/PendingActionsDatabaseFactory.kt index 5645337a2..140171351 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/room/pendingActions/PendingActionsDatabaseFactory.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/room/pendingActions/PendingActionsDatabaseFactory.kt @@ -25,7 +25,7 @@ import androidx.room.Room import androidx.room.RoomDatabase import ch.protonmail.android.core.ProtonMailApplication -@Database(entities = [PendingSend::class, PendingUpload::class, PendingDraft::class], version = 3) +@Database(entities = [PendingSend::class, PendingUpload::class], version = 4) abstract class PendingActionsDatabaseFactory : RoomDatabase() { abstract fun getDatabase(): PendingActionsDatabase diff --git a/app/src/main/java/ch/protonmail/android/api/models/room/pendingActions/PendingDraft.kt b/app/src/main/java/ch/protonmail/android/api/models/room/pendingActions/PendingDraft.kt deleted file mode 100644 index b85e201ba..000000000 --- a/app/src/main/java/ch/protonmail/android/api/models/room/pendingActions/PendingDraft.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2020 Proton Technologies AG - * - * This file is part of ProtonMail. - * - * ProtonMail is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ProtonMail is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ProtonMail. If not, see https://www.gnu.org/licenses/. - */ -package ch.protonmail.android.api.models.room.pendingActions - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey - -// region constants -const val TABLE_PENDING_DRAFT = "pending_draft" -const val COLUMN_PENDING_DRAFT_MESSAGE_ID = "message_db_id" -// endregion - -@Entity(tableName = TABLE_PENDING_DRAFT) -data class PendingDraft( - @PrimaryKey - @ColumnInfo(name = COLUMN_PENDING_DRAFT_MESSAGE_ID) - var messageDbId: Long) diff --git a/app/src/main/java/ch/protonmail/android/api/services/PostMessageServiceFactory.kt b/app/src/main/java/ch/protonmail/android/api/services/PostMessageServiceFactory.kt index deb27ec2d..e9cd15102 100644 --- a/app/src/main/java/ch/protonmail/android/api/services/PostMessageServiceFactory.kt +++ b/app/src/main/java/ch/protonmail/android/api/services/PostMessageServiceFactory.kt @@ -23,7 +23,6 @@ import ch.protonmail.android.activities.messageDetails.repository.MessageDetails import ch.protonmail.android.api.models.SendPreference import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDatabaseFactory -import ch.protonmail.android.api.models.room.pendingActions.PendingDraft import ch.protonmail.android.api.models.room.pendingActions.PendingSend import ch.protonmail.android.api.models.room.pendingActions.PendingUpload import ch.protonmail.android.core.Constants @@ -57,7 +56,6 @@ class PostMessageServiceFactory @Inject constructor( // this is temp fix GlobalScope.launch { val message = handleMessage(messageId, content, username) ?: return@launch - insertPendingDraft(ProtonMailApplication.getApplication(), messageId) handleUpdateDraft(message, uploadAttachments, newAttachments, ProtonMailApplication.getApplication()) jobManager.addJobInBackground(UpdateAndPostDraftJob(messageId, newAttachments, uploadAttachments, oldSenderId, username)) } @@ -142,10 +140,4 @@ class PostMessageServiceFactory @Inject constructor( pendingActionsDatabase.insertPendingForSend(pendingForSending) } } - - private suspend fun insertPendingDraft(context: Context, messageDbId: Long) = - withContext(bgDispatcher) { - val pendingActionsDatabase = PendingActionsDatabaseFactory.getInstance(context).getDatabase() - pendingActionsDatabase.insertPendingDraft(PendingDraft(messageDbId)) - } } diff --git a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt index 710ea96aa..315274a11 100644 --- a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt +++ b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt @@ -377,24 +377,6 @@ class ComposeMessageViewModel @Inject constructor( } } - fun removePendingDraft() { - viewModelScope.launch { - _dbId?.let { - removePendingDraft(it) - } - } - } - - fun insertPendingDraft() { - viewModelScope.launch { - _dbId?.let { - withContext(IO) { - messageDetailsRepository.insertPendingDraft(it) - } - } - } - } - fun saveDraft(message: Message, hasConnectivity: Boolean) { val uploadAttachments = _messageDataResult.uploadAttachments @@ -516,11 +498,6 @@ class ComposeMessageViewModel @Inject constructor( return newAttachments } - private suspend fun removePendingDraft(messageDbId: Long) = - withContext(IO) { - messageDetailsRepository.deletePendingDraft(messageDbId) - } - private suspend fun saveMessage(message: Message): Long = withContext(dispatchers.Io) { messageDetailsRepository.saveMessageInDB(message) @@ -697,8 +674,9 @@ class ComposeMessageViewModel @Inject constructor( fun deleteDraft() { viewModelScope.launch { - deleteMessage(listOf(_draftId.get())) - removePendingDraft() + if (_draftId.get().isNotEmpty()) { + deleteMessage(listOf(_draftId.get())) + } } } @@ -722,9 +700,8 @@ class ComposeMessageViewModel @Inject constructor( message.messageId = UUID.randomUUID().toString() _dbId = saveMessage(message) } else { - // this will ensure the message get latest message id if it was already saved in a create/update - // draft job and also that the message has all the latest edits in between draft saving (creation) - // and sending the message + // this will ensure the message get latest message id if it was already saved in a create/update draft job + // and also that the message has all the latest edits in between draft saving (creation) and sending the message val savedMessage = messageDetailsRepository.findMessageByMessageDbId(_dbId!!, IO) message.dbId = _dbId savedMessage?.let { @@ -738,9 +715,6 @@ class ComposeMessageViewModel @Inject constructor( } if (_dbId != null) { - messageDetailsRepository.deletePendingDraft(message.dbId!!) - - val newAttachments = calculateNewAttachments(true) postMessageServiceFactory.startSendingMessage( _dbId!!, @@ -780,21 +754,19 @@ class ComposeMessageViewModel @Inject constructor( signatureBuilder.append(NEW_LINE) signatureBuilder.append(NEW_LINE) signatureBuilder.append(NEW_LINE) - if (user != null) { - signature = if (!TextUtils.isEmpty(_messageDataResult.addressId)) { - user.getSignatureForAddress(_messageDataResult.addressId) + signature = if (!TextUtils.isEmpty(_messageDataResult.addressId)) { + user.getSignatureForAddress(_messageDataResult.addressId) + } else { + val senderAddresses = user.senderEmailAddresses + if (senderAddresses.isNotEmpty()) { + val selectedEmail = senderAddresses[0] + user.getSignatureForAddress(user.getSenderAddressIdByEmail(selectedEmail)) } else { - val senderAddresses = user.senderEmailAddresses - if (senderAddresses.isNotEmpty()) { - val selectedEmail = senderAddresses[0] - user.getSignatureForAddress(user.getSenderAddressIdByEmail(selectedEmail)) - } else { - val selectedEmail = user.defaultEmail - user.getSignatureForAddress(user.getSenderAddressIdByEmail(selectedEmail)) - } + val selectedEmail = user.defaultEmail + user.getSignatureForAddress(user.getSenderAddressIdByEmail(selectedEmail)) } - mobileSignature = user.mobileSignature } + mobileSignature = user.mobileSignature _messageDataResult = MessageBuilderData.Builder() .fromOld(_messageDataResult) diff --git a/app/src/main/java/ch/protonmail/android/jobs/UpdateAndPostDraftJob.java b/app/src/main/java/ch/protonmail/android/jobs/UpdateAndPostDraftJob.java index 7172fb7e2..85dc5efeb 100644 --- a/app/src/main/java/ch/protonmail/android/jobs/UpdateAndPostDraftJob.java +++ b/app/src/main/java/ch/protonmail/android/jobs/UpdateAndPostDraftJob.java @@ -90,9 +90,6 @@ protected RetryConstraint shouldReRunOnThrowable(@NonNull Throwable throwable, i PendingActionsDatabase pendingActionsDatabase = PendingActionsDatabaseFactory.Companion.getInstance( getApplicationContext(), mUsername).getDatabase(); - if (mUploadAttachments && (mNewAttachments != null && mNewAttachments.size() > 0)) { - pendingActionsDatabase.deletePendingDraftById(mMessageDbId); - } return RetryConstraint.CANCEL; } @@ -146,7 +143,6 @@ protected void onProtonCancel(int cancelReason, @Nullable Throwable throwable) { } PendingActionsDatabase actionsDatabase = PendingActionsDatabaseFactory.Companion.getInstance(getApplicationContext(), mUsername).getDatabase(); actionsDatabase.deletePendingUploadByMessageId(message.getMessageId()); - actionsDatabase.deletePendingDraftById(mMessageDbId); } private void saveMessage(Message message, PendingActionsDatabase pendingActionsDatabase) { diff --git a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt index c4cfd96e8..c7381d59e 100644 --- a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt +++ b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt @@ -75,21 +75,17 @@ class SaveDraft @Inject constructor( ) ) - val messageDbId = messageDetailsRepository.saveMessageLocally(message) - // TODO this is likely uneeded as we never check pending drafts anywhere - messageDetailsRepository.insertPendingDraft(messageDbId) - if (params.newAttachmentIds.isNotEmpty()) { pendingActionsDao.insertPendingForUpload(PendingUpload(messageId)) } + val messageDbId = messageDetailsRepository.saveMessageLocally(message) pendingActionsDao.findPendingSendByDbId(messageDbId)?.let { // TODO allow draft to be saved in this case when starting to use SaveDraft use case in PostMessageJob too return@withContext flowOf(Result.SendingInProgressError) } return@withContext saveDraftOnline(message, params, messageId, addressCrypto) - } private fun saveDraftOnline( diff --git a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt index 49d595d87..289b8c57e 100644 --- a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt +++ b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt @@ -119,32 +119,6 @@ class SaveDraftTest : CoroutinesTest { } } - @Test - fun saveDraftInsertsPendingDraftInPendingActionsDatabase() { - runBlockingTest { - // Given - val message = Message().apply { - dbId = 123L - this.messageId = "456" - addressID = "addressId" - decryptedBody = "Message body in plain text" - } - val addressCrypto = mockk { - every { encrypt("Message body in plain text", true).armored } returns "encrypted armored content" - } - every { addressCryptoFactory.create(Id("addressId"), Name(currentUsername)) } returns addressCrypto - coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 123L - - // When - saveDraft( - SaveDraftParameters(message, emptyList(), null, FORWARD, "previousSenderId1273") - ) - - // Then - coVerify { messageDetailsRepository.insertPendingDraft(123L) } - } - } - @Test fun saveDraftsInsertsPendingUploadWhenThereAreNewAttachments() { runBlockingTest { From 82f63f9392309bb510feea09c6a5077c27427e4a Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Fri, 18 Dec 2020 16:29:18 +0100 Subject: [PATCH 029/145] Use non-blocking findMessageById in SaveDraft use case MAILAND-1018 Extract Result classes to separated files and improve empty checks by using kotlin over TextUtils MAILAND-1018 Use flowOn operator instead of withContext in saveDraftOnline MAILAND-1018 Rename worker's input / output data to avoid lenght warnings MAILAND-1018 --- .../api/models/room/messages/Message.kt | 3 +- .../compose/ComposeMessageViewModel.kt | 11 +-- .../android/usecase/compose/SaveDraft.kt | 82 +++++++++--------- .../usecase/compose/SaveDraftResult.kt | 27 ++++++ .../worker/{ => drafts}/CreateDraftWorker.kt | 86 ++++++++----------- .../worker/drafts/CreateDraftWorkerErrors.kt | 25 ++++++ .../compose/ComposeMessageViewModelTest.kt | 7 +- .../android/usecase/compose/SaveDraftTest.kt | 46 +++++----- .../android/worker/CreateDraftWorkerTest.kt | 37 +++++--- 9 files changed, 184 insertions(+), 140 deletions(-) create mode 100644 app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraftResult.kt rename app/src/main/java/ch/protonmail/android/worker/{ => drafts}/CreateDraftWorker.kt (75%) create mode 100644 app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorkerErrors.kt diff --git a/app/src/main/java/ch/protonmail/android/api/models/room/messages/Message.kt b/app/src/main/java/ch/protonmail/android/api/models/room/messages/Message.kt index 6fe70d7fa..5ad17a092 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/room/messages/Message.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/room/messages/Message.kt @@ -19,7 +19,6 @@ package ch.protonmail.android.api.models.room.messages import android.provider.BaseColumns -import android.text.TextUtils import androidx.annotation.MainThread import androidx.annotation.WorkerThread import androidx.lifecycle.LiveData @@ -220,7 +219,7 @@ data class Message @JvmOverloads constructor( get() { return replyTos .asSequence() - .filterNot { TextUtils.isEmpty(it.address) } + .filter { it.address.isNotEmpty() } .map { it.address } .toList() } diff --git a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt index 315274a11..4aa1a7dc4 100644 --- a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt +++ b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt @@ -53,6 +53,7 @@ import ch.protonmail.android.events.Status import ch.protonmail.android.jobs.contacts.GetSendPreferenceJob import ch.protonmail.android.usecase.VerifyConnection import ch.protonmail.android.usecase.compose.SaveDraft +import ch.protonmail.android.usecase.compose.SaveDraftResult import ch.protonmail.android.usecase.delete.DeleteMessage import ch.protonmail.android.usecase.fetch.FetchPublicKeys import ch.protonmail.android.usecase.model.FetchPublicKeysRequest @@ -437,19 +438,19 @@ class ComposeMessageViewModel @Inject constructor( ) ).collect { when (it) { - is SaveDraft.Result.Success -> onDraftSaved(it.draftId) - SaveDraft.Result.OnlineDraftCreationFailed -> { + is SaveDraftResult.Success -> onDraftSaved(it.draftId) + SaveDraftResult.OnlineDraftCreationFailed -> { val errorMessage = getStringResource( R.string.failed_saving_draft_online ).format(message.subject) _savingDraftError.postValue(errorMessage) } - SaveDraft.Result.UploadDraftAttachmentsFailed -> { + SaveDraftResult.UploadDraftAttachmentsFailed -> { val attachmentFailed = R.string.attachment_failed val errorMessage = getStringResource(attachmentFailed) + message.subject _savingDraftError.postValue(errorMessage) } - SaveDraft.Result.SendingInProgressError -> { + SaveDraftResult.SendingInProgressError -> { } } } @@ -754,7 +755,7 @@ class ComposeMessageViewModel @Inject constructor( signatureBuilder.append(NEW_LINE) signatureBuilder.append(NEW_LINE) signatureBuilder.append(NEW_LINE) - signature = if (!TextUtils.isEmpty(_messageDataResult.addressId)) { + signature = if (_messageDataResult.addressId.isNotEmpty()) { user.getSignatureForAddress(_messageDataResult.addressId) } else { val senderAddresses = user.senderEmailAddresses diff --git a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt index c7381d59e..cced05af8 100644 --- a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt +++ b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt @@ -33,11 +33,12 @@ import ch.protonmail.android.crypto.AddressCrypto import ch.protonmail.android.di.CurrentUsername import ch.protonmail.android.domain.entity.Id import ch.protonmail.android.domain.entity.Name -import ch.protonmail.android.worker.CreateDraftWorker -import ch.protonmail.android.worker.KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID +import ch.protonmail.android.worker.drafts.CreateDraftWorker +import ch.protonmail.android.worker.drafts.KEY_OUTPUT_RESULT_SAVE_DRAFT_MESSAGE_ID import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import me.proton.core.util.kotlin.DispatcherProvider @@ -56,7 +57,7 @@ class SaveDraft @Inject constructor( suspend operator fun invoke( params: SaveDraftParameters - ): Flow = withContext(dispatchers.Io) { + ): Flow = withContext(dispatchers.Io) { Timber.i("Saving Draft for messageId ${params.message.messageId}") val message = params.message @@ -67,13 +68,7 @@ class SaveDraft @Inject constructor( val encryptedBody = addressCrypto.encrypt(message.decryptedBody ?: "", true).armored message.messageBody = encryptedBody - message.setLabelIDs( - listOf( - ALL_DRAFT.messageLocationTypeValue.toString(), - ALL_MAIL.messageLocationTypeValue.toString(), - DRAFT.messageLocationTypeValue.toString() - ) - ) + setMessageAsDraft(message) if (params.newAttachmentIds.isNotEmpty()) { pendingActionsDao.insertPendingForUpload(PendingUpload(messageId)) @@ -81,19 +76,18 @@ class SaveDraft @Inject constructor( val messageDbId = messageDetailsRepository.saveMessageLocally(message) pendingActionsDao.findPendingSendByDbId(messageDbId)?.let { - // TODO allow draft to be saved in this case when starting to use SaveDraft use case in PostMessageJob too - return@withContext flowOf(Result.SendingInProgressError) + return@withContext flowOf(SaveDraftResult.SendingInProgressError) } return@withContext saveDraftOnline(message, params, messageId, addressCrypto) } - private fun saveDraftOnline( + private suspend fun saveDraftOnline( localDraft: Message, params: SaveDraftParameters, localDraftId: String, addressCrypto: AddressCrypto - ): Flow { + ): Flow { return createDraftWorker.enqueue( localDraft, params.parentId, @@ -102,36 +96,35 @@ class SaveDraft @Inject constructor( ) .filter { it.state.isFinished } .map { workInfo -> - withContext(dispatchers.Io) { - if (workInfo.state == WorkInfo.State.SUCCEEDED) { - val createdDraftId = requireNotNull( - workInfo.outputData.getString(KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID) - ) - Timber.d( - "Save Draft to API for messageId $localDraftId succeeded. Created draftId = $createdDraftId" - ) - - updatePendingForSendMessage(createdDraftId, localDraftId) - deleteOfflineDraft(localDraftId) - - messageDetailsRepository.findMessageByIdBlocking(createdDraftId)?.let { - val uploadResult = uploadAttachments(params.newAttachmentIds, it, addressCrypto) - if (uploadResult is UploadAttachments.Result.Failure) { - return@withContext Result.UploadDraftAttachmentsFailed - } - pendingActionsDao.deletePendingUploadByMessageId(localDraftId) - return@withContext Result.Success(createdDraftId) + if (workInfo.state == WorkInfo.State.SUCCEEDED) { + val createdDraftId = requireNotNull( + workInfo.outputData.getString(KEY_OUTPUT_RESULT_SAVE_DRAFT_MESSAGE_ID) + ) + Timber.d( + "Save Draft to API for messageId $localDraftId succeeded. Created draftId = $createdDraftId" + ) + + updatePendingForSendMessage(createdDraftId, localDraftId) + deleteOfflineDraft(localDraftId) + + messageDetailsRepository.findMessageById(createdDraftId)?.let { + val uploadResult = uploadAttachments(params.newAttachmentIds, it, addressCrypto) + if (uploadResult is UploadAttachments.Result.Failure) { + return@map SaveDraftResult.UploadDraftAttachmentsFailed } + pendingActionsDao.deletePendingUploadByMessageId(localDraftId) + return@map SaveDraftResult.Success(createdDraftId) } - - Timber.e("Saving Draft to API for messageId $localDraftId FAILED.") - return@withContext Result.OnlineDraftCreationFailed } + + Timber.e("Saving Draft to API for messageId $localDraftId FAILED.") + return@map SaveDraftResult.OnlineDraftCreationFailed } + .flowOn(dispatchers.Io) } - private fun deleteOfflineDraft(localDraftId: String) { - val offlineDraft = messageDetailsRepository.findMessageByIdBlocking(localDraftId) + private suspend fun deleteOfflineDraft(localDraftId: String) { + val offlineDraft = messageDetailsRepository.findMessageById(localDraftId) offlineDraft?.let { messageDetailsRepository.deleteMessage(offlineDraft) } @@ -145,11 +138,14 @@ class SaveDraft @Inject constructor( } } - sealed class Result { - data class Success(val draftId: String) : Result() - object SendingInProgressError : Result() - object OnlineDraftCreationFailed : Result() - object UploadDraftAttachmentsFailed : Result() + private fun setMessageAsDraft(message: Message) { + message.setLabelIDs( + listOf( + ALL_DRAFT.messageLocationTypeValue.toString(), + ALL_MAIL.messageLocationTypeValue.toString(), + DRAFT.messageLocationTypeValue.toString() + ) + ) } data class SaveDraftParameters( diff --git a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraftResult.kt b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraftResult.kt new file mode 100644 index 000000000..5b34a03be --- /dev/null +++ b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraftResult.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * + * This file is part of ProtonMail. + * + * ProtonMail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonMail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonMail. If not, see https://www.gnu.org/licenses/. + */ + +package ch.protonmail.android.usecase.compose + +sealed class SaveDraftResult { + data class Success(val draftId: String) : SaveDraftResult() + object SendingInProgressError : SaveDraftResult() + object OnlineDraftCreationFailed : SaveDraftResult() + object UploadDraftAttachmentsFailed : SaveDraftResult() +} diff --git a/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt similarity index 75% rename from app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt rename to app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt index ae095128e..3231802c6 100644 --- a/app/src/main/java/ch/protonmail/android/worker/CreateDraftWorker.kt +++ b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt @@ -17,7 +17,7 @@ * along with ProtonMail. If not, see https://www.gnu.org/licenses/. */ -package ch.protonmail.android.worker +package ch.protonmail.android.worker.drafts import android.content.Context import androidx.hilt.Assisted @@ -58,14 +58,14 @@ import timber.log.Timber import java.time.Duration import javax.inject.Inject -internal const val KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_DB_ID = "keyCreateDraftMessageDbId" -internal const val KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_LOCAL_ID = "keyCreateDraftMessageLocalId" -internal const val KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_PARENT_ID = "keyCreateDraftMessageParentId" -internal const val KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_ACTION_TYPE_SERIALIZED = "keyCreateDraftMessageActionTypeSerialized" -internal const val KEY_INPUT_DATA_CREATE_DRAFT_PREVIOUS_SENDER_ADDRESS_ID = "keyCreateDraftPreviousSenderAddressId" +internal const val KEY_INPUT_SAVE_DRAFT_MSG_DB_ID = "keySaveDraftMessageDbId" +internal const val KEY_INPUT_SAVE_DRAFT_MSG_LOCAL_ID = "keySaveDraftMessageLocalId" +internal const val KEY_INPUT_SAVE_DRAFT_MSG_PARENT_ID = "keySaveDraftMessageParentId" +internal const val KEY_INPUT_SAVE_DRAFT_ACTION_TYPE_JSON = "keySaveDraftMessageActionTypeSerialized" +internal const val KEY_INPUT_SAVE_DRAFT_PREV_SENDER_ADDR_ID = "keySaveDraftPreviousSenderAddressId" -internal const val KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_ERROR_ENUM = "keyCreateDraftErrorResult" -internal const val KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID = "keyCreateDraftSuccessResultDbId" +internal const val KEY_OUTPUT_RESULT_SAVE_DRAFT_ERROR_ENUM = "keySaveDraftErrorResult" +internal const val KEY_OUTPUT_RESULT_SAVE_DRAFT_MESSAGE_ID = "keySaveDraftSuccessResultDbId" private const val INPUT_MESSAGE_DB_ID_NOT_FOUND = -1L private const val SAVE_DRAFT_MAX_RETRIES = 10 @@ -118,43 +118,32 @@ class CreateDraftWorker @WorkerInject constructor( updateStoredLocalDraft(responseDraft, message) Timber.i("Create Draft Worker API call succeeded") Result.success( - workDataOf(KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID to response.messageId) + workDataOf(KEY_OUTPUT_RESULT_SAVE_DRAFT_MESSAGE_ID to response.messageId) ) }, onFailure = { handleFailure(it.message) } ) - - // TODO test whether this is needed, drop otherwise -// set inline attachments from parent message that were inline previously -// for (Attachment atta : draftMessage.getAttachments()) { -// if (parentAttachmentList != null && !parentAttachmentList.isEmpty()) { -// for (Attachment parentAtta : parentAttachmentList) { -// if (parentAtta.getKeyPackets().equals(atta.getKeyPackets())) { -// atta.setInline(parentAtta.getInline()); -// } -// } -// } -// } } private fun updateStoredLocalDraft(apiDraft: Message, localDraft: Message) { - apiDraft.dbId = localDraft.dbId - apiDraft.toList = localDraft.toList - apiDraft.ccList = localDraft.ccList - apiDraft.bccList = localDraft.bccList - apiDraft.replyTos = localDraft.replyTos - apiDraft.sender = localDraft.sender - apiDraft.setLabelIDs(localDraft.getEventLabelIDs()) - apiDraft.parsedHeaders = localDraft.parsedHeaders - apiDraft.isDownloaded = true - apiDraft.setIsRead(true) - apiDraft.numAttachments = localDraft.numAttachments - apiDraft.localId = localDraft.messageId - - val id = messageDetailsRepository.saveMessageInDB(apiDraft) - Timber.d("Saved API draft in DB with DB id $id") + apiDraft.apply { + dbId = localDraft.dbId + toList = localDraft.toList + ccList = localDraft.ccList + bccList = localDraft.bccList + replyTos = localDraft.replyTos + sender = localDraft.sender + setLabelIDs(localDraft.getEventLabelIDs()) + parsedHeaders = localDraft.parsedHeaders + isDownloaded = true + setIsRead(true) + numAttachments = localDraft.numAttachments + localId = localDraft.messageId + } + + messageDetailsRepository.saveMessageInDB(apiDraft) } private fun handleFailure(error: String?): Result { @@ -234,28 +223,23 @@ class CreateDraftWorker @WorkerInject constructor( } private fun failureWithError(error: CreateDraftWorkerErrors): Result { - val errorData = workDataOf(KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_ERROR_ENUM to error.name) + val errorData = workDataOf(KEY_OUTPUT_RESULT_SAVE_DRAFT_ERROR_ENUM to error.name) return Result.failure(errorData) } private fun getInputActionType(): Constants.MessageActionType = inputData - .getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_ACTION_TYPE_SERIALIZED)?.deserialize() + .getString(KEY_INPUT_SAVE_DRAFT_ACTION_TYPE_JSON)?.deserialize() ?: NONE private fun getInputPreviousSenderAddressId() = - inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_PREVIOUS_SENDER_ADDRESS_ID) + inputData.getString(KEY_INPUT_SAVE_DRAFT_PREV_SENDER_ADDR_ID) private fun getInputParentId() = - inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_PARENT_ID) + inputData.getString(KEY_INPUT_SAVE_DRAFT_MSG_PARENT_ID) private fun getInputMessageDbId() = - inputData.getLong(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_DB_ID, INPUT_MESSAGE_DB_ID_NOT_FOUND) - - enum class CreateDraftWorkerErrors { - MessageNotFound, - ServerError - } + inputData.getLong(KEY_INPUT_SAVE_DRAFT_MSG_DB_ID, INPUT_MESSAGE_DB_ID_NOT_FOUND) class Enqueuer @Inject constructor(private val workManager: WorkManager) { @@ -272,11 +256,11 @@ class CreateDraftWorker @WorkerInject constructor( .setConstraints(constraints) .setInputData( workDataOf( - KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_DB_ID to message.dbId, - KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_LOCAL_ID to message.messageId, - KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_PARENT_ID to parentId, - KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_ACTION_TYPE_SERIALIZED to actionType.serialize(), - KEY_INPUT_DATA_CREATE_DRAFT_PREVIOUS_SENDER_ADDRESS_ID to previousSenderAddressId + KEY_INPUT_SAVE_DRAFT_MSG_DB_ID to message.dbId, + KEY_INPUT_SAVE_DRAFT_MSG_LOCAL_ID to message.messageId, + KEY_INPUT_SAVE_DRAFT_MSG_PARENT_ID to parentId, + KEY_INPUT_SAVE_DRAFT_ACTION_TYPE_JSON to actionType.serialize(), + KEY_INPUT_SAVE_DRAFT_PREV_SENDER_ADDR_ID to previousSenderAddressId ) ) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, Duration.ofSeconds(TEN_SECONDS)) diff --git a/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorkerErrors.kt b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorkerErrors.kt new file mode 100644 index 000000000..b2c33a63b --- /dev/null +++ b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorkerErrors.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * + * This file is part of ProtonMail. + * + * ProtonMail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonMail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonMail. If not, see https://www.gnu.org/licenses/. + */ + +package ch.protonmail.android.worker.drafts + +enum class CreateDraftWorkerErrors { + MessageNotFound, + ServerError +} diff --git a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt index a6e2ad3cf..3e89305a6 100644 --- a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt +++ b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt @@ -29,6 +29,7 @@ import ch.protonmail.android.testAndroid.lifecycle.testObserver import ch.protonmail.android.testAndroid.rx.TrampolineScheduler import ch.protonmail.android.usecase.VerifyConnection import ch.protonmail.android.usecase.compose.SaveDraft +import ch.protonmail.android.usecase.compose.SaveDraftResult import ch.protonmail.android.usecase.delete.DeleteMessage import ch.protonmail.android.usecase.fetch.FetchPublicKeys import ch.protonmail.android.utils.extensions.InstantExecutorExtension @@ -111,7 +112,7 @@ class ComposeMessageViewModelTest : CoroutinesTest { val createdDraft = Message(messageId = createdDraftId, localId = "local28348") val testObserver = viewModel.savingDraftComplete.testObserver() givenViewModelPropertiesAreInitialised() - coEvery { saveDraft(any()) } returns flowOf(SaveDraft.Result.Success(createdDraftId)) + coEvery { saveDraft(any()) } returns flowOf(SaveDraftResult.Success(createdDraftId)) every { messageDetailsRepository.findMessageByIdBlocking(createdDraftId) } returns createdDraft viewModel.saveDraft(message, hasConnectivity = false) @@ -128,7 +129,7 @@ class ComposeMessageViewModelTest : CoroutinesTest { val localDraftId = "localDraftId" val createdDraft = Message(messageId = createdDraftId, localId = localDraftId) givenViewModelPropertiesAreInitialised() - coEvery { saveDraft(any()) } returns flowOf(SaveDraft.Result.Success(createdDraftId)) + coEvery { saveDraft(any()) } returns flowOf(SaveDraftResult.Success(createdDraftId)) every { messageDetailsRepository.findMessageByIdBlocking(createdDraftId) } returns createdDraft viewModel.saveDraft(Message(), hasConnectivity = false) @@ -144,7 +145,7 @@ class ComposeMessageViewModelTest : CoroutinesTest { val localDraftId = "localDraftId" val createdDraft = Message(messageId = createdDraftId, localId = localDraftId) givenViewModelPropertiesAreInitialised() - coEvery { saveDraft(any()) } returns flowOf(SaveDraft.Result.Success(createdDraftId)) + coEvery { saveDraft(any()) } returns flowOf(SaveDraftResult.Success(createdDraftId)) every { messageDetailsRepository.findMessageByIdBlocking(createdDraftId) } returns createdDraft viewModel.saveDraft(Message(), hasConnectivity = false) diff --git a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt index 289b8c57e..8d7992c88 100644 --- a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt +++ b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt @@ -38,12 +38,11 @@ import ch.protonmail.android.core.Constants.MessageLocationType.DRAFT import ch.protonmail.android.crypto.AddressCrypto import ch.protonmail.android.domain.entity.Id import ch.protonmail.android.domain.entity.Name -import ch.protonmail.android.usecase.compose.SaveDraft.Result import ch.protonmail.android.usecase.compose.SaveDraft.SaveDraftParameters -import ch.protonmail.android.worker.CreateDraftWorker.CreateDraftWorkerErrors -import ch.protonmail.android.worker.CreateDraftWorker.Enqueuer -import ch.protonmail.android.worker.KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_ERROR_ENUM -import ch.protonmail.android.worker.KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID +import ch.protonmail.android.worker.drafts.CreateDraftWorker.Enqueuer +import ch.protonmail.android.worker.drafts.CreateDraftWorkerErrors +import ch.protonmail.android.worker.drafts.KEY_OUTPUT_RESULT_SAVE_DRAFT_ERROR_ENUM +import ch.protonmail.android.worker.drafts.KEY_OUTPUT_RESULT_SAVE_DRAFT_MESSAGE_ID import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every @@ -189,7 +188,7 @@ class SaveDraftTest : CoroutinesTest { ) // Then - val expectedError = Result.SendingInProgressError + val expectedError = SaveDraftResult.SendingInProgressError assertEquals(expectedError, result.first()) verify(exactly = 0) { createDraftScheduler.enqueue(any(), any(), any(), any()) } } @@ -230,6 +229,7 @@ class SaveDraftTest : CoroutinesTest { decryptedBody = "Message body in plain text" localId = localDraftId } + coEvery { messageDetailsRepository.findMessageById(any()) } returns null coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 9833L every { pendingActionsDao.findPendingSendByDbId(9833L) } returns null every { pendingActionsDao.findPendingSendByOfflineMessageId("45623") } answers { @@ -239,7 +239,7 @@ class SaveDraftTest : CoroutinesTest { } coEvery { uploadAttachments(any(), any(), any()) } returns UploadAttachments.Result.Success val workOutputData = workDataOf( - KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID to "createdDraftMessageId" + KEY_OUTPUT_RESULT_SAVE_DRAFT_MESSAGE_ID to "createdDraftMessageId" ) val workerStatusFlow = buildCreateDraftWorkerResponse(WorkInfo.State.SUCCEEDED, workOutputData) every { @@ -275,12 +275,13 @@ class SaveDraftTest : CoroutinesTest { localId = localDraftId } coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 9833L - every { messageDetailsRepository.findMessageByIdBlocking("45623") } returns message + coEvery { messageDetailsRepository.findMessageById("45623") } returns message + coEvery { messageDetailsRepository.findMessageById("createdDraftMessageId345") } returns null every { pendingActionsDao.findPendingSendByDbId(9833L) } returns null every { pendingActionsDao.findPendingSendByOfflineMessageId(localDraftId) } returns PendingSend() coEvery { uploadAttachments(any(), any(), any()) } returns UploadAttachments.Result.Success val workOutputData = workDataOf( - KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID to "createdDraftMessageId345" + KEY_OUTPUT_RESULT_SAVE_DRAFT_MESSAGE_ID to "createdDraftMessageId345" ) val workerStatusFlow = buildCreateDraftWorkerResponse(WorkInfo.State.SUCCEEDED, workOutputData) every { @@ -316,13 +317,13 @@ class SaveDraftTest : CoroutinesTest { } val apiDraft = message.copy(messageId = "createdDraftMessageId345") val workOutputData = workDataOf( - KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID to "createdDraftMessageId345" + KEY_OUTPUT_RESULT_SAVE_DRAFT_MESSAGE_ID to "createdDraftMessageId345" ) val workerStatusFlow = buildCreateDraftWorkerResponse(WorkInfo.State.SUCCEEDED, workOutputData) val newAttachmentIds = listOf("2345", "453") coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 9833L - every { messageDetailsRepository.findMessageByIdBlocking("45623") } returns message - every { messageDetailsRepository.findMessageByIdBlocking("createdDraftMessageId345") } returns apiDraft + coEvery { messageDetailsRepository.findMessageById("45623") } returns message + coEvery { messageDetailsRepository.findMessageById("createdDraftMessageId345") } returns apiDraft every { pendingActionsDao.findPendingSendByDbId(9833L) } returns null every { pendingActionsDao.findPendingSendByOfflineMessageId(localDraftId) } returns PendingSend() coEvery { uploadAttachments(any(), apiDraft, any()) } returns UploadAttachments.Result.Success @@ -360,11 +361,11 @@ class SaveDraftTest : CoroutinesTest { localId = localDraftId } val workOutputData = workDataOf( - KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_ERROR_ENUM to CreateDraftWorkerErrors.ServerError.name + KEY_OUTPUT_RESULT_SAVE_DRAFT_ERROR_ENUM to CreateDraftWorkerErrors.ServerError.name ) val workerStatusFlow = buildCreateDraftWorkerResponse(WorkInfo.State.FAILED, workOutputData) coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 9833L - every { messageDetailsRepository.findMessageByIdBlocking("45623") } returns message + coEvery { messageDetailsRepository.findMessageById("45623") } returns message every { pendingActionsDao.findPendingSendByDbId(9833L) } returns null every { createDraftScheduler.enqueue( @@ -382,7 +383,7 @@ class SaveDraftTest : CoroutinesTest { ).first() // Then - assertEquals(Result.OnlineDraftCreationFailed, result) + assertEquals(SaveDraftResult.OnlineDraftCreationFailed, result) } } @@ -400,11 +401,12 @@ class SaveDraftTest : CoroutinesTest { } val newAttachmentIds = listOf("2345", "453") val workOutputData = workDataOf( - KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID to "newDraftId" + KEY_OUTPUT_RESULT_SAVE_DRAFT_MESSAGE_ID to "newDraftId" ) val workerStatusFlow = buildCreateDraftWorkerResponse(WorkInfo.State.SUCCEEDED, workOutputData) coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 9833L - every { messageDetailsRepository.findMessageByIdBlocking("45623") } returns message + coEvery { messageDetailsRepository.findMessageById("newDraftId") } returns message.copy(messageId = "newDraftId") + coEvery { messageDetailsRepository.findMessageById("45623") } returns message every { pendingActionsDao.findPendingSendByDbId(9833L) } returns null coEvery { uploadAttachments(newAttachmentIds, any(), any()) } returns Failure("Can't upload attachments") every { @@ -422,7 +424,7 @@ class SaveDraftTest : CoroutinesTest { ).first() // Then - assertEquals(Result.UploadDraftAttachmentsFailed, result) + assertEquals(SaveDraftResult.UploadDraftAttachmentsFailed, result) } } @@ -440,13 +442,13 @@ class SaveDraftTest : CoroutinesTest { } val apiDraft = message.copy(messageId = "createdDraftMessageId345") val workOutputData = workDataOf( - KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID to "createdDraftMessageId345" + KEY_OUTPUT_RESULT_SAVE_DRAFT_MESSAGE_ID to "createdDraftMessageId345" ) val workerStatusFlow = buildCreateDraftWorkerResponse(WorkInfo.State.SUCCEEDED, workOutputData) val newAttachmentIds = listOf("2345", "453") coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 9833L - every { messageDetailsRepository.findMessageByIdBlocking("45623") } returns message - every { messageDetailsRepository.findMessageByIdBlocking("createdDraftMessageId345") } returns apiDraft + coEvery { messageDetailsRepository.findMessageById("45623") } returns message + coEvery { messageDetailsRepository.findMessageById("createdDraftMessageId345") } returns apiDraft every { pendingActionsDao.findPendingSendByDbId(9833L) } returns null every { pendingActionsDao.findPendingSendByOfflineMessageId(localDraftId) } returns PendingSend() coEvery { uploadAttachments(any(), apiDraft, any()) } returns UploadAttachments.Result.Success @@ -468,7 +470,7 @@ class SaveDraftTest : CoroutinesTest { // Then verify { pendingActionsDao.deletePendingUploadByMessageId("45623") } - assertEquals(Result.Success("createdDraftMessageId345"), result) + assertEquals(SaveDraftResult.Success("createdDraftMessageId345"), result) } } diff --git a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt index 72345d1cc..f9704e020 100644 --- a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt +++ b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt @@ -53,6 +53,15 @@ import ch.protonmail.android.domain.entity.user.Address import ch.protonmail.android.domain.entity.user.AddressKeys import ch.protonmail.android.utils.base64.Base64Encoder import ch.protonmail.android.utils.extensions.serialize +import ch.protonmail.android.worker.drafts.CreateDraftWorker +import ch.protonmail.android.worker.drafts.CreateDraftWorkerErrors +import ch.protonmail.android.worker.drafts.KEY_INPUT_SAVE_DRAFT_ACTION_TYPE_JSON +import ch.protonmail.android.worker.drafts.KEY_INPUT_SAVE_DRAFT_MSG_DB_ID +import ch.protonmail.android.worker.drafts.KEY_INPUT_SAVE_DRAFT_MSG_LOCAL_ID +import ch.protonmail.android.worker.drafts.KEY_INPUT_SAVE_DRAFT_MSG_PARENT_ID +import ch.protonmail.android.worker.drafts.KEY_INPUT_SAVE_DRAFT_PREV_SENDER_ADDR_ID +import ch.protonmail.android.worker.drafts.KEY_OUTPUT_RESULT_SAVE_DRAFT_ERROR_ENUM +import ch.protonmail.android.worker.drafts.KEY_OUTPUT_RESULT_SAVE_DRAFT_MESSAGE_ID import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every @@ -136,11 +145,11 @@ class CreateDraftWorkerTest : CoroutinesTest { val workSpec = requestSlot.captured.workSpec val constraints = workSpec.constraints val inputData = workSpec.input - val actualMessageDbId = inputData.getLong(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_DB_ID, -1) - val actualMessageLocalId = inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_LOCAL_ID) - val actualMessageParentId = inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_PARENT_ID) - val actualMessageActionType = inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_ACTION_TYPE_SERIALIZED) - val actualPreviousSenderAddress = inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_PREVIOUS_SENDER_ADDRESS_ID) + val actualMessageDbId = inputData.getLong(KEY_INPUT_SAVE_DRAFT_MSG_DB_ID, -1) + val actualMessageLocalId = inputData.getString(KEY_INPUT_SAVE_DRAFT_MSG_LOCAL_ID) + val actualMessageParentId = inputData.getString(KEY_INPUT_SAVE_DRAFT_MSG_PARENT_ID) + val actualMessageActionType = inputData.getString(KEY_INPUT_SAVE_DRAFT_ACTION_TYPE_JSON) + val actualPreviousSenderAddress = inputData.getString(KEY_INPUT_SAVE_DRAFT_PREV_SENDER_ADDR_ID) assertEquals(message.dbId, actualMessageDbId) assertEquals(message.messageId, actualMessageLocalId) assertEquals(messageParentId, actualMessageParentId) @@ -165,9 +174,9 @@ class CreateDraftWorkerTest : CoroutinesTest { val result = worker.doWork() // Then - val error = CreateDraftWorker.CreateDraftWorkerErrors.MessageNotFound + val error = CreateDraftWorkerErrors.MessageNotFound val expectedFailure = ListenableWorker.Result.failure( - Data.Builder().putString(KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_ERROR_ENUM, error.name).build() + Data.Builder().putString(KEY_OUTPUT_RESULT_SAVE_DRAFT_ERROR_ENUM, error.name).build() ) assertEquals(expectedFailure, result) } @@ -572,7 +581,7 @@ class CreateDraftWorkerTest : CoroutinesTest { // Then verify { messageDetailsRepository.saveMessageInDB(responseMessage) } val expected = ListenableWorker.Result.success( - Data.Builder().putString(KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_MESSAGE_ID, "response_message_id").build() + Data.Builder().putString(KEY_OUTPUT_RESULT_SAVE_DRAFT_MESSAGE_ID, "response_message_id").build() ) assertEquals(expected, result) } @@ -638,8 +647,8 @@ class CreateDraftWorkerTest : CoroutinesTest { // Then val expected = ListenableWorker.Result.failure( Data.Builder().putString( - KEY_OUTPUT_DATA_CREATE_DRAFT_RESULT_ERROR_ENUM, - CreateDraftWorker.CreateDraftWorkerErrors.ServerError.name + KEY_OUTPUT_RESULT_SAVE_DRAFT_ERROR_ENUM, + CreateDraftWorkerErrors.ServerError.name ).build() ) assertEquals(expected, result) @@ -647,22 +656,22 @@ class CreateDraftWorkerTest : CoroutinesTest { } private fun givenPreviousSenderAddress(address: String) { - every { parameters.inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_PREVIOUS_SENDER_ADDRESS_ID) } answers { address } + every { parameters.inputData.getString(KEY_INPUT_SAVE_DRAFT_PREV_SENDER_ADDR_ID) } answers { address } } private fun givenActionTypeInput(actionType: Constants.MessageActionType = NONE) { every { - parameters.inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_ACTION_TYPE_SERIALIZED) + parameters.inputData.getString(KEY_INPUT_SAVE_DRAFT_ACTION_TYPE_JSON) } answers { actionType.serialize() } } private fun givenParentIdInput(parentId: String) { - every { parameters.inputData.getString(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_PARENT_ID) } answers { parentId } + every { parameters.inputData.getString(KEY_INPUT_SAVE_DRAFT_MSG_PARENT_ID) } answers { parentId } } private fun givenMessageIdInput(messageDbId: Long) { - every { parameters.inputData.getLong(KEY_INPUT_DATA_CREATE_DRAFT_MESSAGE_DB_ID, -1) } answers { messageDbId } + every { parameters.inputData.getLong(KEY_INPUT_SAVE_DRAFT_MSG_DB_ID, -1) } answers { messageDbId } } } From 35730d2a8a2c1756307597d330d43277f31d4e98 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Tue, 22 Dec 2020 09:54:13 +0100 Subject: [PATCH 030/145] Avoid force unwrapping of attachment's keypacket MAILAND-1018 --- .../worker/drafts/CreateDraftWorker.kt | 19 ++++++++++++------- .../compose/ComposeMessageViewModelTest.kt | 10 ++++------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt index 3231802c6..c05d0222f 100644 --- a/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt +++ b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt @@ -172,23 +172,28 @@ class CreateDraftWorker @WorkerInject constructor( val keyPackets = if (isSenderAddressChanged()) { reEncryptAttachment(senderAddress, attachment) } else { - attachment.keyPackets!! + attachment.keyPackets + } + + keyPackets?.let { + draftAttachments[attachment.attachmentId!!] = keyPackets } - draftAttachments[attachment.attachmentId!!] = keyPackets } return draftAttachments } - private fun reEncryptAttachment(senderAddress: Address, attachment: Attachment): String { + private fun reEncryptAttachment(senderAddress: Address, attachment: Attachment): String? { val previousSenderAddressId = requireNotNull(getInputPreviousSenderAddressId()) val addressCrypto = addressCryptoFactory.create(Id(previousSenderAddressId), Name(userManager.username)) val primaryKey = senderAddress.keys val publicKey = addressCrypto.buildArmoredPublicKey(primaryKey.primaryKey!!.privateKey) - val keyPackage = base64.decode(attachment.keyPackets!!) - val sessionKey = addressCrypto.decryptKeyPacket(keyPackage) - val newKeyPackage = addressCrypto.encryptKeyPacket(sessionKey, publicKey) - return base64.encode(newKeyPackage) + return attachment.keyPackets?.let { + val keyPackage = base64.decode(it) + val sessionKey = addressCrypto.decryptKeyPacket(keyPackage) + val newKeyPackage = addressCrypto.encryptKeyPacket(sessionKey, publicKey) + return base64.encode(newKeyPackage) + } } private fun isReplyActionAndAttachmentNotInline(attachment: Attachment): Boolean { diff --git a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt index 3e89305a6..6793a2d73 100644 --- a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt +++ b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt @@ -35,12 +35,10 @@ import ch.protonmail.android.usecase.fetch.FetchPublicKeys import ch.protonmail.android.utils.extensions.InstantExecutorExtension import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.impl.annotations.InjectMockKs import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.RelaxedMockK import io.mockk.junit5.MockKExtension -import io.mockk.verify import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runBlockingTest import me.proton.core.test.kotlin.CoroutinesTest @@ -113,11 +111,11 @@ class ComposeMessageViewModelTest : CoroutinesTest { val testObserver = viewModel.savingDraftComplete.testObserver() givenViewModelPropertiesAreInitialised() coEvery { saveDraft(any()) } returns flowOf(SaveDraftResult.Success(createdDraftId)) - every { messageDetailsRepository.findMessageByIdBlocking(createdDraftId) } returns createdDraft + coEvery { messageDetailsRepository.findMessageById(createdDraftId) } returns createdDraft viewModel.saveDraft(message, hasConnectivity = false) - verify { messageDetailsRepository.findMessageByIdBlocking(createdDraftId) } + coEvery { messageDetailsRepository.findMessageById(createdDraftId) } assertEquals(createdDraft, testObserver.observedValues[0]) } } @@ -130,7 +128,7 @@ class ComposeMessageViewModelTest : CoroutinesTest { val createdDraft = Message(messageId = createdDraftId, localId = localDraftId) givenViewModelPropertiesAreInitialised() coEvery { saveDraft(any()) } returns flowOf(SaveDraftResult.Success(createdDraftId)) - every { messageDetailsRepository.findMessageByIdBlocking(createdDraftId) } returns createdDraft + coEvery { messageDetailsRepository.findMessageById(createdDraftId) } returns createdDraft viewModel.saveDraft(Message(), hasConnectivity = false) @@ -146,7 +144,7 @@ class ComposeMessageViewModelTest : CoroutinesTest { val createdDraft = Message(messageId = createdDraftId, localId = localDraftId) givenViewModelPropertiesAreInitialised() coEvery { saveDraft(any()) } returns flowOf(SaveDraftResult.Success(createdDraftId)) - every { messageDetailsRepository.findMessageByIdBlocking(createdDraftId) } returns createdDraft + coEvery { messageDetailsRepository.findMessageById(createdDraftId) } returns createdDraft viewModel.saveDraft(Message(), hasConnectivity = false) From 9a11caaac99aa8457de58144db7b7bbef94165f2 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Wed, 30 Dec 2020 09:26:52 +0100 Subject: [PATCH 031/145] Minor improvements and formatting - Replace `!isEmpty` check for `isNotEmpty` and reformat ShowLabelsManagerDialogTask - Explicitly name SaveDraftResult in ViewModel `collect` lambda MAILAND-1018 --- .../mailbox/ShowLabelsManagerDialogTask.kt | 37 +++++++++---------- .../compose/ComposeMessageViewModel.kt | 6 +-- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/activities/mailbox/ShowLabelsManagerDialogTask.kt b/app/src/main/java/ch/protonmail/android/activities/mailbox/ShowLabelsManagerDialogTask.kt index fe773d2a6..2ed8a2197 100644 --- a/app/src/main/java/ch/protonmail/android/activities/mailbox/ShowLabelsManagerDialogTask.kt +++ b/app/src/main/java/ch/protonmail/android/activities/mailbox/ShowLabelsManagerDialogTask.kt @@ -27,31 +27,30 @@ import java.util.ArrayList import java.util.HashMap import java.util.HashSet -/** - * Created by Kamil Rajtar on 24.07.18. - */ -internal class ShowLabelsManagerDialogTask(private val fragmentManager: FragmentManager, - private val messageDetailsRepository: MessageDetailsRepository, - private val messageIds:List):AsyncTask>() { +internal class ShowLabelsManagerDialogTask( + private val fragmentManager: FragmentManager, + private val messageDetailsRepository: MessageDetailsRepository, + private val messageIds: List +) : AsyncTask>() { - override fun doInBackground(vararg voids:Void):List { - return messageIds.filter { !it.isEmpty() }.mapNotNull(messageDetailsRepository::findMessageByIdBlocking) + override fun doInBackground(vararg voids: Void): List { + return messageIds.filter { it.isNotEmpty() }.mapNotNull(messageDetailsRepository::findMessageByIdBlocking) } - override fun onPostExecute(messages:List) { - val attachedLabels=HashSet() - val numberOfSelectedMessages=HashMap() - messages.forEach {message-> - val messageLabelIds=message.labelIDsNotIncludingLocations - messageLabelIds.forEach {labelId-> - numberOfSelectedMessages[labelId]=numberOfSelectedMessages[labelId]?.let {it+1}?:1 + override fun onPostExecute(messages: List) { + val attachedLabels = HashSet() + val numberOfSelectedMessages = HashMap() + messages.forEach { message -> + val messageLabelIds = message.labelIDsNotIncludingLocations + messageLabelIds.forEach { labelId -> + numberOfSelectedMessages[labelId] = numberOfSelectedMessages[labelId]?.let { it + 1 } ?: 1 } attachedLabels.addAll(messageLabelIds) } - val manageLabelsDialogFragment=ManageLabelsDialogFragment.newInstance( - attachedLabels, numberOfSelectedMessages, ArrayList(messageIds)) - val transaction=fragmentManager.beginTransaction() - transaction.add(manageLabelsDialogFragment,manageLabelsDialogFragment.fragmentKey) + val manageLabelsDialogFragment = ManageLabelsDialogFragment.newInstance( + attachedLabels, numberOfSelectedMessages, ArrayList(messageIds)) + val transaction = fragmentManager.beginTransaction() + transaction.add(manageLabelsDialogFragment, manageLabelsDialogFragment.fragmentKey) transaction.commitAllowingStateLoss() } } diff --git a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt index 4aa1a7dc4..c4a09e024 100644 --- a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt +++ b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt @@ -436,9 +436,9 @@ class ComposeMessageViewModel @Inject constructor( _actionId, _oldSenderAddressId ) - ).collect { - when (it) { - is SaveDraftResult.Success -> onDraftSaved(it.draftId) + ).collect { saveDraftResult -> + when (saveDraftResult) { + is SaveDraftResult.Success -> onDraftSaved(saveDraftResult.draftId) SaveDraftResult.OnlineDraftCreationFailed -> { val errorMessage = getStringResource( R.string.failed_saving_draft_online From 1d90531d1fd8011d6b01ebe485f5c223ef02a908 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Mon, 4 Jan 2021 17:46:55 +0100 Subject: [PATCH 032/145] Adapt implementation to new DraftBody model replacing NewMessage Needed after rebasing on changes in develop. - Introduce `setSender`, `setMessageBody` and `addAttachmentKeyPacket` utility methods in `DraftBody` * These serve to protect the client from knowing API data models details (such as `ServerMessageSender`). * `addAttachmentKeyPacket` is also needed to avoid kotlin to throwing `Unsupported OP Exception` when trying to put data into it directly MAILAND-1018 --- .../android/api/models/DraftBody.kt | 18 ++++++++++-- .../messages/receive/IMessageFactory.kt | 4 +-- .../models/messages/receive/MessageFactory.kt | 6 ++-- .../worker/drafts/CreateDraftWorker.kt | 5 ++-- .../android/worker/CreateDraftWorkerTest.kt | 29 +++++++++---------- 5 files changed, 37 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/api/models/DraftBody.kt b/app/src/main/java/ch/protonmail/android/api/models/DraftBody.kt index d9b8d24a0..440565025 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/DraftBody.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/DraftBody.kt @@ -19,9 +19,10 @@ package ch.protonmail.android.api.models import ch.protonmail.android.api.models.messages.receive.ServerMessage +import ch.protonmail.android.api.models.messages.receive.ServerMessageSender +import ch.protonmail.android.api.models.room.messages.MessageSender import ch.protonmail.android.api.utils.Fields import com.google.gson.annotations.SerializedName -import java.util.HashMap data class DraftBody( val serverMessage: ServerMessage @@ -39,9 +40,22 @@ data class DraftBody( var attachmentKeyPackets: MutableMap? = null get() { if (field == null) { - field = HashMap() + field = hashMapOf() } return field } + + fun setSender(messageSender: MessageSender) { + message.sender = ServerMessageSender(messageSender.name, messageSender.emailAddress) + } + + fun setMessageBody(messageBody: String) { + message.body = messageBody + } + + fun addAttachmentKeyPacket(key: String, value: String) { + attachmentKeyPackets!![key] = value + } + } diff --git a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/IMessageFactory.kt b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/IMessageFactory.kt index 7bd065416..fdd17ddf6 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/IMessageFactory.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/IMessageFactory.kt @@ -18,11 +18,11 @@ */ package ch.protonmail.android.api.models.messages.receive -import ch.protonmail.android.api.models.NewMessage +import ch.protonmail.android.api.models.DraftBody import ch.protonmail.android.api.models.room.messages.Message interface IMessageFactory { fun createMessage(serverMessage: ServerMessage): Message fun createServerMessage(message: Message): ServerMessage - fun createDraftApiRequest(message: Message): NewMessage + fun createDraftApiRequest(message: Message): DraftBody } diff --git a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageFactory.kt b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageFactory.kt index 3f1344bd6..981f20950 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageFactory.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageFactory.kt @@ -18,7 +18,7 @@ */ package ch.protonmail.android.api.models.messages.receive -import ch.protonmail.android.api.models.NewMessage +import ch.protonmail.android.api.models.DraftBody import ch.protonmail.android.api.models.enumerations.MessageFlag import ch.protonmail.android.api.models.factories.checkIfSet import ch.protonmail.android.api.models.factories.makeInt @@ -34,8 +34,8 @@ class MessageFactory @Inject constructor( private val messageSenderFactory: IMessageSenderFactory ) : IMessageFactory { - override fun createDraftApiRequest(message: Message): NewMessage = - NewMessage(createServerMessage(message)) + override fun createDraftApiRequest(message: Message): DraftBody = + DraftBody(createServerMessage(message)) override fun createServerMessage(message: Message): ServerMessage { return message.let { diff --git a/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt index c05d0222f..470dfdec3 100644 --- a/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt +++ b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt @@ -39,7 +39,6 @@ import ch.protonmail.android.api.models.room.messages.Attachment import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.api.models.room.messages.MessageSender import ch.protonmail.android.api.segments.TEN_SECONDS -import ch.protonmail.android.api.utils.Fields import ch.protonmail.android.core.Constants import ch.protonmail.android.core.Constants.MessageActionType.FORWARD import ch.protonmail.android.core.Constants.MessageActionType.NONE @@ -92,7 +91,7 @@ class CreateDraftWorker @WorkerInject constructor( val createDraftRequest = messageFactory.createDraftApiRequest(message) parentId?.let { - createDraftRequest.setParentID(parentId) + createDraftRequest.parentID = parentId createDraftRequest.action = getInputActionType().messageActionTypeValue val parentMessage = messageDetailsRepository.findMessageByIdBlocking(parentId) val attachments = parentMessage?.attachments(messageDetailsRepository.databaseProvider.provideMessagesDao()) @@ -103,7 +102,7 @@ class CreateDraftWorker @WorkerInject constructor( } val encryptedMessage = requireNotNull(message.messageBody) - createDraftRequest.addMessageBody(Fields.Message.SELF, encryptedMessage); + createDraftRequest.setMessageBody(encryptedMessage) createDraftRequest.setSender(buildMessageSender(message, senderAddress)) return runCatching { diff --git a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt index f9704e020..f8a974eba 100644 --- a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt +++ b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt @@ -29,14 +29,13 @@ import androidx.work.WorkManager import androidx.work.WorkerParameters import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository import ch.protonmail.android.api.ProtonMailApiManager -import ch.protonmail.android.api.models.NewMessage +import ch.protonmail.android.api.models.DraftBody import ch.protonmail.android.api.models.messages.ParsedHeaders import ch.protonmail.android.api.models.messages.receive.MessageFactory import ch.protonmail.android.api.models.messages.receive.MessageResponse import ch.protonmail.android.api.models.room.messages.Attachment import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.api.models.room.messages.MessageSender -import ch.protonmail.android.api.utils.Fields.Message.SELF import ch.protonmail.android.core.Constants import ch.protonmail.android.core.Constants.MessageActionType.FORWARD import ch.protonmail.android.core.Constants.MessageActionType.NONE @@ -194,7 +193,7 @@ class CreateDraftWorkerTest : CoroutinesTest { addressID = "addressId" messageBody = "messageBody" } - val apiDraftMessage = mockk(relaxed = true) + val apiDraftMessage = mockk(relaxed = true) givenMessageIdInput(messageDbId) givenParentIdInput(parentId) givenActionTypeInput(actionType) @@ -205,7 +204,7 @@ class CreateDraftWorkerTest : CoroutinesTest { worker.doWork() // Then - verify { apiDraftMessage.setParentID(parentId) } + verify { apiDraftMessage.parentID = parentId } verify { apiDraftMessage.action = 2 } // Always get parent message from messageDetailsDB, never from searchDB // ignoring isTransient property as the values in the two DB appears to be the same @@ -224,7 +223,7 @@ class CreateDraftWorkerTest : CoroutinesTest { addressID = addressId messageBody = "messageBody" } - val apiDraftMessage = mockk(relaxed = true) + val apiDraftMessage = mockk(relaxed = true) val address = Address( Id(addressId), null, @@ -251,7 +250,7 @@ class CreateDraftWorkerTest : CoroutinesTest { // Then val messageSender = MessageSender("senderName", "sender@email.it") verify { apiDraftMessage.setSender(messageSender) } - verify { apiDraftMessage.addMessageBody(SELF, "messageBody") } + verify { apiDraftMessage.setMessageBody("messageBody") } } } @@ -267,7 +266,7 @@ class CreateDraftWorkerTest : CoroutinesTest { messageBody = "messageBody2341" sender = MessageSender("sender by alias", "sender+alias@pm.me") } - val apiDraftMessage = mockk(relaxed = true) + val apiDraftMessage = mockk(relaxed = true) givenMessageIdInput(messageDbId) givenActionTypeInput() every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message @@ -302,7 +301,7 @@ class CreateDraftWorkerTest : CoroutinesTest { val decodedPacketsBytes = "decoded attachment packets".toByteArray() val encryptedKeyPackets = "re-encrypted att key packets".toByteArray() - val apiDraftMessage = mockk(relaxed = true) + val apiDraftMessage = mockk(relaxed = true) val parentMessage = mockk { every { attachments(any()) } returns listOf(attachment) } @@ -338,7 +337,7 @@ class CreateDraftWorkerTest : CoroutinesTest { val attachmentReEncrypted = attachment.copy(keyPackets = "encrypted encoded packets") verify { parentMessage.attachments(messageDetailsRepository.databaseProvider.provideMessagesDao()) } verify { addressCrypto.buildArmoredPublicKey(privateKey) } - verify { apiDraftMessage.addAttachmentKeyPacket("attachment1", attachmentReEncrypted.keyPackets) } + verify { apiDraftMessage.addAttachmentKeyPacket("attachment1", attachmentReEncrypted.keyPackets!!) } } } @@ -357,7 +356,7 @@ class CreateDraftWorkerTest : CoroutinesTest { val attachment2 = Attachment("attachment2", "pic2.jpg", keyPackets = "somePackets2", inline = false) val previousSenderAddressId = "previousSenderId82348" - val apiDraftMessage = mockk(relaxed = true) + val apiDraftMessage = mockk(relaxed = true) val parentMessage = mockk { every { attachments(any()) } returns listOf(attachment, attachment2) } @@ -389,7 +388,7 @@ class CreateDraftWorkerTest : CoroutinesTest { addressID = "addressId835" messageBody = "messageBody" } - val apiDraftMessage = mockk(relaxed = true) + val apiDraftMessage = mockk(relaxed = true) val parentMessage = mockk { every { attachments(any()) } returns listOf( Attachment("attachment", keyPackets = "OriginalAttachmentPackets", inline = true), @@ -429,7 +428,7 @@ class CreateDraftWorkerTest : CoroutinesTest { addressID = "addressId835" messageBody = "messageBody" } - val apiDraftMessage = mockk(relaxed = true) + val apiDraftMessage = mockk(relaxed = true) val parentMessage = mockk { every { attachments(any()) } returns listOf( Attachment("attachment", keyPackets = "OriginalAttachmentPackets", inline = true), @@ -475,7 +474,7 @@ class CreateDraftWorkerTest : CoroutinesTest { messageBody = "messageBody" } - val apiDraftMessage = mockk(relaxed = true) + val apiDraftMessage = mockk(relaxed = true) givenMessageIdInput(messageDbId) givenParentIdInput(parentId) givenActionTypeInput(NONE) @@ -507,7 +506,7 @@ class CreateDraftWorkerTest : CoroutinesTest { numAttachments = 3 } - val apiDraftRequest = mockk(relaxed = true) + val apiDraftRequest = mockk(relaxed = true) val responseMessage = mockk(relaxed = true) val apiDraftResponse = mockk { every { code } returns 1000 @@ -560,7 +559,7 @@ class CreateDraftWorkerTest : CoroutinesTest { numAttachments = 3 } - val apiDraftRequest = mockk(relaxed = true) + val apiDraftRequest = mockk(relaxed = true) val responseMessage = mockk(relaxed = true) val apiDraftResponse = mockk { every { code } returns 1000 From ab47dcdd34f5f09a2376e6c823779e4e3765d204 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Thu, 14 Jan 2021 10:03:35 +0100 Subject: [PATCH 033/145] Migrate new tests to junit4 after rebasing on dev Since we recently migrated all the tests to junit4 and removed junit5 libs, this is needed to align with such changes MAILAND-1018 --- .../compose/ComposeMessageViewModelTest.kt | 24 +++++++--- .../android/usecase/compose/SaveDraftTest.kt | 12 +++-- .../extensions/InstantExecutorExtension.kt | 45 ------------------- .../android/worker/CreateDraftWorkerTest.kt | 11 +++-- 4 files changed, 31 insertions(+), 61 deletions(-) delete mode 100644 app/src/test/java/ch/protonmail/android/utils/extensions/InstantExecutorExtension.kt diff --git a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt index 6793a2d73..b468a8148 100644 --- a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt +++ b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt @@ -19,6 +19,7 @@ package ch.protonmail.android.compose +import androidx.arch.core.executor.testing.InstantTaskExecutorRule import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository import ch.protonmail.android.api.NetworkConfigurator import ch.protonmail.android.api.models.room.messages.Message @@ -32,27 +33,29 @@ import ch.protonmail.android.usecase.compose.SaveDraft import ch.protonmail.android.usecase.compose.SaveDraftResult import ch.protonmail.android.usecase.delete.DeleteMessage import ch.protonmail.android.usecase.fetch.FetchPublicKeys -import ch.protonmail.android.utils.extensions.InstantExecutorExtension +import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify +import io.mockk.every import io.mockk.impl.annotations.InjectMockKs import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.RelaxedMockK -import io.mockk.junit5.MockKExtension import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runBlockingTest import me.proton.core.test.kotlin.CoroutinesTest import org.junit.Assert.assertEquals import org.junit.Rule -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith +import kotlin.test.BeforeTest +import kotlin.test.Test -@ExtendWith(MockKExtension::class, InstantExecutorExtension::class) class ComposeMessageViewModelTest : CoroutinesTest { - @Rule + @get:Rule val trampolineSchedulerRule = TrampolineScheduler() + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + @RelaxedMockK lateinit var composeMessageRepository: ComposeMessageRepository @@ -83,6 +86,15 @@ class ComposeMessageViewModelTest : CoroutinesTest { @InjectMockKs lateinit var viewModel: ComposeMessageViewModel + @BeforeTest + fun setUp() { + MockKAnnotations.init(this) + // To avoid `EmptyList` to be returned by Mockk automatically as that causes + // UnsupportedOperationException: Operation is not supported for read-only collection + // when trying to add elements (in prod we ArrayList so this doesn't happen) + every { userManager.user.senderEmailAddresses } returns mutableListOf() + } + @Test fun saveDraftCallsSaveDraftUseCaseWhenTheDraftIsNew() { runBlockingTest { diff --git a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt index 8d7992c88..7b05bab38 100644 --- a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt +++ b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt @@ -43,12 +43,12 @@ import ch.protonmail.android.worker.drafts.CreateDraftWorker.Enqueuer import ch.protonmail.android.worker.drafts.CreateDraftWorkerErrors import ch.protonmail.android.worker.drafts.KEY_OUTPUT_RESULT_SAVE_DRAFT_ERROR_ENUM import ch.protonmail.android.worker.drafts.KEY_OUTPUT_RESULT_SAVE_DRAFT_MESSAGE_ID +import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.InjectMockKs import io.mockk.impl.annotations.RelaxedMockK -import io.mockk.junit5.MockKExtension import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.flow.Flow @@ -57,11 +57,10 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runBlockingTest import me.proton.core.test.kotlin.CoroutinesTest import org.junit.Assert.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith import java.util.UUID +import kotlin.test.BeforeTest +import kotlin.test.Test -@ExtendWith(MockKExtension::class) class SaveDraftTest : CoroutinesTest { @RelaxedMockK @@ -84,6 +83,11 @@ class SaveDraftTest : CoroutinesTest { private val currentUsername = "username" + @BeforeTest + fun setUp() { + MockKAnnotations.init(this) + } + @Test fun saveDraftSavesEncryptedDraftMessageToDb() { runBlockingTest { diff --git a/app/src/test/java/ch/protonmail/android/utils/extensions/InstantExecutorExtension.kt b/app/src/test/java/ch/protonmail/android/utils/extensions/InstantExecutorExtension.kt deleted file mode 100644 index 5b5d5b3fd..000000000 --- a/app/src/test/java/ch/protonmail/android/utils/extensions/InstantExecutorExtension.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2020 Proton Technologies AG - * - * This file is part of ProtonMail. - * - * ProtonMail is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ProtonMail is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ProtonMail. If not, see https://www.gnu.org/licenses/. - */ - -package ch.protonmail.android.utils.extensions - -import androidx.arch.core.executor.ArchTaskExecutor -import androidx.arch.core.executor.TaskExecutor -import org.junit.jupiter.api.extension.AfterEachCallback -import org.junit.jupiter.api.extension.BeforeEachCallback -import org.junit.jupiter.api.extension.ExtensionContext - -class InstantExecutorExtension : BeforeEachCallback, AfterEachCallback { - - override fun beforeEach(context: ExtensionContext?) { - ArchTaskExecutor.getInstance() - .setDelegate(object : TaskExecutor() { - override fun executeOnDiskIO(runnable: Runnable) = runnable.run() - - override fun postToMainThread(runnable: Runnable) = runnable.run() - - override fun isMainThread(): Boolean = true - }) - } - - override fun afterEach(context: ExtensionContext?) { - ArchTaskExecutor.getInstance().setDelegate(null) - } - -} diff --git a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt index f8a974eba..b5986c037 100644 --- a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt +++ b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt @@ -61,12 +61,12 @@ import ch.protonmail.android.worker.drafts.KEY_INPUT_SAVE_DRAFT_MSG_PARENT_ID import ch.protonmail.android.worker.drafts.KEY_INPUT_SAVE_DRAFT_PREV_SENDER_ADDR_ID import ch.protonmail.android.worker.drafts.KEY_OUTPUT_RESULT_SAVE_DRAFT_ERROR_ENUM import ch.protonmail.android.worker.drafts.KEY_OUTPUT_RESULT_SAVE_DRAFT_MESSAGE_ID +import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.InjectMockKs import io.mockk.impl.annotations.RelaxedMockK -import io.mockk.junit5.MockKExtension import io.mockk.mockk import io.mockk.slot import io.mockk.verify @@ -75,12 +75,10 @@ import io.mockk.verifySequence import kotlinx.coroutines.test.runBlockingTest import me.proton.core.test.kotlin.CoroutinesTest import org.junit.Assert.assertEquals -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith import java.io.IOException +import kotlin.test.BeforeTest +import kotlin.test.Test -@ExtendWith(MockKExtension::class) class CreateDraftWorkerTest : CoroutinesTest { @RelaxedMockK @@ -113,8 +111,9 @@ class CreateDraftWorkerTest : CoroutinesTest { @InjectMockKs private lateinit var worker: CreateDraftWorker - @BeforeEach + @BeforeTest fun setUp() { + MockKAnnotations.init(this) coEvery { apiManager.createDraft(any()) } returns mockk(relaxed = true) } From 2b37368814b2c38cce99ceeb98ae34f9a7b56911 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Tue, 22 Dec 2020 17:55:55 +0100 Subject: [PATCH 034/145] ComposeMessageViewModel calls saveDraft use case to update draft MAILAND-1281 --- .../android/attachments/UploadAttachments.kt | 5 +-- .../compose/ComposeMessageViewModel.kt | 15 ++++++--- .../compose/ComposeMessageViewModelTest.kt | 33 +++++++++++++++++++ 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt b/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt index 58a199121..b0ef33f88 100644 --- a/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt +++ b/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt @@ -54,8 +54,9 @@ class UploadAttachments @Inject constructor( val attachment = messageDetailsRepository.findAttachmentById(attachmentId) if (attachment?.filePath == null || attachment.isUploaded || attachment.doesFileExist.not()) { - Timber.e("Skipping attachment: either not found, invalid or" + - " was already uploaded = ${attachment?.isUploaded}") + Timber.e( + "Skipping attachment: not found, invalid or was already uploaded = ${attachment?.isUploaded}" + ) continue } attachment.setMessage(message) diff --git a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt index c4a09e024..54b630a07 100644 --- a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt +++ b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt @@ -397,11 +397,16 @@ class ComposeMessageViewModel @Inject constructor( message.messageId = draftId val newAttachments = calculateNewAttachments(uploadAttachments) - postMessageServiceFactory.startUpdateDraftService( - _dbId!!, - message.decryptedBody ?: "", - newAttachments, uploadAttachments, _oldSenderAddressId - ) + saveDraft( + SaveDraft.SaveDraftParameters( + message, + newAttachments, + parentId, + _actionId, + _oldSenderAddressId + ) + ).collect {} + if (newAttachments.isNotEmpty() && uploadAttachments) { _oldSenderAddressId = message.addressID ?: _messageDataResult.addressId // overwrite "old sender ID" when updating draft diff --git a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt index b468a8148..440db769f 100644 --- a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt +++ b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt @@ -98,11 +98,14 @@ class ComposeMessageViewModelTest : CoroutinesTest { @Test fun saveDraftCallsSaveDraftUseCaseWhenTheDraftIsNew() { runBlockingTest { + // Given val message = Message() givenViewModelPropertiesAreInitialised() + // When viewModel.saveDraft(message, hasConnectivity = false) + // Then val parameters = SaveDraft.SaveDraftParameters( message, emptyList(), @@ -117,6 +120,7 @@ class ComposeMessageViewModelTest : CoroutinesTest { @Test fun saveDraftReadsNewlyCreatedDraftFromRepositoryAndPostsItToLiveDataWhenSaveDraftUseCaseSucceeds() { runBlockingTest { + // Given val message = Message() val createdDraftId = "newDraftId" val createdDraft = Message(messageId = createdDraftId, localId = "local28348") @@ -125,6 +129,7 @@ class ComposeMessageViewModelTest : CoroutinesTest { coEvery { saveDraft(any()) } returns flowOf(SaveDraftResult.Success(createdDraftId)) coEvery { messageDetailsRepository.findMessageById(createdDraftId) } returns createdDraft + // When viewModel.saveDraft(message, hasConnectivity = false) coEvery { messageDetailsRepository.findMessageById(createdDraftId) } @@ -142,8 +147,10 @@ class ComposeMessageViewModelTest : CoroutinesTest { coEvery { saveDraft(any()) } returns flowOf(SaveDraftResult.Success(createdDraftId)) coEvery { messageDetailsRepository.findMessageById(createdDraftId) } returns createdDraft + // When viewModel.saveDraft(Message(), hasConnectivity = false) + // Then coVerify { composeMessageRepository.deleteMessageById(localDraftId) } } } @@ -151,6 +158,7 @@ class ComposeMessageViewModelTest : CoroutinesTest { @Test fun saveDraftObservesMessageInComposeRepositoryToGetNotifiedWhenMessageIsSent() { runBlockingTest { + // Given val createdDraftId = "newDraftId" val localDraftId = "localDraftId" val createdDraft = Message(messageId = createdDraftId, localId = localDraftId) @@ -158,13 +166,38 @@ class ComposeMessageViewModelTest : CoroutinesTest { coEvery { saveDraft(any()) } returns flowOf(SaveDraftResult.Success(createdDraftId)) coEvery { messageDetailsRepository.findMessageById(createdDraftId) } returns createdDraft + // When viewModel.saveDraft(Message(), hasConnectivity = false) + // Then assertEquals(createdDraftId, viewModel.draftId) coVerify { composeMessageRepository.findMessageByIdObservable(createdDraftId) } } } + @Test + fun saveDraftCallsSaveDraftUseCaseWhenTheDraftIsExisting() { + runBlockingTest { + // Given + val message = Message() + givenViewModelPropertiesAreInitialised() + viewModel.draftId = "non-empty-draftId" + + // When + viewModel.saveDraft(message, hasConnectivity = false) + + // Then + val parameters = SaveDraft.SaveDraftParameters( + message, + emptyList(), + "parentId823", + Constants.MessageActionType.FORWARD, + "previousSenderAddressId" + ) + coVerify { saveDraft(parameters) } + } + } + private fun givenViewModelPropertiesAreInitialised() { // Needed to set class fields to the right value and allow code under test to get executed viewModel.prepareMessageData(false, "addressId", "mail-alias", false) From 020b182b055ee48245110959716b601c0ea47961 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Wed, 23 Dec 2020 11:21:13 +0100 Subject: [PATCH 035/145] SaveDraft removes pending upload when a failure occours Either in the save draft API call itself on in the UploadAttachments use case. The responsibility to `insert` and `delete` pending upload would fit better into `UploadAttachments` itself. I decided not to move it yet as that'd change behaviour in case the user takes actions on the draft while it is still uploading (as pendingUpload wouldn't be inserted yet) MAILAND-1281 --- .../java/ch/protonmail/android/usecase/compose/SaveDraft.kt | 4 +++- .../ch/protonmail/android/usecase/compose/SaveDraftTest.kt | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt index cced05af8..8de2ba1f3 100644 --- a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt +++ b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt @@ -109,15 +109,17 @@ class SaveDraft @Inject constructor( messageDetailsRepository.findMessageById(createdDraftId)?.let { val uploadResult = uploadAttachments(params.newAttachmentIds, it, addressCrypto) + pendingActionsDao.deletePendingUploadByMessageId(localDraftId) + if (uploadResult is UploadAttachments.Result.Failure) { return@map SaveDraftResult.UploadDraftAttachmentsFailed } - pendingActionsDao.deletePendingUploadByMessageId(localDraftId) return@map SaveDraftResult.Success(createdDraftId) } } Timber.e("Saving Draft to API for messageId $localDraftId FAILED.") + pendingActionsDao.deletePendingUploadByMessageId(localDraftId) return@map SaveDraftResult.OnlineDraftCreationFailed } .flowOn(dispatchers.Io) diff --git a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt index 7b05bab38..b560a2f66 100644 --- a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt +++ b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt @@ -353,7 +353,7 @@ class SaveDraftTest : CoroutinesTest { } @Test - fun saveDraftsReturnsFailureWhenWorkerFailsCreatingDraftOnAPI() { + fun saveDraftsRemovesPendingUploadAndReturnsFailureWhenWorkerFailsCreatingDraftOnAPI() { runBlockingTest { // Given val localDraftId = "8345" @@ -387,12 +387,13 @@ class SaveDraftTest : CoroutinesTest { ).first() // Then + verify { pendingActionsDao.deletePendingUploadByMessageId("45623") } assertEquals(SaveDraftResult.OnlineDraftCreationFailed, result) } } @Test - fun saveDraftsReturnsErrorWhenUploadingNewAttachmentsFails() { + fun saveDraftsRemovesPendingUploadAndReturnsErrorWhenUploadingNewAttachmentsFails() { runBlockingTest { // Given val localDraftId = "8345" @@ -428,6 +429,7 @@ class SaveDraftTest : CoroutinesTest { ).first() // Then + verify { pendingActionsDao.deletePendingUploadByMessageId("45623") } assertEquals(SaveDraftResult.UploadDraftAttachmentsFailed, result) } } From 242aa2bc9b22b2a7a5fa58b632b94ba3c588537b Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Wed, 23 Dec 2020 13:38:17 +0100 Subject: [PATCH 036/145] AttachmentsRepository returns id of uploaded attachment when succeeding - Make updateStoredLocalDraft method in CreateDraftWorker explicitly suspend MAILAND-1281 --- .../android/attachments/AttachmentsRepository.kt | 4 ++-- .../android/attachments/UploadAttachments.kt | 2 +- .../android/worker/drafts/CreateDraftWorker.kt | 4 ++-- .../android/attachments/AttachmentsRepositoryTest.kt | 10 ++++++---- .../android/attachments/UploadAttachmentsTest.kt | 5 +++-- .../protonmail/android/worker/CreateDraftWorkerTest.kt | 2 +- 6 files changed, 15 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/attachments/AttachmentsRepository.kt b/app/src/main/java/ch/protonmail/android/attachments/AttachmentsRepository.kt index 25d01309e..5cccda6c6 100644 --- a/app/src/main/java/ch/protonmail/android/attachments/AttachmentsRepository.kt +++ b/app/src/main/java/ch/protonmail/android/attachments/AttachmentsRepository.kt @@ -128,7 +128,7 @@ class AttachmentsRepository @Inject constructor( attachment.isUploaded = true messageDetailsRepository.saveAttachment(attachment) Timber.i("Upload attachment successful. attachmentId: ${response.attachmentID}") - return@withContext Result.Success + return@withContext Result.Success(response.attachmentID) } Timber.e("Upload attachment failed: ${response.error}") @@ -151,7 +151,7 @@ class AttachmentsRepository @Inject constructor( sealed class Result { - object Success : Result() + data class Success(val uploadedAttachmentId: String) : Result() data class Failure(val error: String) : Result() } diff --git a/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt b/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt index b0ef33f88..7a83bd869 100644 --- a/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt +++ b/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt @@ -54,7 +54,7 @@ class UploadAttachments @Inject constructor( val attachment = messageDetailsRepository.findAttachmentById(attachmentId) if (attachment?.filePath == null || attachment.isUploaded || attachment.doesFileExist.not()) { - Timber.e( + Timber.d( "Skipping attachment: not found, invalid or was already uploaded = ${attachment?.isUploaded}" ) continue diff --git a/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt index 470dfdec3..2ff6af874 100644 --- a/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt +++ b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt @@ -126,7 +126,7 @@ class CreateDraftWorker @WorkerInject constructor( ) } - private fun updateStoredLocalDraft(apiDraft: Message, localDraft: Message) { + private suspend fun updateStoredLocalDraft(apiDraft: Message, localDraft: Message) { apiDraft.apply { dbId = localDraft.dbId toList = localDraft.toList @@ -142,7 +142,7 @@ class CreateDraftWorker @WorkerInject constructor( localId = localDraft.messageId } - messageDetailsRepository.saveMessageInDB(apiDraft) + messageDetailsRepository.saveMessageLocally(apiDraft) } private fun handleFailure(error: String?): Result { diff --git a/app/src/test/java/ch/protonmail/android/attachments/AttachmentsRepositoryTest.kt b/app/src/test/java/ch/protonmail/android/attachments/AttachmentsRepositoryTest.kt index 0ac512ea5..ca14dfe16 100644 --- a/app/src/test/java/ch/protonmail/android/attachments/AttachmentsRepositoryTest.kt +++ b/app/src/test/java/ch/protonmail/android/attachments/AttachmentsRepositoryTest.kt @@ -80,7 +80,7 @@ class AttachmentsRepositoryTest : CoroutinesTest { MockKAnnotations.init(this) val successResponse = mockk { every { code } returns Constants.RESPONSE_CODE_OK - every { attachmentID } returns "" + every { attachmentID } returns "default success attachment ID" every { attachment.keyPackets } returns null every { attachment.signature } returns null } @@ -248,7 +248,8 @@ class AttachmentsRepositoryTest : CoroutinesTest { attachment.isUploaded = true messageDetailsRepository.saveAttachment(attachment) } - assertEquals(AttachmentsRepository.Result.Success, result) + val expected = AttachmentsRepository.Result.Success(apiAttachmentId) + assertEquals(expected, result) } } @@ -299,7 +300,7 @@ class AttachmentsRepositoryTest : CoroutinesTest { fileName = "publickey - EmailAddress(s=message@email.com) - 0xPUBLICKE.asc", mimeType = "application/pgp-keys", messageId = message.messageId!!, - attachmentId = "", + attachmentId = "default success attachment ID", isUploaded = true ) @@ -314,7 +315,8 @@ class AttachmentsRepositoryTest : CoroutinesTest { assertEquals(MediaType.parse("application/pgp-keys"), keyPackageSlot.captured.contentType()) assertEquals(MediaType.parse("application/pgp-keys"), dataPackageSlot.captured.contentType()) assertEquals(MediaType.parse("application/octet-stream"), signatureSlot.captured.contentType()) - assertEquals(AttachmentsRepository.Result.Success, result) + val expected = AttachmentsRepository.Result.Success("default success attachment ID") + assertEquals(expected, result) } } diff --git a/app/src/test/java/ch/protonmail/android/attachments/UploadAttachmentsTest.kt b/app/src/test/java/ch/protonmail/android/attachments/UploadAttachmentsTest.kt index 39d3a43f4..625aed437 100644 --- a/app/src/test/java/ch/protonmail/android/attachments/UploadAttachmentsTest.kt +++ b/app/src/test/java/ch/protonmail/android/attachments/UploadAttachmentsTest.kt @@ -62,6 +62,7 @@ class UploadAttachmentsTest : CoroutinesTest { fun setUp() { MockKAnnotations.init(this) coEvery { attachmentsRepository.upload(any(), crypto) } returns AttachmentsRepository.Result.Success + coEvery { attachmentsRepository.upload(any(), crypto) } returns AttachmentsRepository.Result.Success("8237423") } @Test @@ -254,7 +255,7 @@ class UploadAttachmentsTest : CoroutinesTest { every { userManager.username } returns username every { userManager.getMailSettings(username)?.getAttachPublicKey() } returns true coEvery { attachmentsRepository.uploadPublicKey(message, crypto) } answers { - AttachmentsRepository.Result.Success + AttachmentsRepository.Result.Success("23421") } uploadAttachments(emptyList(), message, crypto) @@ -283,7 +284,7 @@ class UploadAttachmentsTest : CoroutinesTest { every { messageDetailsRepository.findAttachmentById("1") } returns attachmentMock1 every { messageDetailsRepository.findAttachmentById("2") } returns attachmentMock2 coEvery { attachmentsRepository.upload(attachmentMock1, crypto) } answers { - AttachmentsRepository.Result.Success + AttachmentsRepository.Result.Success("234423") } coEvery { attachmentsRepository.upload(attachmentMock2, crypto) } answers { AttachmentsRepository.Result.Failure("failed") diff --git a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt index b5986c037..9eb47050e 100644 --- a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt +++ b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt @@ -577,7 +577,7 @@ class CreateDraftWorkerTest : CoroutinesTest { val result = worker.doWork() // Then - verify { messageDetailsRepository.saveMessageInDB(responseMessage) } + coVerify { messageDetailsRepository.saveMessageLocally(responseMessage) } val expected = ListenableWorker.Result.success( Data.Builder().putString(KEY_OUTPUT_RESULT_SAVE_DRAFT_MESSAGE_ID, "response_message_id").build() ) From 8ea1a4a15a9474f6e293df92f5950ede55874e7f Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Wed, 23 Dec 2020 15:51:29 +0100 Subject: [PATCH 037/145] UploadAttachments saves each uploaded attachment to the message To replicate the logic implemented when updating a draft MAILAND-1281 --- .../android/attachments/UploadAttachments.kt | 27 ++++++++-- .../attachments/UploadAttachmentsTest.kt | 52 +++++++++++++++++++ 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt b/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt index 7a83bd869..1bad44343 100644 --- a/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt +++ b/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt @@ -50,21 +50,26 @@ class UploadAttachments @Inject constructor( withContext(dispatchers.Io) { Timber.i("UploadAttachments started for messageId ${message.messageId} - attachmentIds $attachmentIds") - for (attachmentId in attachmentIds) { + attachmentIds.forEach { attachmentId -> val attachment = messageDetailsRepository.findAttachmentById(attachmentId) if (attachment?.filePath == null || attachment.isUploaded || attachment.doesFileExist.not()) { Timber.d( "Skipping attachment: not found, invalid or was already uploaded = ${attachment?.isUploaded}" ) - continue + return@forEach } attachment.setMessage(message) val result = attachmentsRepository.upload(attachment, crypto) - if (result is AttachmentsRepository.Result.Failure) { - return@withContext Result.Failure(result.error) + when (result) { + is AttachmentsRepository.Result.Success -> { + updateMessageWithUploadedAttachment(message, result.uploadedAttachmentId) + } + is AttachmentsRepository.Result.Failure -> { + return@withContext Result.Failure(result.error) + } } attachment.deleteLocalFile() @@ -82,6 +87,20 @@ class UploadAttachments @Inject constructor( return@withContext Result.Success } + private suspend fun updateMessageWithUploadedAttachment( + message: Message, + uploadedAttachmentId: String + ) { + val uploadedAttachment = messageDetailsRepository.findAttachmentById(uploadedAttachmentId) + uploadedAttachment?.let { + val attachments = message.Attachments.toMutableList() + attachments.removeIf { it.fileName == uploadedAttachment.fileName } + attachments.add(uploadedAttachment) + message.setAttachmentList(attachments) + messageDetailsRepository.saveMessageLocally(message) + } + } + sealed class Result { object Success : Result() data class Failure(val error: String) : Result() diff --git a/app/src/test/java/ch/protonmail/android/attachments/UploadAttachmentsTest.kt b/app/src/test/java/ch/protonmail/android/attachments/UploadAttachmentsTest.kt index 625aed437..e72f89b97 100644 --- a/app/src/test/java/ch/protonmail/android/attachments/UploadAttachmentsTest.kt +++ b/app/src/test/java/ch/protonmail/android/attachments/UploadAttachmentsTest.kt @@ -33,6 +33,7 @@ import io.mockk.every import io.mockk.impl.annotations.InjectMockKs import io.mockk.impl.annotations.RelaxedMockK import io.mockk.mockk +import io.mockk.slot import io.mockk.verify import junit.framework.Assert.assertEquals import kotlinx.coroutines.test.runBlockingTest @@ -63,6 +64,7 @@ class UploadAttachmentsTest : CoroutinesTest { MockKAnnotations.init(this) coEvery { attachmentsRepository.upload(any(), crypto) } returns AttachmentsRepository.Result.Success coEvery { attachmentsRepository.upload(any(), crypto) } returns AttachmentsRepository.Result.Success("8237423") + coEvery { messageDetailsRepository.saveMessageLocally(any()) } returns 823L } @Test @@ -297,6 +299,56 @@ class UploadAttachmentsTest : CoroutinesTest { } } + @Test + fun uploadAttachmentsUpdatesLocalMessageWithUploadedAttachmentWhenUploadSucceeds() { + runBlockingTest { + val attachmentIds = listOf("1", "2") + val messageId = "82342" + val message = Message(messageId = messageId) + val attachmentMock1 = mockk(relaxed = true) { + every { fileName } returns "att1FileName" + every { attachmentId } returns "1" + every { filePath } returns "filePath1" + every { isUploaded } returns false + every { doesFileExist } returns true + } + val attachmentMock2 = mockk(relaxed = true) { + every { fileName } returns "att2FileName" + every { attachmentId } returns "2" + every { filePath } returns "filePath2" + every { isUploaded } returns false + every { doesFileExist } returns true + } + val uploadAttachmentMock = mockk(relaxed = true) { + every { fileName } returns "att2FileName" + every { attachmentId } returns "234092" + every { filePath } returns "filePath2" + every { keyPackets } returns "uploadedPackets" + every { isUploaded } returns true + every { doesFileExist } returns true + } + message.setAttachmentList(listOf(attachmentMock1, attachmentMock2)) + val uploadedAttachmentId = "234092" + every { messageDetailsRepository.findAttachmentById("1") } returns attachmentMock1 + every { messageDetailsRepository.findAttachmentById("2") } returns attachmentMock2 + every { messageDetailsRepository.findAttachmentById(uploadedAttachmentId) } returns uploadAttachmentMock + coEvery { attachmentsRepository.upload(attachmentMock1, crypto) } answers { + AttachmentsRepository.Result.Success(uploadedAttachmentId) + } + coEvery { attachmentsRepository.upload(attachmentMock2, crypto) } answers { + AttachmentsRepository.Result.Failure("Failed uploading") + } + + uploadAttachments(attachmentIds, message, crypto) + + val actualMessage = slot() + val expectedAttachments = listOf(attachmentMock1, uploadAttachmentMock) + verify { messageDetailsRepository.findAttachmentById(uploadedAttachmentId) } + coVerify { messageDetailsRepository.saveMessageLocally(capture(actualMessage)) } + assertEquals(expectedAttachments, actualMessage.captured.Attachments) + } + } + } From 0bb7bbda1400a0f666dd04fe9c8b3c497015ce21 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Wed, 23 Dec 2020 18:04:25 +0100 Subject: [PATCH 038/145] Pass Unread = false to the draft creation / update request So that the draft is created directly as read and we avoid the need to perform an additional `/messages/read` call MAILAND-1281 --- app/src/main/java/ch/protonmail/android/api/utils/Fields.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/ch/protonmail/android/api/utils/Fields.kt b/app/src/main/java/ch/protonmail/android/api/utils/Fields.kt index 3003dff2f..6b217f0ad 100644 --- a/app/src/main/java/ch/protonmail/android/api/utils/Fields.kt +++ b/app/src/main/java/ch/protonmail/android/api/utils/Fields.kt @@ -136,6 +136,7 @@ object Fields { const val TOTAL = "Total" const val SENDER = "Sender" const val ID = "ID" + const val UNREAD = "Unread" object Send { const val EXPIRES_IN = "ExpiresIn" From c5031b80e5d1acc551a3d589774b754e4ca04d09 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Thu, 24 Dec 2020 15:41:34 +0100 Subject: [PATCH 039/145] Create suspend method to updateDraft on API Renaming the existing one to updateDraftBlocking MAILAND-1281 --- .../android/api/ProtonMailApiManager.kt | 12 +++++++++++- .../android/api/segments/message/MessageApi.kt | 10 ++++++++-- .../api/segments/message/MessageApiSpec.kt | 8 +++++++- .../api/segments/message/MessageService.kt | 15 +++++++++++++-- .../android/jobs/UpdateAndPostDraftJob.java | 2 +- .../android/jobs/messages/PostMessageJob.java | 2 +- 6 files changed, 41 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/api/ProtonMailApiManager.kt b/app/src/main/java/ch/protonmail/android/api/ProtonMailApiManager.kt index a429885f0..1dd7791b1 100644 --- a/app/src/main/java/ch/protonmail/android/api/ProtonMailApiManager.kt +++ b/app/src/main/java/ch/protonmail/android/api/ProtonMailApiManager.kt @@ -318,7 +318,17 @@ class ProtonMailApiManager @Inject constructor(var api: ProtonMailApi) : override suspend fun createDraft(draftBody: DraftBody): MessageResponse = api.createDraft(draftBody) - override fun updateDraft(messageId: String, draftBody: DraftBody, retrofitTag: RetrofitTag): MessageResponse? = api.updateDraft(messageId, draftBody, retrofitTag) + override fun updateDraftBlocking( + messageId: String, + draftBody: DraftBody, + retrofitTag: RetrofitTag + ): MessageResponse? = api.updateDraftBlocking(messageId, newMessage, retrofitTag) + + override suspend fun updateDraft( + messageId: String, + draftBody: DraftBody, + retrofitTag: RetrofitTag + ): MessageResponse = api.updateDraft(messageId, newMessage, retrofitTag) override fun sendMessage(messageId: String, message: MessageSendBody, retrofitTag: RetrofitTag): Call = api.sendMessage(messageId, message, retrofitTag) diff --git a/app/src/main/java/ch/protonmail/android/api/segments/message/MessageApi.kt b/app/src/main/java/ch/protonmail/android/api/segments/message/MessageApi.kt index 487d21f55..680dd9041 100644 --- a/app/src/main/java/ch/protonmail/android/api/segments/message/MessageApi.kt +++ b/app/src/main/java/ch/protonmail/android/api/segments/message/MessageApi.kt @@ -130,9 +130,15 @@ class MessageApi(private val service: MessageService) : BaseApi(), MessageApiSpe override suspend fun createDraft(draftBody: DraftBody): MessageResponse = service.createDraft(draftBody) + override suspend fun updateDraft( + messageId: String, + draftBody: DraftBody, + retrofitTag: RetrofitTag + ): MessageResponse = service.updateDraft(messageId, newMessage, retrofitTag) + @Throws(IOException::class) - override fun updateDraft(messageId: String, draftBody: DraftBody, retrofitTag: RetrofitTag): MessageResponse? = - ParseUtils.parse(service.updateDraft(messageId, draftBody, retrofitTag).execute()) + override fun updateDraftBlocking(messageId: String, draftBody: DraftBody, retrofitTag: RetrofitTag): MessageResponse? = + ParseUtils.parse(service.updateDraftCall(messageId, newMessage, retrofitTag).execute()) override fun sendMessage(messageId: String, message: MessageSendBody, retrofitTag: RetrofitTag): Call = service.sendMessage(messageId, message, retrofitTag) diff --git a/app/src/main/java/ch/protonmail/android/api/segments/message/MessageApiSpec.kt b/app/src/main/java/ch/protonmail/android/api/segments/message/MessageApiSpec.kt index dd8275996..1aaf88a60 100644 --- a/app/src/main/java/ch/protonmail/android/api/segments/message/MessageApiSpec.kt +++ b/app/src/main/java/ch/protonmail/android/api/segments/message/MessageApiSpec.kt @@ -97,7 +97,13 @@ interface MessageApiSpec { suspend fun createDraft(draftBody: DraftBody): MessageResponse @Throws(IOException::class) - fun updateDraft(messageId: String, draftBody: DraftBody, retrofitTag: RetrofitTag): MessageResponse? + fun updateDraftBlocking(messageId: String, draftBody: DraftBody, retrofitTag: RetrofitTag): MessageResponse? + + suspend fun updateDraft( + messageId: String, + draftBody: DraftBody, + retrofitTag: RetrofitTag + ): MessageResponse fun sendMessage(messageId: String, message: MessageSendBody, retrofitTag: RetrofitTag): Call diff --git a/app/src/main/java/ch/protonmail/android/api/segments/message/MessageService.kt b/app/src/main/java/ch/protonmail/android/api/segments/message/MessageService.kt index e5271a51b..d38ec31a2 100644 --- a/app/src/main/java/ch/protonmail/android/api/segments/message/MessageService.kt +++ b/app/src/main/java/ch/protonmail/android/api/segments/message/MessageService.kt @@ -115,8 +115,19 @@ interface MessageService { @PUT("mail/v4/messages/{messageId}") @Headers(CONTENT_TYPE, ACCEPT_HEADER_V1) - fun updateDraft(@Path("messageId") messageId: String, - @Body draftBody: DraftBody, @Tag retrofitTag: RetrofitTag): Call + fun updateDraftCall( + @Path("messageId") messageId: String, + @Body draftBody: DraftBody, + @Tag retrofitTag: RetrofitTag + ): Call + + @PUT("mail/v4/messages/{messageId}") + @Headers(CONTENT_TYPE, ACCEPT_HEADER_V1) + fun updateDraft( + @Path("messageId") messageId: String, + @Body draftBody: DraftBody, + @Tag retrofitTag: RetrofitTag + ): MessageResponse @POST("mail/v4/messages/{messageId}") @Headers(CONTENT_TYPE, ACCEPT_HEADER_V1) diff --git a/app/src/main/java/ch/protonmail/android/jobs/UpdateAndPostDraftJob.java b/app/src/main/java/ch/protonmail/android/jobs/UpdateAndPostDraftJob.java index 85dc5efeb..c4ac73536 100644 --- a/app/src/main/java/ch/protonmail/android/jobs/UpdateAndPostDraftJob.java +++ b/app/src/main/java/ch/protonmail/android/jobs/UpdateAndPostDraftJob.java @@ -120,7 +120,7 @@ public void onRun() throws Throwable { if (message.getSenderEmail().contains("+")) { // it's being sent by alias draftBody.getMessage().setSender(new ServerMessageSender(message.getSenderName(), message.getSenderEmail())); } - final MessageResponse draftResponse = getApi().updateDraft(draftBody.getMessage().getId(), draftBody, new RetrofitTag(mUsername)); + final MessageResponse draftResponse = getApi().updateDraftBlocking(draftBody.getMessage().getId(), draftBody, new RetrofitTag(mUsername)); if (draftResponse.getCode() == Constants.RESPONSE_CODE_OK) { getApi().markMessageAsRead(new IDList(Arrays.asList(draftBody.getMessage().getId()))); } else { diff --git a/app/src/main/java/ch/protonmail/android/jobs/messages/PostMessageJob.java b/app/src/main/java/ch/protonmail/android/jobs/messages/PostMessageJob.java index cb16c8c41..2fc18c2f1 100644 --- a/app/src/main/java/ch/protonmail/android/jobs/messages/PostMessageJob.java +++ b/app/src/main/java/ch/protonmail/android/jobs/messages/PostMessageJob.java @@ -329,7 +329,7 @@ private void onRunPostMessage(PendingActionsDatabase pendingActionsDatabase, @No newMessage.getMessage().setSender(new ServerMessageSender(message.getSenderName(), message.getSenderEmail())); } - final MessageResponse draftResponse = getApi().updateDraft(message.getMessageId(), newMessage, new RetrofitTag(mUsername)); + final MessageResponse draftResponse = getApi().updateDraftBlocking(message.getMessageId(), newMessage, new RetrofitTag(mUsername)); EventBuilder eventBuilder = new EventBuilder() .withTag(SENDING_FAILED_TAG, getAppVersion()) .withTag(SENDING_FAILED_DEVICE_TAG, Build.MODEL + " " + Build.VERSION.SDK_INT) From 1c313026fad4bfa9749767f09de80f86fd45950b Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Mon, 28 Dec 2020 10:10:10 +0100 Subject: [PATCH 040/145] CreateDraftWorker calls update draft API when draft exists Using the id of the message to decide, as during the draft creation process the UUID that's set to the local messages gets changed for a server ID (in a different format). So if messageId is not a UUID, the draft was already created MAILAND-1281 --- .../api/segments/message/MessageService.kt | 2 +- .../worker/drafts/CreateDraftWorker.kt | 8 ++- .../android/worker/CreateDraftWorkerTest.kt | 59 +++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/api/segments/message/MessageService.kt b/app/src/main/java/ch/protonmail/android/api/segments/message/MessageService.kt index d38ec31a2..38ce8b0da 100644 --- a/app/src/main/java/ch/protonmail/android/api/segments/message/MessageService.kt +++ b/app/src/main/java/ch/protonmail/android/api/segments/message/MessageService.kt @@ -123,7 +123,7 @@ interface MessageService { @PUT("mail/v4/messages/{messageId}") @Headers(CONTENT_TYPE, ACCEPT_HEADER_V1) - fun updateDraft( + suspend fun updateDraft( @Path("messageId") messageId: String, @Body draftBody: DraftBody, @Tag retrofitTag: RetrofitTag diff --git a/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt index 2ff6af874..3b4f93025 100644 --- a/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt +++ b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt @@ -34,6 +34,7 @@ import androidx.work.WorkerParameters import androidx.work.workDataOf import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository import ch.protonmail.android.api.ProtonMailApiManager +import ch.protonmail.android.api.interceptors.RetrofitTag import ch.protonmail.android.api.models.messages.receive.MessageFactory import ch.protonmail.android.api.models.room.messages.Attachment import ch.protonmail.android.api.models.room.messages.Message @@ -49,6 +50,7 @@ import ch.protonmail.android.crypto.AddressCrypto import ch.protonmail.android.domain.entity.Id import ch.protonmail.android.domain.entity.Name import ch.protonmail.android.domain.entity.user.Address +import ch.protonmail.android.utils.MessageUtils import ch.protonmail.android.utils.base64.Base64Encoder import ch.protonmail.android.utils.extensions.deserialize import ch.protonmail.android.utils.extensions.serialize @@ -106,7 +108,11 @@ class CreateDraftWorker @WorkerInject constructor( createDraftRequest.setSender(buildMessageSender(message, senderAddress)) return runCatching { - apiManager.createDraft(createDraftRequest) + if (MessageUtils.isLocalMessageId(message.messageId)) { + apiManager.createDraft(createDraftRequest) + } else { + apiManager.updateDraft(message.messageId!!, createDraftRequest, RetrofitTag(userManager.username))!! + } }.fold( onSuccess = { response -> if (response.code != Constants.RESPONSE_CODE_OK) { diff --git a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt index 9eb47050e..9b38c7a51 100644 --- a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt +++ b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt @@ -30,6 +30,7 @@ import androidx.work.WorkerParameters import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository import ch.protonmail.android.api.ProtonMailApiManager import ch.protonmail.android.api.models.DraftBody +import ch.protonmail.android.api.interceptors.RetrofitTag import ch.protonmail.android.api.models.messages.ParsedHeaders import ch.protonmail.android.api.models.messages.receive.MessageFactory import ch.protonmail.android.api.models.messages.receive.MessageResponse @@ -497,6 +498,7 @@ class CreateDraftWorkerTest : CoroutinesTest { val messageDbId = 345L val message = Message().apply { dbId = messageDbId + messageId = "ac7b3d53-fc64-4d44-a1f5-39ed45b629ef" addressID = "addressId835" messageBody = "messageBody" sender = MessageSender("sender2342", "senderEmail@2340.com") @@ -551,6 +553,7 @@ class CreateDraftWorkerTest : CoroutinesTest { val message = Message().apply { dbId = messageDbId addressID = "addressId835" + messageId = "ac7b3d53-fc64-4d44-a1f5-39df45b629ef" messageBody = "messageBody" sender = MessageSender("sender2342", "senderEmail@2340.com") setLabelIDs(listOf("label", "label1", "label2")) @@ -653,6 +656,62 @@ class CreateDraftWorkerTest : CoroutinesTest { } } + @Test + fun workerPerformsUpdateDraftRequestWhenMessageIsNotLocal() { + runBlockingTest { + // Given + val parentId = "89345" + val messageDbId = 345L + val remoteMessageId = "7pmfkddyCO69Ch5Gzn0b517H-x-zycdj1Urhn-pj6Eam38FnYY3IxZ62jJ-gbwxVg==" + val message = Message().apply { + dbId = messageDbId + messageId = remoteMessageId + addressID = "addressId835" + messageBody = "messageBody" + sender = MessageSender("sender2342", "senderEmail@2340.com") + setLabelIDs(listOf("label", "label1", "label2")) + parsedHeaders = ParsedHeaders("recEncryption", "recAuth") + numAttachments = 3 + } + + val apiDraftRequest = mockk(relaxed = true) + val responseMessage = mockk(relaxed = true) + val apiDraftResponse = mockk { + every { code } returns 1000 + every { messageId } returns "response_message_id" + every { this@mockk.message } returns responseMessage + } + val retrofitTag = RetrofitTag(userManager.username) + givenMessageIdInput(messageDbId) + givenParentIdInput(parentId) + givenActionTypeInput(NONE) + givenPreviousSenderAddress("") + every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message + every { messageFactory.createDraftApiRequest(message) } returns apiDraftRequest + coEvery { apiManager.updateDraft(remoteMessageId, apiDraftRequest, retrofitTag) } returns apiDraftResponse + + // When + worker.doWork() + + // Then + coVerify { apiManager.updateDraft(remoteMessageId, apiDraftRequest, retrofitTag) } + verifySequence { + responseMessage.dbId = messageDbId + responseMessage.toList = listOf() + responseMessage.ccList = listOf() + responseMessage.bccList = listOf() + responseMessage.replyTos = listOf() + responseMessage.sender = message.sender + responseMessage.setLabelIDs(message.getEventLabelIDs()) + responseMessage.parsedHeaders = message.parsedHeaders + responseMessage.isDownloaded = true + responseMessage.setIsRead(true) + responseMessage.numAttachments = message.numAttachments + responseMessage.localId = message.messageId + } + } + } + private fun givenPreviousSenderAddress(address: String) { every { parameters.inputData.getString(KEY_INPUT_SAVE_DRAFT_PREV_SENDER_ADDR_ID) } answers { address } } From 6f7a2ad47b1ac73356bca3a0c90875dff11486e9 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Mon, 28 Dec 2020 14:20:07 +0100 Subject: [PATCH 041/145] Do not delete local draft after create / update API draft This piece of logic was replicated based on the logic in place beforehand, but it turns out is not needed as when the draft is successfully created / updated the result of the API call is stored to DB _overriding the existing local one._ In case of failure, we do not want to delete the local one instead. - Make CreateDraftWorkerTest explicitly check that the local draft is overridden (write to DB with same DB id) when the create / update request succeeds - As part of the above, `copy` the response's message before updaing it's field to respect object immutability MAILAND-1281 --- .../android/attachments/UploadAttachments.kt | 2 + .../compose/ComposeMessageViewModel.kt | 5 -- .../android/usecase/compose/SaveDraft.kt | 8 -- .../worker/drafts/CreateDraftWorker.kt | 8 +- .../compose/ComposeMessageViewModelTest.kt | 18 ----- .../android/usecase/compose/SaveDraftTest.kt | 41 ---------- .../android/worker/CreateDraftWorkerTest.kt | 75 +++++++++++-------- 7 files changed, 52 insertions(+), 105 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt b/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt index 1bad44343..2aa228885 100644 --- a/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt +++ b/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt @@ -65,9 +65,11 @@ class UploadAttachments @Inject constructor( when (result) { is AttachmentsRepository.Result.Success -> { + Timber.d("UploadAttachment $attachmentId to API for messageId ${message.messageId} Succeeded.") updateMessageWithUploadedAttachment(message, result.uploadedAttachmentId) } is AttachmentsRepository.Result.Failure -> { + Timber.e("UploadAttachment $attachmentId to API for messageId ${message.messageId} FAILED.") return@withContext Result.Failure(result.error) } } diff --git a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt index 54b630a07..a0c1e038c 100644 --- a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt +++ b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt @@ -478,11 +478,6 @@ class ComposeMessageViewModel @Inject constructor( val draft = requireNotNull(messageDetailsRepository.findMessageById(savedDraftId)) viewModelScope.launch(dispatchers.Main) { - if (_draftId.get().isNotEmpty() && draft.messageId.isNullOrEmpty().not()) { - draft.localId?.let { - composeMessageRepository.deleteMessageById(it) - } - } _draftId.set(draft.messageId) watchForMessageSent() } diff --git a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt index 8de2ba1f3..e0ae6905d 100644 --- a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt +++ b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt @@ -105,7 +105,6 @@ class SaveDraft @Inject constructor( ) updatePendingForSendMessage(createdDraftId, localDraftId) - deleteOfflineDraft(localDraftId) messageDetailsRepository.findMessageById(createdDraftId)?.let { val uploadResult = uploadAttachments(params.newAttachmentIds, it, addressCrypto) @@ -125,13 +124,6 @@ class SaveDraft @Inject constructor( .flowOn(dispatchers.Io) } - private suspend fun deleteOfflineDraft(localDraftId: String) { - val offlineDraft = messageDetailsRepository.findMessageById(localDraftId) - offlineDraft?.let { - messageDetailsRepository.deleteMessage(offlineDraft) - } - } - private fun updatePendingForSendMessage(createdDraftId: String, messageId: String) { val pendingForSending = pendingActionsDao.findPendingSendByOfflineMessageId(messageId) pendingForSending?.let { diff --git a/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt index 3b4f93025..b28acb0eb 100644 --- a/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt +++ b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt @@ -111,7 +111,11 @@ class CreateDraftWorker @WorkerInject constructor( if (MessageUtils.isLocalMessageId(message.messageId)) { apiManager.createDraft(createDraftRequest) } else { - apiManager.updateDraft(message.messageId!!, createDraftRequest, RetrofitTag(userManager.username))!! + apiManager.updateDraft( + requireNotNull(message.messageId), + createDraftRequest, + RetrofitTag(userManager.username) + ) } }.fold( onSuccess = { response -> @@ -119,7 +123,7 @@ class CreateDraftWorker @WorkerInject constructor( return handleFailure(response.error) } - val responseDraft = response.message + val responseDraft = response.message.copy() updateStoredLocalDraft(responseDraft, message) Timber.i("Create Draft Worker API call succeeded") Result.success( diff --git a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt index 440db769f..60516e9d4 100644 --- a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt +++ b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt @@ -137,24 +137,6 @@ class ComposeMessageViewModelTest : CoroutinesTest { } } - @Test - fun saveDraftDeletesLocalMessageFromComposerRepositoryWhenSaveDraftUseCaseIsSuccessful() { - runBlockingTest { - val createdDraftId = "newDraftId" - val localDraftId = "localDraftId" - val createdDraft = Message(messageId = createdDraftId, localId = localDraftId) - givenViewModelPropertiesAreInitialised() - coEvery { saveDraft(any()) } returns flowOf(SaveDraftResult.Success(createdDraftId)) - coEvery { messageDetailsRepository.findMessageById(createdDraftId) } returns createdDraft - - // When - viewModel.saveDraft(Message(), hasConnectivity = false) - - // Then - coVerify { composeMessageRepository.deleteMessageById(localDraftId) } - } - } - @Test fun saveDraftObservesMessageInComposeRepositoryToGetNotifiedWhenMessageIsSent() { runBlockingTest { diff --git a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt index b560a2f66..86cc9062f 100644 --- a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt +++ b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt @@ -266,47 +266,6 @@ class SaveDraftTest : CoroutinesTest { } } - @Test - fun saveDraftsDeletesOfflineDraftWhenCreatingRemoteDraftThroughApiSucceds() { - runBlockingTest { - // Given - val localDraftId = "8345" - val message = Message().apply { - dbId = 123L - this.messageId = "45623" - addressID = "addressId" - decryptedBody = "Message body in plain text" - localId = localDraftId - } - coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 9833L - coEvery { messageDetailsRepository.findMessageById("45623") } returns message - coEvery { messageDetailsRepository.findMessageById("createdDraftMessageId345") } returns null - every { pendingActionsDao.findPendingSendByDbId(9833L) } returns null - every { pendingActionsDao.findPendingSendByOfflineMessageId(localDraftId) } returns PendingSend() - coEvery { uploadAttachments(any(), any(), any()) } returns UploadAttachments.Result.Success - val workOutputData = workDataOf( - KEY_OUTPUT_RESULT_SAVE_DRAFT_MESSAGE_ID to "createdDraftMessageId345" - ) - val workerStatusFlow = buildCreateDraftWorkerResponse(WorkInfo.State.SUCCEEDED, workOutputData) - every { - createDraftScheduler.enqueue( - message, - "parentId234", - REPLY_ALL, - "previousSenderId132423" - ) - } answers { workerStatusFlow } - - // When - saveDraft.invoke( - SaveDraftParameters(message, emptyList(), "parentId234", REPLY_ALL, "previousSenderId132423") - ).first() - - // Then - verify { messageDetailsRepository.deleteMessage(message) } - } - } - @Test fun saveDraftsCallsUploadAttachmentsUseCaseToUploadNewAttachments() { runBlockingTest { diff --git a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt index 9b38c7a51..9da4deb8f 100644 --- a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt +++ b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt @@ -72,7 +72,6 @@ import io.mockk.mockk import io.mockk.slot import io.mockk.verify import io.mockk.verifyOrder -import io.mockk.verifySequence import kotlinx.coroutines.test.runBlockingTest import me.proton.core.test.kotlin.CoroutinesTest import org.junit.Assert.assertEquals @@ -508,7 +507,7 @@ class CreateDraftWorkerTest : CoroutinesTest { } val apiDraftRequest = mockk(relaxed = true) - val responseMessage = mockk(relaxed = true) + val responseMessage = Message() val apiDraftResponse = mockk { every { code } returns 1000 every { messageId } returns "response_message_id" @@ -527,25 +526,26 @@ class CreateDraftWorkerTest : CoroutinesTest { // Then coVerify { apiManager.createDraft(apiDraftRequest) } - verifySequence { - responseMessage.dbId = messageDbId - responseMessage.toList = listOf() - responseMessage.ccList = listOf() - responseMessage.bccList = listOf() - responseMessage.replyTos = listOf() - responseMessage.sender = message.sender - responseMessage.setLabelIDs(message.getEventLabelIDs()) - responseMessage.parsedHeaders = message.parsedHeaders - responseMessage.isDownloaded = true - responseMessage.setIsRead(true) - responseMessage.numAttachments = message.numAttachments - responseMessage.localId = message.messageId + val expected = Message().apply { + this.dbId = messageDbId + this.toList = listOf() + this.ccList = listOf() + this.bccList = listOf() + this.replyTos = listOf() + this.sender = message.sender + this.setLabelIDs(message.getEventLabelIDs()) + this.parsedHeaders = message.parsedHeaders + this.isDownloaded = true + this.setIsRead(true) + this.numAttachments = message.numAttachments + this.localId = message.messageId } + coVerify { messageDetailsRepository.saveMessageLocally(expected) } } } @Test - fun workerSavesCreatedDraftToDbAndReturnsSuccessWhenRequestSucceeds() { + fun workerUpdatesLocalDbDraftWithCreatedDraftAndReturnsSuccessWhenRequestSucceeds() { runBlockingTest { // Given val parentId = "89345" @@ -561,8 +561,19 @@ class CreateDraftWorkerTest : CoroutinesTest { numAttachments = 3 } +<<<<<<< HEAD val apiDraftRequest = mockk(relaxed = true) val responseMessage = mockk(relaxed = true) +======= + val apiDraftRequest = mockk(relaxed = true) + val responseMessage = message.copy( + messageId = "response_message_id", + isDownloaded = true, + localId = "ac7b3d53-fc64-4d44-a1f5-39df45b629ef", + Unread = false + ) + responseMessage.dbId = messageDbId +>>>>>>> d9274306 (Do not delete local draft after create / update API draft) val apiDraftResponse = mockk { every { code } returns 1000 every { messageId } returns "response_message_id" @@ -581,6 +592,7 @@ class CreateDraftWorkerTest : CoroutinesTest { // Then coVerify { messageDetailsRepository.saveMessageLocally(responseMessage) } + assertEquals(message.dbId, responseMessage.dbId) val expected = ListenableWorker.Result.success( Data.Builder().putString(KEY_OUTPUT_RESULT_SAVE_DRAFT_MESSAGE_ID, "response_message_id").build() ) @@ -657,7 +669,7 @@ class CreateDraftWorkerTest : CoroutinesTest { } @Test - fun workerPerformsUpdateDraftRequestWhenMessageIsNotLocal() { + fun workerPerformsUpdateDraftRequestAndStoresResponseMessageInDbWhenMessageIsNotLocal() { runBlockingTest { // Given val parentId = "89345" @@ -675,7 +687,7 @@ class CreateDraftWorkerTest : CoroutinesTest { } val apiDraftRequest = mockk(relaxed = true) - val responseMessage = mockk(relaxed = true) + val responseMessage = Message() val apiDraftResponse = mockk { every { code } returns 1000 every { messageId } returns "response_message_id" @@ -695,20 +707,21 @@ class CreateDraftWorkerTest : CoroutinesTest { // Then coVerify { apiManager.updateDraft(remoteMessageId, apiDraftRequest, retrofitTag) } - verifySequence { - responseMessage.dbId = messageDbId - responseMessage.toList = listOf() - responseMessage.ccList = listOf() - responseMessage.bccList = listOf() - responseMessage.replyTos = listOf() - responseMessage.sender = message.sender - responseMessage.setLabelIDs(message.getEventLabelIDs()) - responseMessage.parsedHeaders = message.parsedHeaders - responseMessage.isDownloaded = true - responseMessage.setIsRead(true) - responseMessage.numAttachments = message.numAttachments - responseMessage.localId = message.messageId + val expectedMessage = Message().apply { + this.dbId = messageDbId + this.toList = listOf() + this.ccList = listOf() + this.bccList = listOf() + this.replyTos = listOf() + this.sender = message.sender + this.setLabelIDs(message.getEventLabelIDs()) + this.parsedHeaders = message.parsedHeaders + this.isDownloaded = true + this.setIsRead(true) + this.numAttachments = message.numAttachments + this.localId = message.messageId } + coVerify { messageDetailsRepository.saveMessageLocally(expectedMessage) } } } From 94d301e3d1c41a920916b8bbb347ae03ac16c4a2 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Mon, 28 Dec 2020 14:24:32 +0100 Subject: [PATCH 042/145] Use GloalScope to execute saveDraft use case This is needed to avoid the process of creating / updating a draft to be interrupted because the composeMessageVM context got destroyed (as per out current drafts lifecycle, the update draft process is only initiated when the user leaves the composer choosing "save draft" in the dialog shown) Work is underway to better define how drafts should behave, this is likely to change in the future. MAILAND-1281 --- .../ch/protonmail/android/compose/ComposeMessageViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt index a0c1e038c..6c6b1d220 100644 --- a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt +++ b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt @@ -381,7 +381,7 @@ class ComposeMessageViewModel @Inject constructor( fun saveDraft(message: Message, hasConnectivity: Boolean) { val uploadAttachments = _messageDataResult.uploadAttachments - viewModelScope.launch(dispatchers.Main) { + GlobalScope.launch(dispatchers.Main) { if (_dbId == null) { _dbId = saveMessage(message) message.dbId = _dbId From ea86bb83cb4335061895763c218b14f8e375410d Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Tue, 29 Dec 2020 13:40:33 +0100 Subject: [PATCH 043/145] Allow attachments to be added when draft was not created on API yet This was also the behavior before the saveDraft refactoring took place, as despite the logic in `AddAttachmentsActivity` seems to be in place to avoid it, the `PostMessageServiceFactory` was posting a `DraftCreatedEvent` which when received in `AddAttachmentsActivity` would set mDraftCreated field to true (even if the draft was just local like in the case of no connectivity). The mentioned logic that received the DraftCreatedEvent was deleted by commit 779a328 as part of this refactoring. This same behavior is achieved by not considering `isLocalMessageId` to comptute EXTRA_DRAFT_CREATED boolean. Except for an edge case for which the user clicks the "add attachments" menu immediately after opening the composer, this condition will always be true. Tracked in a task to be completely removed after this refactoring MAILAND-1281 --- .../protonmail/android/activities/AddAttachmentsActivity.java | 4 +--- .../activities/composeMessage/ComposeMessageActivity.java | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/activities/AddAttachmentsActivity.java b/app/src/main/java/ch/protonmail/android/activities/AddAttachmentsActivity.java index e5f63e9a1..502ac4d2b 100644 --- a/app/src/main/java/ch/protonmail/android/activities/AddAttachmentsActivity.java +++ b/app/src/main/java/ch/protonmail/android/activities/AddAttachmentsActivity.java @@ -97,8 +97,6 @@ public class AddAttachmentsActivity extends BaseStoragePermissionActivity implem private static final int REQUEST_CODE_TAKE_PHOTO = 2; private static final String STATE_PHOTO_PATH = "STATE_PATH_TO_PHOTO"; - private MessagesDatabase messagesDatabase; - private AttachmentListAdapter mAdapter; @BindView(R.id.progress_layout) View mProgressLayout; @@ -193,7 +191,7 @@ protected boolean checkForPermissionOnStartup() { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - messagesDatabase = MessagesDatabaseFactory.Companion.getInstance( + MessagesDatabase messagesDatabase = MessagesDatabaseFactory.Companion.getInstance( getApplicationContext()).getDatabase(); ActionBar actionBar = getSupportActionBar(); diff --git a/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java b/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java index 84b49dccb..638b62619 100644 --- a/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java +++ b/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java @@ -2145,7 +2145,7 @@ private class AddAttachmentsObserver implements Observer> public void onChanged(@Nullable List attachments) { String draftId = composeMessageViewModel.getDraftId(); Intent intent = AppUtil.decorInAppIntent(new Intent(ComposeMessageActivity.this, AddAttachmentsActivity.class)); - intent.putExtra(AddAttachmentsActivity.EXTRA_DRAFT_CREATED, !TextUtils.isEmpty(draftId) && !MessageUtils.INSTANCE.isLocalMessageId(draftId)); + intent.putExtra(AddAttachmentsActivity.EXTRA_DRAFT_CREATED, !TextUtils.isEmpty(draftId)); intent.putParcelableArrayListExtra(AddAttachmentsActivity.EXTRA_ATTACHMENT_LIST, new ArrayList<>(attachments)); intent.putExtra(AddAttachmentsActivity.EXTRA_DRAFT_ID, draftId); startActivityForResult(intent, REQUEST_CODE_ADD_ATTACHMENTS); From 299992a8ad1d5015a02a810ceb89f63b8aec3ccf Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Tue, 29 Dec 2020 16:26:39 +0100 Subject: [PATCH 044/145] Make create / update draft API requests unique by messageId This is needed to avoid multiple requests to be enqueued for the same messageId in the event of drafts being created with no connectivity. As by using `enqueueUniqueWork` it does happen that `getWorkInfoByIdLiveData` returns a null WorkInfo, a test was added to ensure CreateDraftWorker.Enqueuer.enqueue returns a `Flow - if (workInfo.state == WorkInfo.State.SUCCEEDED) { + if (workInfo?.state == WorkInfo.State.SUCCEEDED) { val createdDraftId = requireNotNull( workInfo.outputData.getString(KEY_OUTPUT_RESULT_SAVE_DRAFT_MESSAGE_ID) ) diff --git a/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt index b28acb0eb..5c1b4b3da 100644 --- a/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt +++ b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt @@ -26,6 +26,7 @@ import androidx.lifecycle.asFlow import androidx.work.BackoffPolicy import androidx.work.Constraints import androidx.work.CoroutineWorker +import androidx.work.ExistingWorkPolicy import androidx.work.NetworkType import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkInfo @@ -262,7 +263,7 @@ class CreateDraftWorker @WorkerInject constructor( parentId: String?, actionType: Constants.MessageActionType, previousSenderAddressId: String - ): Flow { + ): Flow { val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() @@ -280,7 +281,11 @@ class CreateDraftWorker @WorkerInject constructor( .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, Duration.ofSeconds(TEN_SECONDS)) .build() - workManager.enqueue(createDraftRequest) + workManager.enqueueUniqueWork( + requireNotNull(message.messageId), + ExistingWorkPolicy.REPLACE, + createDraftRequest + ) return workManager.getWorkInfoByIdLiveData(createDraftRequest.id).asFlow() } } diff --git a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt index 86cc9062f..70b56a56e 100644 --- a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt +++ b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt @@ -54,6 +54,7 @@ import io.mockk.verify import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runBlockingTest import me.proton.core.test.kotlin.CoroutinesTest import org.junit.Assert.assertEquals @@ -221,6 +222,34 @@ class SaveDraftTest : CoroutinesTest { } } + @Test + fun saveDraftsIgnoresEmissionsFromCreateDraftWorkerWhenWorkInfoIsNull() { + // This test is needed to ensure CreateDraftWorker is returning a flow of (Optional) WorkInfo? + // as this is possible because of `getWorkInfoByIdLiveData` implementation + runBlockingTest { + // Given + val message = Message().apply { + dbId = 123L + this.messageId = "456" + addressID = "addressId" + decryptedBody = "Message body in plain text" + } + coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 9833L + every { pendingActionsDao.findPendingSendByDbId(9833L) } returns null + every { + createDraftScheduler.enqueue(message, "parentId123", REPLY_ALL, "previousSenderId1273") + } answers { flowOf(null) } + + // When + saveDraft.invoke( + SaveDraftParameters(message, emptyList(), "parentId123", REPLY_ALL, "previousSenderId1273") + ) + + // Then\ + coVerify(exactly = 0) { messageDetailsRepository.findMessageById(any()) } + } + } + @Test fun saveDraftsUpdatesPendingForSendingMessageIdWithNewApiDraftIdWhenWorkerSucceedsAndMessageIsPendingForSending() { runBlockingTest { diff --git a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt index 9da4deb8f..6f2155323 100644 --- a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt +++ b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt @@ -20,11 +20,14 @@ package ch.protonmail.android.worker import android.content.Context +import androidx.lifecycle.liveData import androidx.work.BackoffPolicy import androidx.work.Data +import androidx.work.ExistingWorkPolicy import androidx.work.ListenableWorker import androidx.work.NetworkType import androidx.work.OneTimeWorkRequest +import androidx.work.WorkInfo import androidx.work.WorkManager import androidx.work.WorkerParameters import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository @@ -118,18 +121,20 @@ class CreateDraftWorkerTest : CoroutinesTest { } @Test - fun workerEnqueuerCreatesOneTimeRequestWorkerWithParams() { + fun workerEnqueuerCreatesOneTimeRequestWorkerWhichIsUniqueForMessageId() { runBlockingTest { // Given val messageParentId = "98234" - val messageLocalId = "2834" + val messageId = "2834" val messageDbId = 534L val messageActionType = REPLY_ALL - val message = Message(messageLocalId) + val message = Message(messageId = messageId) message.dbId = messageDbId val previousSenderAddressId = "previousSenderId82348" val requestSlot = slot() - every { workManager.enqueue(capture(requestSlot)) } answers { mockk() } + every { + workManager.enqueueUniqueWork(messageId, ExistingWorkPolicy.REPLACE, capture(requestSlot)) + } answers { mockk() } // When CreateDraftWorker.Enqueuer(workManager).enqueue( From 8fe168ae176bd478a6d9db06142aa460a3439d80 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Wed, 30 Dec 2020 15:42:38 +0100 Subject: [PATCH 045/145] Solve crash caused by `Duration` usage on API < 26 Avoid usage of `setBackoffCriteria(BackoffPolicy, Duration)` as it was only introduced in API 26. MAILAND-1281 --- .../ch/protonmail/android/worker/drafts/CreateDraftWorker.kt | 4 ++-- .../ch/protonmail/android/worker/CreateDraftWorkerTest.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt index 5c1b4b3da..3c67b743e 100644 --- a/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt +++ b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt @@ -57,7 +57,7 @@ import ch.protonmail.android.utils.extensions.deserialize import ch.protonmail.android.utils.extensions.serialize import kotlinx.coroutines.flow.Flow import timber.log.Timber -import java.time.Duration +import java.util.concurrent.TimeUnit import javax.inject.Inject internal const val KEY_INPUT_SAVE_DRAFT_MSG_DB_ID = "keySaveDraftMessageDbId" @@ -278,7 +278,7 @@ class CreateDraftWorker @WorkerInject constructor( KEY_INPUT_SAVE_DRAFT_PREV_SENDER_ADDR_ID to previousSenderAddressId ) ) - .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, Duration.ofSeconds(TEN_SECONDS)) + .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 2 * TEN_SECONDS, TimeUnit.SECONDS) .build() workManager.enqueueUniqueWork( diff --git a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt index 6f2155323..0a18a3163 100644 --- a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt +++ b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt @@ -160,7 +160,7 @@ class CreateDraftWorkerTest : CoroutinesTest { assertEquals(previousSenderAddressId, actualPreviousSenderAddress) assertEquals(NetworkType.CONNECTED, constraints.requiredNetworkType) assertEquals(BackoffPolicy.EXPONENTIAL, workSpec.backoffPolicy) - assertEquals(10000, workSpec.backoffDelayDuration) + assertEquals(20000, workSpec.backoffDelayDuration) verify { workManager.getWorkInfoByIdLiveData(any()) } } } From 75bd0af14c380d63b27f3ef1c53e0b08563d2d1a Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Wed, 30 Dec 2020 17:33:00 +0100 Subject: [PATCH 046/145] Remove usage of `removeIf` method untroduced with Java 8 Because it won't work on android versions with API level < 26 MAILAND-1281 --- .../ch/protonmail/android/attachments/UploadAttachments.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt b/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt index 2aa228885..f0970dabd 100644 --- a/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt +++ b/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt @@ -96,7 +96,9 @@ class UploadAttachments @Inject constructor( val uploadedAttachment = messageDetailsRepository.findAttachmentById(uploadedAttachmentId) uploadedAttachment?.let { val attachments = message.Attachments.toMutableList() - attachments.removeIf { it.fileName == uploadedAttachment.fileName } + attachments + .find { it.fileName == uploadedAttachment.fileName } + ?.let { attachments.remove(it) } attachments.add(uploadedAttachment) message.setAttachmentList(attachments) messageDetailsRepository.saveMessageLocally(message) From a84a9509c27e66fe5ca16ec10a9f0e7ea2046a16 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Wed, 30 Dec 2020 17:38:28 +0100 Subject: [PATCH 047/145] Allow draft to be opened while attachments are being uploaded This solves the edge case mentioned in MAILAND-1301 for which attacahments are not uploaded if app is killed right after requiring a draft's update. They will stay pending for upload and be uploaded once the draft gets re-opened in the app. MAILAND-1301 --- .../android/activities/mailbox/MailboxActivity.kt | 13 +++++-------- .../android/compose/ComposeMessageViewModel.kt | 8 +++++++- app/src/main/res/values/strings.xml | 1 + 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/activities/mailbox/MailboxActivity.kt b/app/src/main/java/ch/protonmail/android/activities/mailbox/MailboxActivity.kt index 467c66552..60d650d27 100644 --- a/app/src/main/java/ch/protonmail/android/activities/mailbox/MailboxActivity.kt +++ b/app/src/main/java/ch/protonmail/android/activities/mailbox/MailboxActivity.kt @@ -1811,20 +1811,17 @@ class MailboxActivity : ) : AsyncTask() { override fun doInBackground(vararg params: Unit): Boolean { - // return if message is not in sending process and can be opened - val pendingUploads = pendingActionsDatabase?.findPendingUploadByMessageId(messageId!!) + // return true if message is not in sending process and can be opened val pendingForSending = pendingActionsDatabase?.findPendingSendByMessageId(messageId!!) - return pendingUploads == null && - ( - pendingForSending == null || - pendingForSending.sent != null && !pendingForSending.sent!! - ) + return pendingForSending == null || + pendingForSending.sent != null && + !pendingForSending.sent!! } override fun onPostExecute(openMessage: Boolean) { val mailboxActivity = mailboxActivity.get() if (!openMessage) { - mailboxActivity?.showToast(R.string.draft_attachments_uploading, Toast.LENGTH_SHORT) + mailboxActivity?.showToast(R.string.cannot_open_message_while_being_sent, Toast.LENGTH_SHORT) return } val intent = AppUtil.decorInAppIntent(Intent(mailboxActivity, ComposeMessageActivity::class.java)) diff --git a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt index 6c6b1d220..339efb464 100644 --- a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt +++ b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt @@ -378,9 +378,13 @@ class ComposeMessageViewModel @Inject constructor( } } + @SuppressLint("GlobalCoroutineUsage") fun saveDraft(message: Message, hasConnectivity: Boolean) { val uploadAttachments = _messageDataResult.uploadAttachments + // This coroutine **needs** to be launched in `GlobalScope` to allow the process of saving a + // draft to complete without depending on this VM's lifecycle. See MAILAND-1301 for more details + // and notes on the plan to remove this GlobalScope usage GlobalScope.launch(dispatchers.Main) { if (_dbId == null) { _dbId = saveMessage(message) @@ -405,7 +409,9 @@ class ComposeMessageViewModel @Inject constructor( _actionId, _oldSenderAddressId ) - ).collect {} + ).collect { + Timber.d("Received saveDraft outcome on VM: $it") + } if (newAttachments.isNotEmpty() && uploadAttachments) { _oldSenderAddressId = message.addressID diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 63a09bd38..562a016d2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1118,4 +1118,5 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Invalid Firebase API key. Push notifications will not work. Failed saving online draft for message %s + Can\'t open this message while it\'s being sent From 63e13a6fa012969af195cae491df58599dcca7b2 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Mon, 4 Jan 2021 10:20:42 +0100 Subject: [PATCH 048/145] Improve UploadAttachments tests - Test attachment is not updated locally when failing to upload on API - Test attachment is correctly updated locally when succeeding to upload on API - Test updated attachment is not added to message if it wasn't in the message before (to keep the same logic that UploadAndPostDraft job currently has) MAILAND-1281 --- .../android/attachments/UploadAttachments.kt | 6 +- .../attachments/UploadAttachmentsTest.kt | 95 +++++++++++++++++-- 2 files changed, 91 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt b/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt index f0970dabd..85d51be02 100644 --- a/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt +++ b/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt @@ -98,8 +98,10 @@ class UploadAttachments @Inject constructor( val attachments = message.Attachments.toMutableList() attachments .find { it.fileName == uploadedAttachment.fileName } - ?.let { attachments.remove(it) } - attachments.add(uploadedAttachment) + ?.let { + attachments.remove(it) + attachments.add(uploadedAttachment) + } message.setAttachmentList(attachments) messageDetailsRepository.saveMessageLocally(message) } diff --git a/app/src/test/java/ch/protonmail/android/attachments/UploadAttachmentsTest.kt b/app/src/test/java/ch/protonmail/android/attachments/UploadAttachmentsTest.kt index e72f89b97..3847ef11e 100644 --- a/app/src/test/java/ch/protonmail/android/attachments/UploadAttachmentsTest.kt +++ b/app/src/test/java/ch/protonmail/android/attachments/UploadAttachmentsTest.kt @@ -305,6 +305,8 @@ class UploadAttachmentsTest : CoroutinesTest { val attachmentIds = listOf("1", "2") val messageId = "82342" val message = Message(messageId = messageId) + val uploadedAttachmentMock1Id = "823472" + val uploadedAttachmentMock2Id = "234092" val attachmentMock1 = mockk(relaxed = true) { every { fileName } returns "att1FileName" every { attachmentId } returns "1" @@ -312,6 +314,13 @@ class UploadAttachmentsTest : CoroutinesTest { every { isUploaded } returns false every { doesFileExist } returns true } + val uploadedAttachmentMock1 = mockk(relaxed = true) { + every { fileName } returns "att1FileName" + every { attachmentId } returns uploadedAttachmentMock1Id + every { filePath } returns "filePath1" + every { isUploaded } returns true + every { doesFileExist } returns true + } val attachmentMock2 = mockk(relaxed = true) { every { fileName } returns "att2FileName" every { attachmentId } returns "2" @@ -319,36 +328,106 @@ class UploadAttachmentsTest : CoroutinesTest { every { isUploaded } returns false every { doesFileExist } returns true } - val uploadAttachmentMock = mockk(relaxed = true) { + val uploadedAttachmentMock2 = mockk(relaxed = true) { every { fileName } returns "att2FileName" - every { attachmentId } returns "234092" + every { attachmentId } returns uploadedAttachmentMock2Id every { filePath } returns "filePath2" every { keyPackets } returns "uploadedPackets" every { isUploaded } returns true every { doesFileExist } returns true } message.setAttachmentList(listOf(attachmentMock1, attachmentMock2)) - val uploadedAttachmentId = "234092" every { messageDetailsRepository.findAttachmentById("1") } returns attachmentMock1 every { messageDetailsRepository.findAttachmentById("2") } returns attachmentMock2 - every { messageDetailsRepository.findAttachmentById(uploadedAttachmentId) } returns uploadAttachmentMock + every { messageDetailsRepository.findAttachmentById(uploadedAttachmentMock1Id) } returns uploadedAttachmentMock1 + every { messageDetailsRepository.findAttachmentById(uploadedAttachmentMock2Id) } returns uploadedAttachmentMock2 coEvery { attachmentsRepository.upload(attachmentMock1, crypto) } answers { - AttachmentsRepository.Result.Success(uploadedAttachmentId) + AttachmentsRepository.Result.Success(uploadedAttachmentMock1Id) } coEvery { attachmentsRepository.upload(attachmentMock2, crypto) } answers { - AttachmentsRepository.Result.Failure("Failed uploading") + AttachmentsRepository.Result.Success(uploadedAttachmentMock2Id) } uploadAttachments(attachmentIds, message, crypto) val actualMessage = slot() - val expectedAttachments = listOf(attachmentMock1, uploadAttachmentMock) - verify { messageDetailsRepository.findAttachmentById(uploadedAttachmentId) } + val expectedAttachments = listOf(uploadedAttachmentMock1, uploadedAttachmentMock2) + verify { messageDetailsRepository.findAttachmentById(uploadedAttachmentMock2Id) } coVerify { messageDetailsRepository.saveMessageLocally(capture(actualMessage)) } assertEquals(expectedAttachments, actualMessage.captured.Attachments) } } + @Test + fun uploadAttachmentsDoesNotUpdateLocalMessageWithUploadedAttachmentWhenUploadFails() { + runBlockingTest { + val attachmentIds = listOf("1") + val messageId = "82342" + val message = Message(messageId = messageId) + val attachmentMock1 = mockk(relaxed = true) { + every { fileName } returns "att1FileName" + every { attachmentId } returns "1" + every { filePath } returns "filePath1" + every { isUploaded } returns false + every { doesFileExist } returns true + } + message.setAttachmentList(listOf(attachmentMock1)) + every { messageDetailsRepository.findAttachmentById("1") } returns attachmentMock1 + coEvery { attachmentsRepository.upload(attachmentMock1, crypto) } answers { + AttachmentsRepository.Result.Failure("Failed uploading attachment!") + } + + uploadAttachments(attachmentIds, message, crypto) + + verify(exactly = 1) { messageDetailsRepository.findAttachmentById("1") } + coVerify(exactly = 0) { messageDetailsRepository.saveMessageLocally(any()) } + } + } + + @Test + fun uploadAttachmentsDoesNotAddUploadedAttachmentToLocalMessageIfAttachmentWasNotInTheMessageAttachments() { + runBlockingTest { + val attachmentIds = listOf("1", "2") + val messageId = "82342" + val message = Message(messageId = messageId) + val attachmentMock1 = mockk(relaxed = true) { + every { fileName } returns "att1FileName" + every { attachmentId } returns "1" + every { filePath } returns "filePath1" + every { isUploaded } returns false + every { doesFileExist } returns true + } + val attachmentMock2 = mockk(relaxed = true) { + every { fileName } returns "att2FileName" + every { attachmentId } returns "2" + every { filePath } returns "filePath2" + every { isUploaded } returns false + every { doesFileExist } returns true + } + val uploadedAttachment1Id = "82372" + val uploadedAttachment2Id = "24832" + message.setAttachmentList(listOf(attachmentMock1)) + every { messageDetailsRepository.findAttachmentById("1") } returns attachmentMock1 + every { messageDetailsRepository.findAttachmentById("2") } returns attachmentMock2 + // Reusing the same mock for the success just to avoid creating new ones + // In production, this would be a different attachment with another ID + every { messageDetailsRepository.findAttachmentById(uploadedAttachment1Id) } returns attachmentMock1 + every { messageDetailsRepository.findAttachmentById(uploadedAttachment2Id) } returns attachmentMock2 + coEvery { attachmentsRepository.upload(attachmentMock1, crypto) } answers { + AttachmentsRepository.Result.Success(uploadedAttachment1Id) + } + coEvery { attachmentsRepository.upload(attachmentMock2, crypto) } answers { + AttachmentsRepository.Result.Success(uploadedAttachment2Id) + } + + uploadAttachments(attachmentIds, message, crypto) + + val actualMessage = slot() + val expectedAttachments = listOf(attachmentMock1) + coVerify(exactly = 2) { messageDetailsRepository.saveMessageLocally(capture(actualMessage)) } + assertEquals(expectedAttachments, actualMessage.captured.Attachments) + } + } } From f73d8f4181a470e6d82036c87b52b84fbdd9daf5 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Mon, 4 Jan 2021 15:59:55 +0100 Subject: [PATCH 049/145] CreateDraftWorker updates pendingUpload with ID of created draft This is needed to ensure attachments upload is being done also when the app is killed immediately after creating a draft with no connectivity. - Local Message attachments are preserved after creating a draft Tech details: As the upload of attachments happens in `saveDraft` use case when the worker's API request finishes, in case of app being killed it won't be called (as only the worker will get executed) causing the "PendingUpload" stored at the beginning of the process to be wrong as it contains the id of the local message instead of the newly created remote one. --- .../worker/drafts/CreateDraftWorker.kt | 18 +++- .../android/worker/CreateDraftWorkerTest.kt | 83 ++++++++++++++++--- 2 files changed, 88 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt index 3c67b743e..5470993bc 100644 --- a/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt +++ b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt @@ -40,6 +40,7 @@ import ch.protonmail.android.api.models.messages.receive.MessageFactory import ch.protonmail.android.api.models.room.messages.Attachment import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.api.models.room.messages.MessageSender +import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDao import ch.protonmail.android.api.segments.TEN_SECONDS import ch.protonmail.android.core.Constants import ch.protonmail.android.core.Constants.MessageActionType.FORWARD @@ -80,7 +81,8 @@ class CreateDraftWorker @WorkerInject constructor( private val userManager: UserManager, private val addressCryptoFactory: AddressCrypto.Factory, private val base64: Base64Encoder, - val apiManager: ProtonMailApiManager + private val apiManager: ProtonMailApiManager, + private val pendingActionsDao: PendingActionsDao ) : CoroutineWorker(context, params) { internal var retries: Int = 0 @@ -108,12 +110,14 @@ class CreateDraftWorker @WorkerInject constructor( createDraftRequest.setMessageBody(encryptedMessage) createDraftRequest.setSender(buildMessageSender(message, senderAddress)) + val messageId = requireNotNull(message.messageId) + return runCatching { if (MessageUtils.isLocalMessageId(message.messageId)) { apiManager.createDraft(createDraftRequest) } else { apiManager.updateDraft( - requireNotNull(message.messageId), + messageId, createDraftRequest, RetrofitTag(userManager.username) ) @@ -126,6 +130,8 @@ class CreateDraftWorker @WorkerInject constructor( val responseDraft = response.message.copy() updateStoredLocalDraft(responseDraft, message) + updatePendingUploadMessage(messageId, responseDraft) + Timber.i("Create Draft Worker API call succeeded") Result.success( workDataOf(KEY_OUTPUT_RESULT_SAVE_DRAFT_MESSAGE_ID to response.messageId) @@ -137,6 +143,14 @@ class CreateDraftWorker @WorkerInject constructor( ) } + private fun updatePendingUploadMessage(messageId: String, responseDraft: Message) { + val pendingForUpload = pendingActionsDao.findPendingUploadByMessageId(messageId) + pendingForUpload?.let { + pendingForUpload.messageId = requireNotNull(responseDraft.messageId) + pendingActionsDao.insertPendingForUpload(pendingForUpload) + } + } + private suspend fun updateStoredLocalDraft(apiDraft: Message, localDraft: Message) { apiDraft.apply { dbId = localDraft.dbId diff --git a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt index 0a18a3163..d084d76ac 100644 --- a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt +++ b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt @@ -20,14 +20,12 @@ package ch.protonmail.android.worker import android.content.Context -import androidx.lifecycle.liveData import androidx.work.BackoffPolicy import androidx.work.Data import androidx.work.ExistingWorkPolicy import androidx.work.ListenableWorker import androidx.work.NetworkType import androidx.work.OneTimeWorkRequest -import androidx.work.WorkInfo import androidx.work.WorkManager import androidx.work.WorkerParameters import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository @@ -40,6 +38,9 @@ import ch.protonmail.android.api.models.messages.receive.MessageResponse import ch.protonmail.android.api.models.room.messages.Attachment import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.api.models.room.messages.MessageSender +import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDao +import ch.protonmail.android.api.models.room.pendingActions.PendingUpload +import ch.protonmail.android.api.utils.Fields.Message.SELF import ch.protonmail.android.core.Constants import ch.protonmail.android.core.Constants.MessageActionType.FORWARD import ch.protonmail.android.core.Constants.MessageActionType.NONE @@ -111,6 +112,9 @@ class CreateDraftWorkerTest : CoroutinesTest { @RelaxedMockK private lateinit var apiManager: ProtonMailApiManager + @RelaxedMockK + private lateinit var pendingActionsDao: PendingActionsDao + @InjectMockKs private lateinit var worker: CreateDraftWorker @@ -194,6 +198,7 @@ class CreateDraftWorkerTest : CoroutinesTest { val messageDbId = 345L val message = Message().apply { dbId = messageDbId + messageId = "17575c27-c3d9-4f3a-9188-02dea1321cc6" addressID = "addressId" messageBody = "messageBody" } @@ -224,6 +229,7 @@ class CreateDraftWorkerTest : CoroutinesTest { val addressId = "addressId" val message = Message().apply { dbId = messageDbId + messageId = "17575c24-c3d9-4f3a-9188-02dea1321cc6" addressID = addressId messageBody = "messageBody" } @@ -266,6 +272,7 @@ class CreateDraftWorkerTest : CoroutinesTest { val addressId = "addressId234" val message = Message().apply { dbId = messageDbId + messageId = "17575c30-c3d9-4f3a-9188-02dea1321cc6" addressID = addressId messageBody = "messageBody2341" sender = MessageSender("sender by alias", "sender+alias@pm.me") @@ -294,6 +301,7 @@ class CreateDraftWorkerTest : CoroutinesTest { val messageDbId = 345L val message = Message().apply { dbId = messageDbId + messageId = "17575c26-c3d9-4f3a-9188-02dea1321cc6" addressID = "addressId835" messageBody = "messageBody" } @@ -353,6 +361,7 @@ class CreateDraftWorkerTest : CoroutinesTest { val messageDbId = 345L val message = Message().apply { dbId = messageDbId + messageId = "17575c25-c3d9-4f3a-9188-02dea1321cc6" addressID = "addressId835" messageBody = "messageBody" } @@ -389,6 +398,7 @@ class CreateDraftWorkerTest : CoroutinesTest { val messageDbId = 345L val message = Message().apply { dbId = messageDbId + messageId = "17575c29-c3d9-4f3a-9188-02dea1321cc6" addressID = "addressId835" messageBody = "messageBody" } @@ -429,6 +439,7 @@ class CreateDraftWorkerTest : CoroutinesTest { val messageDbId = 345L val message = Message().apply { dbId = messageDbId + messageId = "17575c28-c3d9-4f3a-9188-02dea1321cc6" addressID = "addressId835" messageBody = "messageBody" } @@ -474,6 +485,7 @@ class CreateDraftWorkerTest : CoroutinesTest { val messageDbId = 345L val message = Message().apply { dbId = messageDbId + messageId = "17575c31-c3d9-4f3a-9188-02dea1321cc6" addressID = "addressId835" messageBody = "messageBody" } @@ -512,10 +524,10 @@ class CreateDraftWorkerTest : CoroutinesTest { } val apiDraftRequest = mockk(relaxed = true) - val responseMessage = Message() + val responseMessage = Message(messageId = "created_draft_id") val apiDraftResponse = mockk { every { code } returns 1000 - every { messageId } returns "response_message_id" + every { messageId } returns "created_draft_id" every { this@mockk.message } returns responseMessage } givenMessageIdInput(messageDbId) @@ -533,6 +545,7 @@ class CreateDraftWorkerTest : CoroutinesTest { coVerify { apiManager.createDraft(apiDraftRequest) } val expected = Message().apply { this.dbId = messageDbId + this.messageId = "created_draft_id" this.toList = listOf() this.ccList = listOf() this.bccList = listOf() @@ -566,11 +579,7 @@ class CreateDraftWorkerTest : CoroutinesTest { numAttachments = 3 } -<<<<<<< HEAD val apiDraftRequest = mockk(relaxed = true) - val responseMessage = mockk(relaxed = true) -======= - val apiDraftRequest = mockk(relaxed = true) val responseMessage = message.copy( messageId = "response_message_id", isDownloaded = true, @@ -578,7 +587,6 @@ class CreateDraftWorkerTest : CoroutinesTest { Unread = false ) responseMessage.dbId = messageDbId ->>>>>>> d9274306 (Do not delete local draft after create / update API draft) val apiDraftResponse = mockk { every { code } returns 1000 every { messageId } returns "response_message_id" @@ -605,6 +613,56 @@ class CreateDraftWorkerTest : CoroutinesTest { } } + @Test + fun workerUpdatesPendingForUploadActionInDbWithIdOfCreatedDraft() { + runBlockingTest { + // Given + val parentId = "89345" + val messageDbId = 345L + val localMessageId = "ac7b3d53-fc64-4d44-a1f5-39df45b629ef" + val message = Message().apply { + dbId = messageDbId + addressID = "addressId835" + messageId = localMessageId + messageBody = "messageBody" + sender = MessageSender("sender2342", "senderEmail@2340.com") + setLabelIDs(listOf("label", "label1", "label2")) + parsedHeaders = ParsedHeaders("recEncryption", "recAuth") + numAttachments = 3 + } + + val apiDraftRequest = mockk(relaxed = true) + val responseMessage = message.copy( + messageId = "response_message_id", + isDownloaded = true, + localId = localMessageId, + Unread = false + ) + responseMessage.dbId = messageDbId + val apiDraftResponse = mockk { + every { code } returns 1000 + every { messageId } returns "response_message_id" + every { this@mockk.message } returns responseMessage + } + val pendingUpload = PendingUpload(localMessageId) + givenMessageIdInput(messageDbId) + givenParentIdInput(parentId) + givenActionTypeInput(NONE) + givenPreviousSenderAddress("") + every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message + every { messageFactory.createDraftApiRequest(message) } returns apiDraftRequest + coEvery { apiManager.createDraft(apiDraftRequest) } returns apiDraftResponse + every { pendingActionsDao.findPendingUploadByMessageId(localMessageId) } returns pendingUpload + + // When + val result = worker.doWork() + + // Then + val expectedPendingUpload = PendingUpload("response_message_id") + coVerify { pendingActionsDao.insertPendingForUpload(expectedPendingUpload) } + } + } + @Test fun workerRetriesSavingDraftWhenApiRequestFailsAndMaxTriesWereNotReached() { runBlockingTest { @@ -613,6 +671,7 @@ class CreateDraftWorkerTest : CoroutinesTest { val messageDbId = 345L val message = Message().apply { dbId = messageDbId + messageId = "17575c23-c3d9-4f3a-9188-02dea1321cc6" addressID = "addressId835" messageBody = "messageBody" } @@ -647,6 +706,7 @@ class CreateDraftWorkerTest : CoroutinesTest { val messageDbId = 345L val message = Message().apply { dbId = messageDbId + messageId = "17575c22-c3d9-4f3a-9188-02dea1321cc6" addressID = "addressId835" messageBody = "messageBody" } @@ -692,10 +752,10 @@ class CreateDraftWorkerTest : CoroutinesTest { } val apiDraftRequest = mockk(relaxed = true) - val responseMessage = Message() + val responseMessage = Message(messageId = "created_draft_id") val apiDraftResponse = mockk { every { code } returns 1000 - every { messageId } returns "response_message_id" + every { messageId } returns "created_draft_id" every { this@mockk.message } returns responseMessage } val retrofitTag = RetrofitTag(userManager.username) @@ -714,6 +774,7 @@ class CreateDraftWorkerTest : CoroutinesTest { coVerify { apiManager.updateDraft(remoteMessageId, apiDraftRequest, retrofitTag) } val expectedMessage = Message().apply { this.dbId = messageDbId + this.messageId = "created_draft_id" this.toList = listOf() this.ccList = listOf() this.bccList = listOf() From c0532cce125fa5f47f57456ba709ba75645a894d Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Mon, 4 Jan 2021 20:00:24 +0100 Subject: [PATCH 050/145] Solve some compilation errors due to conflicts with DraftBody model MAILAND-1281 --- .../java/ch/protonmail/android/api/ProtonMailApiManager.kt | 6 +++--- .../protonmail/android/api/segments/message/MessageApi.kt | 6 +++--- .../ch/protonmail/android/worker/CreateDraftWorkerTest.kt | 7 +++---- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/api/ProtonMailApiManager.kt b/app/src/main/java/ch/protonmail/android/api/ProtonMailApiManager.kt index 1dd7791b1..6c5b5b029 100644 --- a/app/src/main/java/ch/protonmail/android/api/ProtonMailApiManager.kt +++ b/app/src/main/java/ch/protonmail/android/api/ProtonMailApiManager.kt @@ -37,6 +37,7 @@ import ch.protonmail.android.api.models.CreateUpdateSubscriptionResponse import ch.protonmail.android.api.models.DeleteContactResponse import ch.protonmail.android.api.models.DirectEnabledResponse import ch.protonmail.android.api.models.DonateBody +import ch.protonmail.android.api.models.DraftBody import ch.protonmail.android.api.models.GetPaymentTokenResponse import ch.protonmail.android.api.models.GetSubscriptionResponse import ch.protonmail.android.api.models.HumanVerifyOptionsResponse @@ -51,7 +52,6 @@ import ch.protonmail.android.api.models.MailSettingsResponse import ch.protonmail.android.api.models.MailboxResetBody import ch.protonmail.android.api.models.ModulusResponse import ch.protonmail.android.api.models.MoveToFolderResponse -import ch.protonmail.android.api.models.DraftBody import ch.protonmail.android.api.models.OrganizationResponse import ch.protonmail.android.api.models.PasswordVerifier import ch.protonmail.android.api.models.PaymentMethodResponse @@ -322,13 +322,13 @@ class ProtonMailApiManager @Inject constructor(var api: ProtonMailApi) : messageId: String, draftBody: DraftBody, retrofitTag: RetrofitTag - ): MessageResponse? = api.updateDraftBlocking(messageId, newMessage, retrofitTag) + ): MessageResponse? = api.updateDraftBlocking(messageId, draftBody, retrofitTag) override suspend fun updateDraft( messageId: String, draftBody: DraftBody, retrofitTag: RetrofitTag - ): MessageResponse = api.updateDraft(messageId, newMessage, retrofitTag) + ): MessageResponse = api.updateDraft(messageId, draftBody, retrofitTag) override fun sendMessage(messageId: String, message: MessageSendBody, retrofitTag: RetrofitTag): Call = api.sendMessage(messageId, message, retrofitTag) diff --git a/app/src/main/java/ch/protonmail/android/api/segments/message/MessageApi.kt b/app/src/main/java/ch/protonmail/android/api/segments/message/MessageApi.kt index 680dd9041..23b023f87 100644 --- a/app/src/main/java/ch/protonmail/android/api/segments/message/MessageApi.kt +++ b/app/src/main/java/ch/protonmail/android/api/segments/message/MessageApi.kt @@ -20,9 +20,9 @@ package ch.protonmail.android.api.segments.message import androidx.annotation.WorkerThread import ch.protonmail.android.api.interceptors.RetrofitTag +import ch.protonmail.android.api.models.DraftBody import ch.protonmail.android.api.models.IDList import ch.protonmail.android.api.models.MoveToFolderResponse -import ch.protonmail.android.api.models.DraftBody import ch.protonmail.android.api.models.UnreadTotalMessagesResponse import ch.protonmail.android.api.models.messages.receive.MessageResponse import ch.protonmail.android.api.models.messages.receive.MessagesResponse @@ -134,11 +134,11 @@ class MessageApi(private val service: MessageService) : BaseApi(), MessageApiSpe messageId: String, draftBody: DraftBody, retrofitTag: RetrofitTag - ): MessageResponse = service.updateDraft(messageId, newMessage, retrofitTag) + ): MessageResponse = service.updateDraft(messageId, draftBody, retrofitTag) @Throws(IOException::class) override fun updateDraftBlocking(messageId: String, draftBody: DraftBody, retrofitTag: RetrofitTag): MessageResponse? = - ParseUtils.parse(service.updateDraftCall(messageId, newMessage, retrofitTag).execute()) + ParseUtils.parse(service.updateDraftCall(messageId, draftBody, retrofitTag).execute()) override fun sendMessage(messageId: String, message: MessageSendBody, retrofitTag: RetrofitTag): Call = service.sendMessage(messageId, message, retrofitTag) diff --git a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt index d084d76ac..da0accc62 100644 --- a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt +++ b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt @@ -30,8 +30,8 @@ import androidx.work.WorkManager import androidx.work.WorkerParameters import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository import ch.protonmail.android.api.ProtonMailApiManager -import ch.protonmail.android.api.models.DraftBody import ch.protonmail.android.api.interceptors.RetrofitTag +import ch.protonmail.android.api.models.DraftBody import ch.protonmail.android.api.models.messages.ParsedHeaders import ch.protonmail.android.api.models.messages.receive.MessageFactory import ch.protonmail.android.api.models.messages.receive.MessageResponse @@ -40,7 +40,6 @@ import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.api.models.room.messages.MessageSender import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDao import ch.protonmail.android.api.models.room.pendingActions.PendingUpload -import ch.protonmail.android.api.utils.Fields.Message.SELF import ch.protonmail.android.core.Constants import ch.protonmail.android.core.Constants.MessageActionType.FORWARD import ch.protonmail.android.core.Constants.MessageActionType.NONE @@ -631,7 +630,7 @@ class CreateDraftWorkerTest : CoroutinesTest { numAttachments = 3 } - val apiDraftRequest = mockk(relaxed = true) + val apiDraftRequest = mockk(relaxed = true) val responseMessage = message.copy( messageId = "response_message_id", isDownloaded = true, @@ -751,7 +750,7 @@ class CreateDraftWorkerTest : CoroutinesTest { numAttachments = 3 } - val apiDraftRequest = mockk(relaxed = true) + val apiDraftRequest = mockk(relaxed = true) val responseMessage = Message(messageId = "created_draft_id") val apiDraftResponse = mockk { every { code } returns 1000 From 37766481c9407efee3321f54556c80aa687a5025 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Tue, 5 Jan 2021 10:45:58 +0100 Subject: [PATCH 051/145] Save local message attachments to message after creating draft This is connected to what mentioned in this commit 78698558c163a187f3c MAILAND-1281 --- .../android/worker/drafts/CreateDraftWorker.kt | 1 + .../android/worker/CreateDraftWorkerTest.kt | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt index 5470993bc..9724feaef 100644 --- a/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt +++ b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt @@ -164,6 +164,7 @@ class CreateDraftWorker @WorkerInject constructor( isDownloaded = true setIsRead(true) numAttachments = localDraft.numAttachments + Attachments = localDraft.Attachments localId = localDraft.messageId } diff --git a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt index da0accc62..18a8c4a2f 100644 --- a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt +++ b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt @@ -519,7 +519,8 @@ class CreateDraftWorkerTest : CoroutinesTest { sender = MessageSender("sender2342", "senderEmail@2340.com") setLabelIDs(listOf("label", "label1", "label2")) parsedHeaders = ParsedHeaders("recEncryption", "recAuth") - numAttachments = 3 + numAttachments = 2 + Attachments = listOf(Attachment("235423"), Attachment("823421")) } val apiDraftRequest = mockk(relaxed = true) @@ -555,9 +556,13 @@ class CreateDraftWorkerTest : CoroutinesTest { this.isDownloaded = true this.setIsRead(true) this.numAttachments = message.numAttachments + this.Attachments = message.Attachments this.localId = message.messageId } - coVerify { messageDetailsRepository.saveMessageLocally(expected) } + val actualMessage = slot() + coVerify { messageDetailsRepository.saveMessageLocally(capture(actualMessage)) } + assertEquals(expected, actualMessage.captured) + assertEquals(expected.Attachments, actualMessage.captured.Attachments) } } @@ -747,7 +752,8 @@ class CreateDraftWorkerTest : CoroutinesTest { sender = MessageSender("sender2342", "senderEmail@2340.com") setLabelIDs(listOf("label", "label1", "label2")) parsedHeaders = ParsedHeaders("recEncryption", "recAuth") - numAttachments = 3 + numAttachments = 1 + Attachments = listOf(Attachment(attachmentId = "82374")) } val apiDraftRequest = mockk(relaxed = true) @@ -784,9 +790,13 @@ class CreateDraftWorkerTest : CoroutinesTest { this.isDownloaded = true this.setIsRead(true) this.numAttachments = message.numAttachments + this.Attachments = message.Attachments this.localId = message.messageId } - coVerify { messageDetailsRepository.saveMessageLocally(expectedMessage) } + val actualMessage = slot() + coVerify { messageDetailsRepository.saveMessageLocally(capture(actualMessage)) } + assertEquals(expectedMessage, actualMessage.captured) + assertEquals(expectedMessage.Attachments, actualMessage.captured.Attachments) } } From d9b64760a637164f069a853c157509a13e66d518 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Wed, 6 Jan 2021 11:19:57 +0100 Subject: [PATCH 052/145] Pass Unread = false as part of draft cration / update Setting unread field on DraftBody with the val of the message.Unread field. As the message was just created the field will be false by default MAILAND-1281 --- .../main/java/ch/protonmail/android/api/models/DraftBody.kt | 3 +++ .../java/ch/protonmail/android/api/models/MessagePayload.kt | 4 +++- .../android/api/models/messages/receive/ServerMessage.kt | 3 ++- .../api/models/messages/receive/ServerMessageTest.kt | 6 ++++-- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/api/models/DraftBody.kt b/app/src/main/java/ch/protonmail/android/api/models/DraftBody.kt index 440565025..740faf9e1 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/DraftBody.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/DraftBody.kt @@ -36,6 +36,9 @@ data class DraftBody( @SerializedName(Fields.Message.ACTION) var action = 0 + @SerializedName(Fields.Message.UNREAD) + var unread: Int? = message.unread + @SerializedName(Fields.Message.Send.ATTACHMENT_KEY_PACKETS) var attachmentKeyPackets: MutableMap? = null get() { diff --git a/app/src/main/java/ch/protonmail/android/api/models/MessagePayload.kt b/app/src/main/java/ch/protonmail/android/api/models/MessagePayload.kt index 2c3247fb3..f8e4970ca 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/MessagePayload.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/MessagePayload.kt @@ -37,5 +37,7 @@ data class MessagePayload( @SerializedName(Fields.Message.CC_LIST) var ccList: List? = null, @SerializedName(Fields.Message.BCC_LIST) - var bccList: List? = null + var bccList: List? = null, + @SerializedName(Fields.Message.UNREAD) + var unread: Int? = null ) diff --git a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/ServerMessage.kt b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/ServerMessage.kt index fff7c543e..7ed4faea4 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/ServerMessage.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/ServerMessage.kt @@ -73,6 +73,7 @@ data class ServerMessage( Body, ToList, CCList, - BCCList + BCCList, + Unread ) } diff --git a/app/src/test/java/ch/protonmail/android/api/models/messages/receive/ServerMessageTest.kt b/app/src/test/java/ch/protonmail/android/api/models/messages/receive/ServerMessageTest.kt index 4808703ed..2b5647d14 100644 --- a/app/src/test/java/ch/protonmail/android/api/models/messages/receive/ServerMessageTest.kt +++ b/app/src/test/java/ch/protonmail/android/api/models/messages/receive/ServerMessageTest.kt @@ -35,7 +35,8 @@ class ServerMessageTest { Body = "Body", ToList = listOf(MessageRecipient("User1", "user1@protonmail.com"), MessageRecipient("User2", "user2@pm.me")), CCList = listOf(MessageRecipient("User3", "user3@protonmail.com")), - BCCList = listOf() + BCCList = listOf(), + Unread = 1 ) val actual = serverMessage.toMessagePayload() @@ -47,7 +48,8 @@ class ServerMessageTest { "Body", listOf(MessageRecipient("User1", "user1@protonmail.com"), MessageRecipient("User2", "user2@pm.me")), listOf(MessageRecipient("User3", "user3@protonmail.com")), - listOf() + listOf(), + 1 ) assertEquals(expected, actual) From 9d791fd23f48f47eb0c17ff33562d5a06aabbe3c Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Wed, 6 Jan 2021 12:07:52 +0100 Subject: [PATCH 053/145] Reformat NotificationServer and its interface MAILAND-1281 --- .../notification/INotificationServer.kt | 68 +++++++++---------- .../notification/NotificationServer.kt | 23 +++---- 2 files changed, 44 insertions(+), 47 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/servers/notification/INotificationServer.kt b/app/src/main/java/ch/protonmail/android/servers/notification/INotificationServer.kt index 09fab463c..45d088e49 100644 --- a/app/src/main/java/ch/protonmail/android/servers/notification/INotificationServer.kt +++ b/app/src/main/java/ch/protonmail/android/servers/notification/INotificationServer.kt @@ -18,6 +18,7 @@ */ package ch.protonmail.android.servers.notification +import android.annotation.SuppressLint import android.app.Notification import android.net.Uri import ch.protonmail.android.api.models.User @@ -26,27 +27,32 @@ import ch.protonmail.android.api.models.room.sendingFailedNotifications.SendingF import ch.protonmail.android.core.UserManager import ch.protonmail.android.api.models.room.notifications.Notification as RoomNotification -/** - * Created by Kamil Rajtar on 13.07.18. - */ interface INotificationServer { + fun createCheckingMailboxNotification(): Notification + fun createEmailsChannel(): String + fun createAttachmentsChannel(): String + fun createAccountChannel(): String fun notifyUserLoggedOut(user: User?) - fun notifyVerificationNeeded(user: User?, - messageTitle: String, - messageId: String, - messageInline: Boolean, - messageAddressId: String) + fun notifyVerificationNeeded( + user: User?, + messageTitle: String, + messageId: String, + messageInline: Boolean, + messageAddressId: String + ) - fun notifyAboutAttachment(filename: String, - uri: Uri, - mimeType: String?, - showNotification: Boolean) + fun notifyAboutAttachment( + filename: String, + uri: Uri, + mimeType: String?, + showNotification: Boolean + ) /** * Show a Notification for a SINGLE new Email. This will be called ONLY if there are not other @@ -59,37 +65,31 @@ interface INotificationServer { * @param notificationBody [String] body of the Notification * @param sender [String] name of the sender of the email */ + @SuppressLint("LongParameterList") fun notifySingleNewEmail( - userManager: UserManager, - user: User, - message: Message?, - messageId: String, - notificationBody: String?, - sender: String, - primaryUser: Boolean + userManager: UserManager, + user: User, + message: Message?, + messageId: String, + notificationBody: String?, + sender: String, + primaryUser: Boolean ) - /** - * Show a Notification for MORE THAN ONE unread Emails. This will be called ONLY if there are - * MORE than one unread Notifications - * - * @param user current logged [User] - * @param unreadNotifications[List] of [RoomNotification] to show to the user - */ fun notifyMultipleUnreadEmail( - userManager: UserManager, - user: User, - unreadNotifications: List + userManager: UserManager, + loggedInUser: User, + unreadNotifications: List ) fun notifySingleErrorSendingMessage( - message: Message, - error: String, - user: User + message: Message, + error: String, + user: User ) fun notifyMultipleErrorSendingMessage( - unreadSendingFailedNotifications: List, - user: User + unreadSendingFailedNotifications: List, + user: User ) } diff --git a/app/src/main/java/ch/protonmail/android/servers/notification/NotificationServer.kt b/app/src/main/java/ch/protonmail/android/servers/notification/NotificationServer.kt index ede13b16e..5f61bc696 100644 --- a/app/src/main/java/ch/protonmail/android/servers/notification/NotificationServer.kt +++ b/app/src/main/java/ch/protonmail/android/servers/notification/NotificationServer.kt @@ -60,23 +60,18 @@ import ch.protonmail.android.utils.extensions.showToast import javax.inject.Inject import ch.protonmail.android.api.models.room.notifications.Notification as RoomNotification -// region constants const val CHANNEL_ID_EMAIL = "emails" const val CHANNEL_ID_ONGOING_OPS = "ongoingOperations" const val CHANNEL_ID_ACCOUNT = "account" const val CHANNEL_ID_ATTACHMENTS = "attachments" - const val NOTIFICATION_GROUP_ID_EMAIL = 99 const val NOTIFICATION_ID_SENDING_FAILED = 680 - const val EXTRA_MAILBOX_LOCATION = "mailbox_location" const val EXTRA_USERNAME = "username" -// endregion /** * A class that is responsible for creating notification channels, and creating and showing notifications. */ - class NotificationServer @Inject constructor( private val context: Context, private val notificationManager: NotificationManager @@ -432,20 +427,20 @@ class NotificationServer @Inject constructor( * Show a Notification for MORE THAN ONE unread Emails. This will be called ONLY if there are * MORE than one unread Notifications * - * @param user current logged [User] + * @param loggedInUser current logged [User] * @param unreadNotifications [List] of [RoomNotification] to show to the user */ override fun notifyMultipleUnreadEmail( userManager: UserManager, - user: User, + loggedInUser: User, unreadNotifications: List ) { val currentUserUsername = userManager.username // Create content Intent for open MailboxActivity val contentIntent = Intent(context, MailboxActivity::class.java) - if (currentUserUsername != user.username) { + if (currentUserUsername != loggedInUser.username) { contentIntent.putExtra(EXTRA_SWITCHED_USER, true) - contentIntent.putExtra(EXTRA_SWITCHED_TO_USER, user.username) + contentIntent.putExtra(EXTRA_SWITCHED_TO_USER, loggedInUser.username) } val currentTime = System.currentTimeMillis().toInt() val contentPendingIntent = PendingIntent.getActivity(context, currentTime, contentIntent, 0) @@ -456,7 +451,7 @@ class NotificationServer @Inject constructor( // Create Notification Style val inboxStyle = NotificationCompat.InboxStyle() .setBigContentTitle(notificationTitle) - .setSummaryText(user.username) + .setSummaryText(loggedInUser.username) unreadNotifications.reversed().forEach { notification -> inboxStyle.addLine(createSpannableLine( notification.notificationTitle, notification.notificationBody @@ -464,7 +459,7 @@ class NotificationServer @Inject constructor( } // Create Notification's Builder with the prepared params - val builder = createGenericEmailNotification(user) + val builder = createGenericEmailNotification(loggedInUser) .setContentTitle(notificationTitle) .setContentIntent(contentPendingIntent) .setStyle(inboxStyle) @@ -472,7 +467,7 @@ class NotificationServer @Inject constructor( // Build the Notification val notification = builder.build() - notificationManager.notify(user.username.hashCode(), notification) + notificationManager.notify(loggedInUser.username.hashCode(), notification) } /** @return [Spannable] a single line [Spannable] where [title] is [BOLD] */ @@ -483,7 +478,9 @@ class NotificationServer @Inject constructor( return spannableText } - private fun createSpannableBigText(sendingFailedNotifications: List): Spannable { + private fun createSpannableBigText( + sendingFailedNotifications: List + ): Spannable { val spannableStringBuilder = SpannableStringBuilder() sendingFailedNotifications.reversed().forEach { sendingFailedNotification -> spannableStringBuilder.append(createSpannableLine( From ae0d528d170c5780d0c9b33fb3a4c3c78889ecda Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Wed, 6 Jan 2021 12:51:14 +0100 Subject: [PATCH 054/145] Show error notification when uploading drafts attachments fails - using a notification as a way to ensure errors are shown independently on the app's state (background...) - ComposeMessageViewModel just collects saveDraft request with caring of emitted items as it will be destryed right after invoking the `saveDraft` use case when updting a draft, so result doesn't get here - Introduce ErrorNotifier to grant more flexibility and avoid further dependencies on NotificationServer MAILAND-1281 --- .../android/attachments/UploadAttachments.kt | 1 + .../compose/ComposeMessageViewModel.kt | 4 +- .../android/di/ApplicationModule.kt | 9 +++ .../notification/INotificationServer.kt | 2 + .../notification/NotificationServer.kt | 26 +++++++-- .../android/usecase/compose/SaveDraft.kt | 5 +- .../utils/notifier/AndroidErrorNotifier.kt | 35 ++++++++++++ .../android/utils/notifier/ErrorNotifier.kt | 24 ++++++++ app/src/main/res/values/strings.xml | 2 +- .../android/usecase/compose/SaveDraftTest.kt | 11 +++- .../notifier/AndroidErrorNotifierTest.kt | 55 +++++++++++++++++++ 11 files changed, 162 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/ch/protonmail/android/utils/notifier/AndroidErrorNotifier.kt create mode 100644 app/src/main/java/ch/protonmail/android/utils/notifier/ErrorNotifier.kt create mode 100644 app/src/test/java/ch/protonmail/android/utils/notifier/AndroidErrorNotifierTest.kt diff --git a/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt b/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt index 85d51be02..155dcc3ca 100644 --- a/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt +++ b/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt @@ -79,6 +79,7 @@ class UploadAttachments @Inject constructor( val isAttachPublicKey = userManager.getMailSettings(userManager.username)?.getAttachPublicKey() ?: false if (isAttachPublicKey) { + Timber.i("UploadAttachments attaching publicKey for messageId ${message.messageId}") val result = attachmentsRepository.uploadPublicKey(message, crypto) if (result is AttachmentsRepository.Result.Failure) { diff --git a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt index 339efb464..c7ccd9947 100644 --- a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt +++ b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt @@ -409,9 +409,7 @@ class ComposeMessageViewModel @Inject constructor( _actionId, _oldSenderAddressId ) - ).collect { - Timber.d("Received saveDraft outcome on VM: $it") - } + ).collect() if (newAttachments.isNotEmpty() && uploadAttachments) { _oldSenderAddressId = message.addressID diff --git a/app/src/main/java/ch/protonmail/android/di/ApplicationModule.kt b/app/src/main/java/ch/protonmail/android/di/ApplicationModule.kt index d0521b24a..3418da85c 100644 --- a/app/src/main/java/ch/protonmail/android/di/ApplicationModule.kt +++ b/app/src/main/java/ch/protonmail/android/di/ApplicationModule.kt @@ -50,10 +50,13 @@ import ch.protonmail.android.core.UserManager import ch.protonmail.android.crypto.UserCrypto import ch.protonmail.android.domain.entity.Name import ch.protonmail.android.domain.usecase.DownloadFile +import ch.protonmail.android.servers.notification.NotificationServer import ch.protonmail.android.utils.BuildInfo import ch.protonmail.android.utils.base64.AndroidBase64Encoder import ch.protonmail.android.utils.base64.Base64Encoder import ch.protonmail.android.utils.extensions.app +import ch.protonmail.android.utils.notifier.AndroidErrorNotifier +import ch.protonmail.android.utils.notifier.ErrorNotifier import com.birbit.android.jobqueue.JobManager import com.squareup.inject.assisted.dagger2.AssistedModule import dagger.Module @@ -221,6 +224,12 @@ object ApplicationModule { @Provides fun base64Encoder(): Base64Encoder = AndroidBase64Encoder() + + @Provides + fun errorNotifier( + notificationServer: NotificationServer, + userManager: UserManager + ): ErrorNotifier = AndroidErrorNotifier(notificationServer, userManager) } @Module diff --git a/app/src/main/java/ch/protonmail/android/servers/notification/INotificationServer.kt b/app/src/main/java/ch/protonmail/android/servers/notification/INotificationServer.kt index 45d088e49..44061af73 100644 --- a/app/src/main/java/ch/protonmail/android/servers/notification/INotificationServer.kt +++ b/app/src/main/java/ch/protonmail/android/servers/notification/INotificationServer.kt @@ -92,4 +92,6 @@ interface INotificationServer { unreadSendingFailedNotifications: List, user: User ) + + fun notifySaveDraftError(errorMessage: String, messageSubject: String?, username: String) } diff --git a/app/src/main/java/ch/protonmail/android/servers/notification/NotificationServer.kt b/app/src/main/java/ch/protonmail/android/servers/notification/NotificationServer.kt index 5f61bc696..8adf41829 100644 --- a/app/src/main/java/ch/protonmail/android/servers/notification/NotificationServer.kt +++ b/app/src/main/java/ch/protonmail/android/servers/notification/NotificationServer.kt @@ -66,6 +66,7 @@ const val CHANNEL_ID_ACCOUNT = "account" const val CHANNEL_ID_ATTACHMENTS = "attachments" const val NOTIFICATION_GROUP_ID_EMAIL = 99 const val NOTIFICATION_ID_SENDING_FAILED = 680 +const val NOTIFICATION_ID_SAVE_DRAFT_ERROR = 6812 const val EXTRA_MAILBOX_LOCATION = "mailbox_location" const val EXTRA_USERNAME = "username" @@ -492,7 +493,7 @@ class NotificationServer @Inject constructor( } private fun createGenericErrorSendingMessageNotification( - user: User + username: String ): NotificationCompat.Builder { // Create channel and get id @@ -501,13 +502,13 @@ class NotificationServer @Inject constructor( // Create content Intent to open Drafts val contentIntent = Intent(context, MailboxActivity::class.java) contentIntent.putExtra(EXTRA_MAILBOX_LOCATION, Constants.MessageLocationType.DRAFT.messageLocationTypeValue) - contentIntent.putExtra(EXTRA_USERNAME, user.username) + contentIntent.putExtra(EXTRA_USERNAME, username) val stackBuilder = TaskStackBuilder.create(context) .addParentStack(MailboxActivity::class.java) .addNextIntent(contentIntent) - val contentPendingIntent = stackBuilder.getPendingIntent(user.username.hashCode() + NOTIFICATION_ID_SENDING_FAILED, PendingIntent.FLAG_UPDATE_CURRENT) + val contentPendingIntent = stackBuilder.getPendingIntent(username.hashCode() + NOTIFICATION_ID_SENDING_FAILED, PendingIntent.FLAG_UPDATE_CURRENT) // Set Notification's colors val mainColor = context.getColorCompat(R.color.ocean_blue) @@ -535,7 +536,7 @@ class NotificationServer @Inject constructor( .bigText(error) // Create notification builder - val notificationBuilder = createGenericErrorSendingMessageNotification(user) + val notificationBuilder = createGenericErrorSendingMessageNotification(user.username) .setStyle(bigTextStyle) // Build and show notification @@ -557,11 +558,26 @@ class NotificationServer @Inject constructor( .bigText(createSpannableBigText(unreadSendingFailedNotifications)) // Create notification builder - val notificationBuilder = createGenericErrorSendingMessageNotification(user) + val notificationBuilder = createGenericErrorSendingMessageNotification(user.username) .setStyle(bigTextStyle) // Build and show notification val notification = notificationBuilder.build() notificationManager.notify(user.username.hashCode() + NOTIFICATION_ID_SENDING_FAILED, notification) } + + override fun notifySaveDraftError(errorMessage: String, messageSubject: String?, username: String) { + val title = context.getString(R.string.failed_saving_draft_online, messageSubject) + + val bigTextStyle = NotificationCompat.BigTextStyle() + .setBigContentTitle(title) + .setSummaryText(username) + .bigText(errorMessage) + + val notificationBuilder = createGenericErrorSendingMessageNotification(username) + .setStyle(bigTextStyle) + + val notification = notificationBuilder.build() + notificationManager.notify(username.hashCode() + NOTIFICATION_ID_SAVE_DRAFT_ERROR, notification) + } } diff --git a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt index d46d01e4e..200fb7844 100644 --- a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt +++ b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt @@ -33,6 +33,7 @@ import ch.protonmail.android.crypto.AddressCrypto import ch.protonmail.android.di.CurrentUsername import ch.protonmail.android.domain.entity.Id import ch.protonmail.android.domain.entity.Name +import ch.protonmail.android.utils.notifier.ErrorNotifier import ch.protonmail.android.worker.drafts.CreateDraftWorker import ch.protonmail.android.worker.drafts.KEY_OUTPUT_RESULT_SAVE_DRAFT_MESSAGE_ID import kotlinx.coroutines.flow.Flow @@ -52,7 +53,8 @@ class SaveDraft @Inject constructor( private val pendingActionsDao: PendingActionsDao, private val createDraftWorker: CreateDraftWorker.Enqueuer, @CurrentUsername private val username: String, - val uploadAttachments: UploadAttachments + private val uploadAttachments: UploadAttachments, + private val errorNotifier: ErrorNotifier ) { suspend operator fun invoke( @@ -111,6 +113,7 @@ class SaveDraft @Inject constructor( pendingActionsDao.deletePendingUploadByMessageId(localDraftId) if (uploadResult is UploadAttachments.Result.Failure) { + errorNotifier.showPersistentError(uploadResult.error, localDraft.subject) return@map SaveDraftResult.UploadDraftAttachmentsFailed } return@map SaveDraftResult.Success(createdDraftId) diff --git a/app/src/main/java/ch/protonmail/android/utils/notifier/AndroidErrorNotifier.kt b/app/src/main/java/ch/protonmail/android/utils/notifier/AndroidErrorNotifier.kt new file mode 100644 index 000000000..8d35c855f --- /dev/null +++ b/app/src/main/java/ch/protonmail/android/utils/notifier/AndroidErrorNotifier.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * + * This file is part of ProtonMail. + * + * ProtonMail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonMail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonMail. If not, see https://www.gnu.org/licenses/. + */ + +package ch.protonmail.android.utils.notifier + +import ch.protonmail.android.core.UserManager +import ch.protonmail.android.servers.notification.INotificationServer +import javax.inject.Inject + +class AndroidErrorNotifier @Inject constructor( + private val notificationServer: INotificationServer, + private val userManager: UserManager +) : ErrorNotifier { + + override fun showPersistentError(errorMessage: String, messageSubject: String?) { + notificationServer.notifySaveDraftError(errorMessage, messageSubject, userManager.username) + } + +} diff --git a/app/src/main/java/ch/protonmail/android/utils/notifier/ErrorNotifier.kt b/app/src/main/java/ch/protonmail/android/utils/notifier/ErrorNotifier.kt new file mode 100644 index 000000000..53deb733a --- /dev/null +++ b/app/src/main/java/ch/protonmail/android/utils/notifier/ErrorNotifier.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * + * This file is part of ProtonMail. + * + * ProtonMail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonMail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonMail. If not, see https://www.gnu.org/licenses/. + */ + +package ch.protonmail.android.utils.notifier + +interface ErrorNotifier { + fun showPersistentError(errorMessage: String, messageSubject: String?) +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 562a016d2..9f0ef3d03 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1117,6 +1117,6 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Invalid Firebase API key. Push notifications will not work. - Failed saving online draft for message %s + Failed saving online draft for message: "%s" Can\'t open this message while it\'s being sent diff --git a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt index 70b56a56e..7d9fc52f4 100644 --- a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt +++ b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt @@ -39,6 +39,7 @@ import ch.protonmail.android.crypto.AddressCrypto import ch.protonmail.android.domain.entity.Id import ch.protonmail.android.domain.entity.Name import ch.protonmail.android.usecase.compose.SaveDraft.SaveDraftParameters +import ch.protonmail.android.utils.notifier.ErrorNotifier import ch.protonmail.android.worker.drafts.CreateDraftWorker.Enqueuer import ch.protonmail.android.worker.drafts.CreateDraftWorkerErrors import ch.protonmail.android.worker.drafts.KEY_OUTPUT_RESULT_SAVE_DRAFT_ERROR_ENUM @@ -64,6 +65,9 @@ import kotlin.test.Test class SaveDraftTest : CoroutinesTest { + @RelaxedMockK + private lateinit var errorNotifier: ErrorNotifier + @RelaxedMockK private lateinit var uploadAttachments: UploadAttachments @@ -381,7 +385,7 @@ class SaveDraftTest : CoroutinesTest { } @Test - fun saveDraftsRemovesPendingUploadAndReturnsErrorWhenUploadingNewAttachmentsFails() { + fun saveDraftsRemovesPendingUploadAndShowAndReturnsErrorWhenUploadingNewAttachmentsFails() { runBlockingTest { // Given val localDraftId = "8345" @@ -391,17 +395,19 @@ class SaveDraftTest : CoroutinesTest { addressID = "addressId" decryptedBody = "Message body in plain text" localId = localDraftId + subject = "Message Subject" } val newAttachmentIds = listOf("2345", "453") val workOutputData = workDataOf( KEY_OUTPUT_RESULT_SAVE_DRAFT_MESSAGE_ID to "newDraftId" ) val workerStatusFlow = buildCreateDraftWorkerResponse(WorkInfo.State.SUCCEEDED, workOutputData) + val errorMessage = "Can't upload attachments" coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 9833L coEvery { messageDetailsRepository.findMessageById("newDraftId") } returns message.copy(messageId = "newDraftId") coEvery { messageDetailsRepository.findMessageById("45623") } returns message every { pendingActionsDao.findPendingSendByDbId(9833L) } returns null - coEvery { uploadAttachments(newAttachmentIds, any(), any()) } returns Failure("Can't upload attachments") + coEvery { uploadAttachments(newAttachmentIds, any(), any()) } returns Failure(errorMessage) every { createDraftScheduler.enqueue( message, @@ -418,6 +424,7 @@ class SaveDraftTest : CoroutinesTest { // Then verify { pendingActionsDao.deletePendingUploadByMessageId("45623") } + verify { errorNotifier.showPersistentError(errorMessage, "Message Subject") } assertEquals(SaveDraftResult.UploadDraftAttachmentsFailed, result) } } diff --git a/app/src/test/java/ch/protonmail/android/utils/notifier/AndroidErrorNotifierTest.kt b/app/src/test/java/ch/protonmail/android/utils/notifier/AndroidErrorNotifierTest.kt new file mode 100644 index 000000000..47a71d297 --- /dev/null +++ b/app/src/test/java/ch/protonmail/android/utils/notifier/AndroidErrorNotifierTest.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * + * This file is part of ProtonMail. + * + * ProtonMail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonMail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonMail. If not, see https://www.gnu.org/licenses/. + */ + +package ch.protonmail.android.utils.notifier + +import ch.protonmail.android.core.UserManager +import ch.protonmail.android.servers.notification.INotificationServer +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.impl.annotations.RelaxedMockK +import io.mockk.junit5.MockKExtension +import io.mockk.verify +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(MockKExtension::class) +class AndroidErrorNotifierTest { + + @RelaxedMockK + private lateinit var notificationServer: INotificationServer + + @MockK + private lateinit var userManager: UserManager + + @InjectMockKs + private lateinit var errorNotifier: AndroidErrorNotifier + + @Test + fun errorNotifierCallsNotificationServerToDisplayErrorInPersistentNotification() { + val errorMessage = "Failed uploading attachments" + val subject = "Message subject" + every { userManager.username } returns "loggedInUsername" + + errorNotifier.showPersistentError(errorMessage, subject) + + verify { notificationServer.notifySaveDraftError(errorMessage, subject, "loggedInUsername") } + } +} From 903891ccd100fd0cd21abd7ed3801289144c389e Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Wed, 6 Jan 2021 15:18:30 +0100 Subject: [PATCH 055/145] CreateDraft worker shows notification with error when request fails and there are no retries left. MAILAND-1281 --- .../android/worker/drafts/CreateDraftWorker.kt | 13 ++++++++----- .../android/worker/CreateDraftWorkerTest.kt | 15 ++++++++++++--- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt index 9724feaef..97f2f7099 100644 --- a/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt +++ b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt @@ -56,6 +56,7 @@ import ch.protonmail.android.utils.MessageUtils import ch.protonmail.android.utils.base64.Base64Encoder import ch.protonmail.android.utils.extensions.deserialize import ch.protonmail.android.utils.extensions.serialize +import ch.protonmail.android.utils.notifier.ErrorNotifier import kotlinx.coroutines.flow.Flow import timber.log.Timber import java.util.concurrent.TimeUnit @@ -82,7 +83,8 @@ class CreateDraftWorker @WorkerInject constructor( private val addressCryptoFactory: AddressCrypto.Factory, private val base64: Base64Encoder, private val apiManager: ProtonMailApiManager, - private val pendingActionsDao: PendingActionsDao + private val pendingActionsDao: PendingActionsDao, + private val errorNotifier: ErrorNotifier ) : CoroutineWorker(context, params) { internal var retries: Int = 0 @@ -125,7 +127,7 @@ class CreateDraftWorker @WorkerInject constructor( }.fold( onSuccess = { response -> if (response.code != Constants.RESPONSE_CODE_OK) { - return handleFailure(response.error) + return retryOrFail(response.error, createDraftRequest.message.subject) } val responseDraft = response.message.copy() @@ -138,7 +140,7 @@ class CreateDraftWorker @WorkerInject constructor( ) }, onFailure = { - handleFailure(it.message) + retryOrFail(it.message, createDraftRequest.message.subject) } ) } @@ -171,13 +173,14 @@ class CreateDraftWorker @WorkerInject constructor( messageDetailsRepository.saveMessageLocally(apiDraft) } - private fun handleFailure(error: String?): Result { + private fun retryOrFail(error: String?, messageSubject: String?): Result { if (retries <= SAVE_DRAFT_MAX_RETRIES) { retries++ - Timber.w("Create Draft Worker API call FAILED with error = $error. Retrying...") + Timber.d("Create Draft Worker API call FAILED with error = $error. Retrying...") return Result.retry() } Timber.e("Create Draft Worker API call failed all the retries. error = $error. FAILING") + errorNotifier.showPersistentError(error.orEmpty(), messageSubject) return failureWithError(CreateDraftWorkerErrors.ServerError) } diff --git a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt index 18a8c4a2f..9cb4e356e 100644 --- a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt +++ b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt @@ -56,6 +56,7 @@ import ch.protonmail.android.domain.entity.user.Address import ch.protonmail.android.domain.entity.user.AddressKeys import ch.protonmail.android.utils.base64.Base64Encoder import ch.protonmail.android.utils.extensions.serialize +import ch.protonmail.android.utils.notifier.ErrorNotifier import ch.protonmail.android.worker.drafts.CreateDraftWorker import ch.protonmail.android.worker.drafts.CreateDraftWorkerErrors import ch.protonmail.android.worker.drafts.KEY_INPUT_SAVE_DRAFT_ACTION_TYPE_JSON @@ -84,6 +85,9 @@ import kotlin.test.Test class CreateDraftWorkerTest : CoroutinesTest { + @RelaxedMockK + private lateinit var errorNotifier: ErrorNotifier + @RelaxedMockK private lateinit var context: Context @@ -703,7 +707,7 @@ class CreateDraftWorkerTest : CoroutinesTest { } @Test - fun workerReturnsFailureWithErrorWhenAPIRequestFailsAndMaxTriesWereReached() { + fun workerNotifiesErrorAndReturnsFailureWithErrorWhenAPIRequestFailsAndMaxTriesWereReached() { runBlockingTest { // Given val parentId = "89345" @@ -713,20 +717,25 @@ class CreateDraftWorkerTest : CoroutinesTest { messageId = "17575c22-c3d9-4f3a-9188-02dea1321cc6" addressID = "addressId835" messageBody = "messageBody" + subject = "Subject001" } + val errorMessage = "Error performing request" givenMessageIdInput(messageDbId) givenParentIdInput(parentId) givenActionTypeInput(NONE) givenPreviousSenderAddress("") every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message - every { messageFactory.createDraftApiRequest(message) } returns mockk(relaxed = true) - coEvery { apiManager.createDraft(any()) } throws IOException("Error performing request") + every { messageFactory.createDraftApiRequest(message) } returns mockk(relaxed = true) { + every { this@mockk.message.subject } returns "Subject001" + } + coEvery { apiManager.createDraft(any()) } throws IOException(errorMessage) worker.retries = 11 // When val result = worker.doWork() // Then + verify { errorNotifier.showPersistentError(errorMessage, "Subject001") } val expected = ListenableWorker.Result.failure( Data.Builder().putString( KEY_OUTPUT_RESULT_SAVE_DRAFT_ERROR_ENUM, From 103d27a35fa893296608e1205e6caf0f66c1317a Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Wed, 6 Jan 2021 16:26:16 +0100 Subject: [PATCH 056/145] Use worker's runAttemptCount to handle worker's retry logic As instance variables are reset between workers retries, while it is persistent in workerParameters.runAttemptCount MAILAND-1281 --- .../protonmail/android/worker/drafts/CreateDraftWorker.kt | 5 +---- .../ch/protonmail/android/worker/CreateDraftWorkerTest.kt | 7 +++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt index 97f2f7099..cc0c191c6 100644 --- a/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt +++ b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt @@ -87,8 +87,6 @@ class CreateDraftWorker @WorkerInject constructor( private val errorNotifier: ErrorNotifier ) : CoroutineWorker(context, params) { - internal var retries: Int = 0 - override suspend fun doWork(): Result { val message = messageDetailsRepository.findMessageByMessageDbId(getInputMessageDbId()) ?: return failureWithError(CreateDraftWorkerErrors.MessageNotFound) @@ -174,8 +172,7 @@ class CreateDraftWorker @WorkerInject constructor( } private fun retryOrFail(error: String?, messageSubject: String?): Result { - if (retries <= SAVE_DRAFT_MAX_RETRIES) { - retries++ + if (runAttemptCount <= SAVE_DRAFT_MAX_RETRIES) { Timber.d("Create Draft Worker API call FAILED with error = $error. Retrying...") return Result.retry() } diff --git a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt index 9cb4e356e..f7899426f 100644 --- a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt +++ b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt @@ -663,7 +663,7 @@ class CreateDraftWorkerTest : CoroutinesTest { every { pendingActionsDao.findPendingUploadByMessageId(localMessageId) } returns pendingUpload // When - val result = worker.doWork() + worker.doWork() // Then val expectedPendingUpload = PendingUpload("response_message_id") @@ -694,7 +694,7 @@ class CreateDraftWorkerTest : CoroutinesTest { every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message every { messageFactory.createDraftApiRequest(message) } returns mockk(relaxed = true) coEvery { apiManager.createDraft(any()) } returns errorAPIResponse - worker.retries = 0 + every { parameters.runAttemptCount } returns 0 // When val result = worker.doWork() @@ -702,7 +702,6 @@ class CreateDraftWorkerTest : CoroutinesTest { // Then val expected = ListenableWorker.Result.retry() assertEquals(expected, result) - assertEquals(1, worker.retries) } } @@ -729,7 +728,7 @@ class CreateDraftWorkerTest : CoroutinesTest { every { this@mockk.message.subject } returns "Subject001" } coEvery { apiManager.createDraft(any()) } throws IOException(errorMessage) - worker.retries = 11 + every { parameters.runAttemptCount } returns 11 // When val result = worker.doWork() From 21b01c8d3cca7164e151bf24a36b6a990705d31f Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Wed, 6 Jan 2021 16:55:52 +0100 Subject: [PATCH 057/145] Delete old implementation of Upload Draft and related classes MAILAND-1281 --- .../activities/mailbox/MailboxActivity.kt | 9 - .../messageDetails/MessageDetailsActivity.kt | 10 - .../api/models/room/messages/Attachment.kt | 69 ----- .../api/services/PostMessageServiceFactory.kt | 32 +-- .../android/events/AttachmentFailedEvent.java | 37 --- .../android/jobs/UpdateAndPostDraftJob.java | 240 ------------------ 6 files changed, 1 insertion(+), 396 deletions(-) delete mode 100644 app/src/main/java/ch/protonmail/android/events/AttachmentFailedEvent.java delete mode 100644 app/src/main/java/ch/protonmail/android/jobs/UpdateAndPostDraftJob.java diff --git a/app/src/main/java/ch/protonmail/android/activities/mailbox/MailboxActivity.kt b/app/src/main/java/ch/protonmail/android/activities/mailbox/MailboxActivity.kt index 60d650d27..9420e442e 100644 --- a/app/src/main/java/ch/protonmail/android/activities/mailbox/MailboxActivity.kt +++ b/app/src/main/java/ch/protonmail/android/activities/mailbox/MailboxActivity.kt @@ -127,7 +127,6 @@ import ch.protonmail.android.core.Constants.Prefs.PREF_SWIPE_GESTURES_DIALOG_SHO import ch.protonmail.android.core.Constants.SWIPE_GESTURES_CHANGED_VERSION import ch.protonmail.android.core.ProtonMailApplication import ch.protonmail.android.data.ContactsRepository -import ch.protonmail.android.events.AttachmentFailedEvent import ch.protonmail.android.events.AuthStatus import ch.protonmail.android.events.FetchLabelsEvent import ch.protonmail.android.events.FetchUpdatesEvent @@ -1052,14 +1051,6 @@ class MailboxActivity : networkSnackBarUtil.hideNoConnectionSnackBar() } - @Subscribe - fun onAttachmentFailedEvent(event: AttachmentFailedEvent) { - showToast( - "${getString(R.string.attachment_failed)} ${event.messageSubject}", - Toast.LENGTH_SHORT - ) - } - @Subscribe fun onMailboxLoginEvent(event: MailboxLoginEvent?) { if (event == null) { diff --git a/app/src/main/java/ch/protonmail/android/activities/messageDetails/MessageDetailsActivity.kt b/app/src/main/java/ch/protonmail/android/activities/messageDetails/MessageDetailsActivity.kt index 7e0f1f768..cd87d42f3 100644 --- a/app/src/main/java/ch/protonmail/android/activities/messageDetails/MessageDetailsActivity.kt +++ b/app/src/main/java/ch/protonmail/android/activities/messageDetails/MessageDetailsActivity.kt @@ -70,7 +70,6 @@ import ch.protonmail.android.core.Constants import ch.protonmail.android.core.Constants.MessageActionType import ch.protonmail.android.core.Constants.MessageLocationType.Companion.fromInt import ch.protonmail.android.core.UserManager -import ch.protonmail.android.events.AttachmentFailedEvent import ch.protonmail.android.events.DownloadEmbeddedImagesEvent import ch.protonmail.android.events.DownloadedAttachmentEvent import ch.protonmail.android.events.LogoutEvent @@ -709,15 +708,6 @@ internal class MessageDetailsActivity : onBackPressed() } - @Subscribe - @Suppress("unused") - fun onAttachmentFailedEvent(event: AttachmentFailedEvent) { - showToast( - "${getString(R.string.attachment_failed)} ${event.messageSubject}", - Toast.LENGTH_SHORT - ) - } - private var showActionButtons = false private inner class Copy(private val text: CharSequence?) : MenuItem.OnMenuItemClickListener { diff --git a/app/src/main/java/ch/protonmail/android/api/models/room/messages/Attachment.kt b/app/src/main/java/ch/protonmail/android/api/models/room/messages/Attachment.kt index f3acc1223..6c7736c52 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/room/messages/Attachment.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/room/messages/Attachment.kt @@ -27,17 +27,10 @@ import androidx.room.Entity import androidx.room.Ignore import androidx.room.Index import androidx.room.PrimaryKey -import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository -import ch.protonmail.android.api.ProtonMailApiManager import ch.protonmail.android.api.models.AttachmentHeaders -import ch.protonmail.android.core.Constants -import ch.protonmail.android.crypto.AddressCrypto import ch.protonmail.android.utils.AppUtil import com.google.gson.annotations.Expose import com.google.gson.annotations.SerializedName -import com.proton.gopenpgp.armor.Armor -import okhttp3.MediaType -import okhttp3.RequestBody import java.io.ByteArrayOutputStream import java.io.File import java.io.IOException @@ -154,30 +147,6 @@ data class Attachment @JvmOverloads constructor( embeddedMimeTypes.contains(mimeType) } - @Throws(Exception::class) - @Deprecated( - "To be deleted to avoid logic on Model", - ReplaceWith( - "attachmentRepository.upload(attachment, crypto)", - imports = arrayOf("ch.protonmail.android.attachments.AttachmentRepository") - ) - ) - fun uploadAndSave( - messageDetailsRepository: MessageDetailsRepository, - api: ProtonMailApiManager, - crypto: AddressCrypto - ): String? { - val filePath = filePath - val fileContent = if (URLUtil.isDataUrl(filePath)) { - Base64.decode(filePath!!.split(",").dropLastWhile { it.isEmpty() }.toTypedArray()[1], - Base64.DEFAULT) - } else { - val file = File(filePath!!) - AppUtil.getByteArray(file) - } - return uploadAndSave(messageDetailsRepository, fileContent, api, crypto) - } - fun deleteLocalFile() { if (doesFileExist) { File(filePath).delete() @@ -197,44 +166,6 @@ data class Attachment @JvmOverloads constructor( } } ?: byteArrayOf() - @Throws(Exception::class) - @Deprecated("To be deleted once last usages of the public `uploadAndSave` were removed") - private fun uploadAndSave( - messageDetailsRepository: MessageDetailsRepository, - fileContent: ByteArray, - api: ProtonMailApiManager, - crypto: AddressCrypto - ): String? { - val headers = headers - val bct = crypto.encrypt(fileContent, fileName!!) - val keyPackage = RequestBody.create(MediaType.parse(mimeType!!), bct.keyPacket) - val dataPackage = RequestBody.create(MediaType.parse(mimeType!!), bct.dataPacket) - val signature = RequestBody.create(MediaType.parse("application/octet-stream"), Armor.unarmor(crypto.sign(fileContent))) - val response = - if (headers != null && headers.contentDisposition.contains("inline") && headers.contentId != null) { - var contentID = headers.contentId - val parts = contentID.split("<").dropLastWhile { it.isEmpty() }.toTypedArray() - if (parts.size > 1) { - contentID = parts[1].replace(">", "") - } - api.uploadAttachmentInlineBlocking(this, messageId, contentID, keyPackage, dataPackage, signature) - } else { - api.uploadAttachmentBlocking(this, keyPackage, dataPackage, signature) - } - - if (response.code == Constants.RESPONSE_CODE_OK) { - attachmentId = response.attachmentID - keyPackets = response.attachment.keyPackets - this.signature = response.attachment.signature - isUploaded = true - messageDetailsRepository.saveAttachment(this) - } else { - throw IOException("Attachment upload failed") - } - - return attachmentId - } - companion object { private fun fromLocalAttachment( messagesDatabase: MessagesDatabase, diff --git a/app/src/main/java/ch/protonmail/android/api/services/PostMessageServiceFactory.kt b/app/src/main/java/ch/protonmail/android/api/services/PostMessageServiceFactory.kt index e9cd15102..b1889720c 100644 --- a/app/src/main/java/ch/protonmail/android/api/services/PostMessageServiceFactory.kt +++ b/app/src/main/java/ch/protonmail/android/api/services/PostMessageServiceFactory.kt @@ -24,13 +24,10 @@ import ch.protonmail.android.api.models.SendPreference import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDatabaseFactory import ch.protonmail.android.api.models.room.pendingActions.PendingSend -import ch.protonmail.android.api.models.room.pendingActions.PendingUpload import ch.protonmail.android.core.Constants import ch.protonmail.android.core.ProtonMailApplication -import ch.protonmail.android.core.QueueNetworkUtil import ch.protonmail.android.core.UserManager import ch.protonmail.android.crypto.Crypto -import ch.protonmail.android.jobs.UpdateAndPostDraftJob import ch.protonmail.android.jobs.messages.PostMessageJob import ch.protonmail.android.utils.ServerTime import com.birbit.android.jobqueue.JobManager @@ -46,21 +43,11 @@ import javax.inject.Inject class PostMessageServiceFactory @Inject constructor( private val messageDetailsRepository: MessageDetailsRepository, private val userManager: UserManager, - private val jobManager: JobManager, - private val networkUtil: QueueNetworkUtil + private val jobManager: JobManager ) { private val bgDispatcher: CoroutineDispatcher = Dispatchers.IO - fun startUpdateDraftService(messageId: Long, content: String, newAttachments: List, uploadAttachments: Boolean, oldSenderId: String, username: String = userManager.username) { - // this is temp fix - GlobalScope.launch { - val message = handleMessage(messageId, content, username) ?: return@launch - handleUpdateDraft(message, uploadAttachments, newAttachments, ProtonMailApplication.getApplication()) - jobManager.addJobInBackground(UpdateAndPostDraftJob(messageId, newAttachments, uploadAttachments, oldSenderId, username)) - } - } - fun startSendingMessage(messageDbId: Long, content: String, outsidersPassword: String?, outsidersHint: String?, expiresIn: Long, parentId: String?, actionType: Constants.MessageActionType, newAttachments: List, sendPreferences: ArrayList, oldSenderId: String, username: String = userManager.username) { // this is temp fix @@ -88,17 +75,6 @@ class PostMessageServiceFactory @Inject constructor( return message } - private suspend fun handleUpdateDraft(message: Message, uploadAttachments: Boolean, newAttachments: List, context: Context) { - if (!networkUtil.isConnected()) { - return - } - message.setLabelIDs(listOf(Constants.MessageLocationType.ALL_DRAFT.messageLocationTypeValue.toString(), Constants.MessageLocationType.ALL_MAIL.messageLocationTypeValue.toString(), Constants.MessageLocationType.DRAFT.messageLocationTypeValue.toString())) - messageDetailsRepository.saveMessageLocally(message) - if (uploadAttachments && newAttachments.isNotEmpty()) { - insertPendingUpload(context, message.messageId!!) - } - } - private suspend fun handleSendMessage(context: Context, message: Message) { message.location = Constants.MessageLocationType.ALL_DRAFT.messageLocationTypeValue message.setLabelIDs(listOf(Constants.MessageLocationType.ALL_DRAFT.messageLocationTypeValue.toString(), Constants.MessageLocationType.ALL_MAIL.messageLocationTypeValue.toString(), Constants.MessageLocationType.DRAFT.messageLocationTypeValue.toString())) @@ -115,12 +91,6 @@ class PostMessageServiceFactory @Inject constructor( insertPendingSend(context, message.messageId, message.dbId) } - private suspend fun insertPendingUpload(context: Context, messageId: String) = - withContext(bgDispatcher) { - val actionsDatabase = PendingActionsDatabaseFactory.getInstance(context).getDatabase() - actionsDatabase.insertPendingForUpload(PendingUpload(messageId)) - } - private suspend fun insertPendingSend(context: Context, messageId: String?, messageDbId: Long?) = withContext(bgDispatcher) { val pendingActionsDatabase = PendingActionsDatabaseFactory.getInstance(context).getDatabase() diff --git a/app/src/main/java/ch/protonmail/android/events/AttachmentFailedEvent.java b/app/src/main/java/ch/protonmail/android/events/AttachmentFailedEvent.java deleted file mode 100644 index 9873cdc7e..000000000 --- a/app/src/main/java/ch/protonmail/android/events/AttachmentFailedEvent.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2020 Proton Technologies AG - * - * This file is part of ProtonMail. - * - * ProtonMail is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ProtonMail is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ProtonMail. If not, see https://www.gnu.org/licenses/. - */ -package ch.protonmail.android.events; - -public class AttachmentFailedEvent { - private final String messageId; - private final String messageSubject; - - public AttachmentFailedEvent(String messageId, String messageSubject) { - this.messageId = messageId; - this.messageSubject = messageSubject; - } - - public String getMessageId(){ - return messageId; - } - - public String getMessageSubject() { - return messageSubject; - } -} diff --git a/app/src/main/java/ch/protonmail/android/jobs/UpdateAndPostDraftJob.java b/app/src/main/java/ch/protonmail/android/jobs/UpdateAndPostDraftJob.java deleted file mode 100644 index c4ac73536..000000000 --- a/app/src/main/java/ch/protonmail/android/jobs/UpdateAndPostDraftJob.java +++ /dev/null @@ -1,240 +0,0 @@ -/* - * Copyright (c) 2020 Proton Technologies AG - * - * This file is part of ProtonMail. - * - * ProtonMail is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ProtonMail is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ProtonMail. If not, see https://www.gnu.org/licenses/. - */ -package ch.protonmail.android.jobs; - -import android.text.TextUtils; -import android.util.Base64; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.birbit.android.jobqueue.Params; -import com.birbit.android.jobqueue.RetryConstraint; - -import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import ch.protonmail.android.api.interceptors.RetrofitTag; -import ch.protonmail.android.api.models.IDList; -import ch.protonmail.android.api.models.DraftBody; -import ch.protonmail.android.api.models.User; -import ch.protonmail.android.api.models.address.Address; -import ch.protonmail.android.api.models.messages.receive.AttachmentFactory; -import ch.protonmail.android.api.models.messages.receive.MessageFactory; -import ch.protonmail.android.api.models.messages.receive.MessageResponse; -import ch.protonmail.android.api.models.messages.receive.MessageSenderFactory; -import ch.protonmail.android.api.models.messages.receive.ServerMessage; -import ch.protonmail.android.api.models.messages.receive.ServerMessageSender; -import ch.protonmail.android.api.models.room.messages.Attachment; -import ch.protonmail.android.api.models.room.messages.Message; -import ch.protonmail.android.api.models.room.messages.MessageSender; -import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDatabase; -import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDatabaseFactory; -import ch.protonmail.android.api.utils.Fields; -import ch.protonmail.android.core.Constants; -import ch.protonmail.android.crypto.AddressCrypto; -import ch.protonmail.android.crypto.Crypto; -import ch.protonmail.android.domain.entity.user.AddressKeys; -import ch.protonmail.android.events.AttachmentFailedEvent; -import ch.protonmail.android.utils.AppUtil; -import ch.protonmail.android.utils.Logger; - -public class UpdateAndPostDraftJob extends ProtonMailBaseJob { - - private static final String TAG_UPDATE_AND_POST_DRAFT_JOB = "UpdateAndPostDraftJob"; - private static final int UPDATE_DRAFT_RETRY_LIMIT = 10; - - private Long mMessageDbId; - private final List mNewAttachments; - private final boolean mUploadAttachments; - private final String mOldSenderAddressID; - private final String mUsername; - - public UpdateAndPostDraftJob(@NonNull Long messageDbId, @NonNull List newAttachments, - boolean uploadAttachments, String oldSenderId, String username) { - super(new Params(Priority.HIGH).requireNetwork().persist().groupBy(Constants.JOB_GROUP_SENDING)); - mMessageDbId = messageDbId; - mNewAttachments = newAttachments; - mUploadAttachments = uploadAttachments; - mOldSenderAddressID = oldSenderId; - mUsername = username; - } - - @Override - protected int getRetryLimit() { - return UPDATE_DRAFT_RETRY_LIMIT; - } - - @Override - protected RetryConstraint shouldReRunOnThrowable(@NonNull Throwable throwable, int runCount, int maxRunCount) { - PendingActionsDatabase pendingActionsDatabase = PendingActionsDatabaseFactory.Companion.getInstance( - getApplicationContext(), mUsername).getDatabase(); - - return RetryConstraint.CANCEL; - } - - @Override - public void onRun() throws Throwable { - getMessageDetailsRepository().reloadDependenciesForUser(mUsername); - PendingActionsDatabase pendingActionsDatabase = PendingActionsDatabaseFactory.Companion.getInstance(getApplicationContext(), mUsername).getDatabase(); - - Message message = getMessageDetailsRepository().findMessageByMessageDbId(mMessageDbId); - if (message == null) { - // todo show error to the user - return; - } - String addressId = message.getAddressID(); - AttachmentFactory attachmentFactory = new AttachmentFactory(); - MessageSenderFactory messageSenderFactory = new MessageSenderFactory(); - MessageFactory messageFactory = new MessageFactory(attachmentFactory, messageSenderFactory); - final ServerMessage serverMessage = messageFactory.createServerMessage(message); - final DraftBody draftBody = new DraftBody(serverMessage); - String encryptedMessage = message.getMessageBody(); - - User user = getUserManager().getUser(mUsername); - Address senderAddress = user.getAddressById(addressId); - draftBody.getMessage().setSender(new ServerMessageSender(senderAddress.getDisplayName(), senderAddress.getEmail())); - - draftBody.getMessage().setBody(encryptedMessage); - updateAttachmentKeyPackets(mNewAttachments, draftBody, mOldSenderAddressID, senderAddress); - if (message.getSenderEmail().contains("+")) { // it's being sent by alias - draftBody.getMessage().setSender(new ServerMessageSender(message.getSenderName(), message.getSenderEmail())); - } - final MessageResponse draftResponse = getApi().updateDraftBlocking(draftBody.getMessage().getId(), draftBody, new RetrofitTag(mUsername)); - if (draftResponse.getCode() == Constants.RESPONSE_CODE_OK) { - getApi().markMessageAsRead(new IDList(Arrays.asList(draftBody.getMessage().getId()))); - } else { - pendingActionsDatabase.deletePendingUploadByMessageId(message.getMessageId()); - return; - } - message.setLabelIDs(draftResponse.getMessage().getEventLabelIDs()); - message.setIsRead(true); - message.setDownloaded(true); - message.setLocation(Constants.MessageLocationType.DRAFT.getMessageLocationTypeValue()); - saveMessage(message, pendingActionsDatabase); - } - - @Override - protected void onProtonCancel(int cancelReason, @Nullable Throwable throwable) { - getMessageDetailsRepository().reloadDependenciesForUser(mUsername); - Message message = getMessageDetailsRepository().findMessageByMessageDbId(mMessageDbId); - if (message == null) { - return; - } - PendingActionsDatabase actionsDatabase = PendingActionsDatabaseFactory.Companion.getInstance(getApplicationContext(), mUsername).getDatabase(); - actionsDatabase.deletePendingUploadByMessageId(message.getMessageId()); - } - - private void saveMessage(Message message, PendingActionsDatabase pendingActionsDatabase) { - AddressCrypto addressCrypto = Crypto.forAddress(getUserManager(), mUsername, message.getAddressID()); - Set currentAttachments = new HashSet<>(message.getAttachments()); - List updatedAtt = updateDraft(pendingActionsDatabase, addressCrypto, message.getMessageId()); - for (Attachment updatedAttachment : updatedAtt) { - boolean found = false; - Attachment att = null; - for (Attachment currentAttachment : currentAttachments) { - if (currentAttachment.getFileName().equals(updatedAttachment.getFileName())) { - att = currentAttachment; - found = true; - break; - } - } - if (found) { - currentAttachments.remove(att); - currentAttachments.add(updatedAttachment); - } - } - message.setAttachmentList(new ArrayList<>(currentAttachments)); - getMessageDetailsRepository().saveMessageInDB(message); - } - - private void updateAttachmentKeyPackets(List attachmentList, DraftBody draftBody, String oldSenderAddress, Address newSenderAddress) throws Exception { - if (!TextUtils.isEmpty(oldSenderAddress)) { - AddressCrypto oldCrypto = Crypto.forAddress(getUserManager(), mUsername, oldSenderAddress); - AddressKeys newAddressKeys = newSenderAddress.toNewAddress().getKeys(); - String newPublicKey = oldCrypto.buildArmoredPublicKey(newAddressKeys.getPrimaryKey().getPrivateKey()); - for (String attachmentId : attachmentList) { - Attachment attachment = getMessageDetailsRepository().findAttachmentById(attachmentId); - String AttachmentID = attachment.getAttachmentId(); - String keyPackets = attachment.getKeyPackets(); - if (TextUtils.isEmpty(keyPackets)) { - continue; - } - try { - byte[] keyPackage = Base64.decode(keyPackets, Base64.DEFAULT); - byte[] sessionKey = oldCrypto.decryptKeyPacket(keyPackage); - byte[] newKeyPackage = oldCrypto.encryptKeyPacket(sessionKey, newPublicKey); - String newKeyPackets = Base64.encodeToString(newKeyPackage, Base64.NO_WRAP); - if (!TextUtils.isEmpty(keyPackets)) { - draftBody.getAttachmentKeyPackets().put(AttachmentID, newKeyPackets); - } - } catch (Exception e) { - if (!TextUtils.isEmpty(keyPackets)) { - draftBody.getAttachmentKeyPackets().put(AttachmentID, keyPackets); - } - Logger.doLogException(e); - } - } - } - } - - private ArrayList updateDraft(PendingActionsDatabase pendingActionsDatabase, AddressCrypto addressCrypto, String messageId) { - ArrayList updatedAttachments = new ArrayList<>(); - Message message = getMessageDetailsRepository().findMessageByMessageDbId(mMessageDbId); - if (message != null && mUploadAttachments && (mNewAttachments != null && mNewAttachments.size() > 0)) { - String mMessageId = message.getMessageId(); - for (String attachmentId : mNewAttachments) { - Attachment attachment = getMessageDetailsRepository().findAttachmentById(attachmentId); - try { - if (attachment == null) { - continue; - } - if (attachment.getFilePath() == null) { - continue; - } - if (attachment.isUploaded()) { - continue; - } - String filePath = attachment.getFilePath(); - if (TextUtils.isEmpty(filePath)) { - continue; - } - final File file = new File(attachment.getFilePath()); - if (!file.exists()) { - continue; - } - // this is just a hack until new complete composer refactoring is done for some of the next versions - attachment.setMessageId(messageId); - attachment.setAttachmentId(attachment.uploadAndSave(getMessageDetailsRepository(), getApi(), addressCrypto)); - updatedAttachments.add(attachment); - } catch (Exception e) { - Logger.doLogException(TAG_UPDATE_AND_POST_DRAFT_JOB, "error while attaching file: " + attachment.getFilePath(), e); - AppUtil.postEventOnUi(new AttachmentFailedEvent(message.getMessageId(), - message.getSubject())); - } - } - pendingActionsDatabase.deletePendingUploadByMessageId(mMessageId); - } - return updatedAttachments; - } -} From 39c2097eb0da140ab90a841651ac22c7b8b4f9f8 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Wed, 6 Jan 2021 17:12:16 +0100 Subject: [PATCH 058/145] Fix ComposeMessageViewModel tests MAILAND-1281 --- .../android/compose/ComposeMessageViewModelTest.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt index 60516e9d4..abc02e405 100644 --- a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt +++ b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt @@ -86,9 +86,14 @@ class ComposeMessageViewModelTest : CoroutinesTest { @InjectMockKs lateinit var viewModel: ComposeMessageViewModel +<<<<<<< HEAD @BeforeTest fun setUp() { MockKAnnotations.init(this) +======= + @BeforeEach + fun setUp() { +>>>>>>> d30d7302 (Fix ComposeMessageViewModel tests) // To avoid `EmptyList` to be returned by Mockk automatically as that causes // UnsupportedOperationException: Operation is not supported for read-only collection // when trying to add elements (in prod we ArrayList so this doesn't happen) From 98d05ca8a4d62845686df41f3e772b096eaaf32f Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Wed, 6 Jan 2021 17:57:08 +0100 Subject: [PATCH 059/145] Simplify attachmentKeyPackets initialization in DraftBody MAILAND-1281 --- .../java/ch/protonmail/android/api/models/DraftBody.kt | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/api/models/DraftBody.kt b/app/src/main/java/ch/protonmail/android/api/models/DraftBody.kt index 740faf9e1..6d7e0c62c 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/DraftBody.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/DraftBody.kt @@ -40,13 +40,7 @@ data class DraftBody( var unread: Int? = message.unread @SerializedName(Fields.Message.Send.ATTACHMENT_KEY_PACKETS) - var attachmentKeyPackets: MutableMap? = null - get() { - if (field == null) { - field = hashMapOf() - } - return field - } + var attachmentKeyPackets: MutableMap = hashMapOf() fun setSender(messageSender: MessageSender) { message.sender = ServerMessageSender(messageSender.name, messageSender.emailAddress) From b5393d8544a9ac5b2aa543d1797e8166df8095e4 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Thu, 7 Jan 2021 13:10:29 +0100 Subject: [PATCH 060/145] Create Draft doesn't retry when API returns bad response code MAILAND-1281 --- .../worker/drafts/CreateDraftWorker.kt | 4 +- .../worker/drafts/CreateDraftWorkerErrors.kt | 3 +- .../android/worker/CreateDraftWorkerTest.kt | 53 +++++++++++++++++-- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt index cc0c191c6..ccd33de8d 100644 --- a/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt +++ b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt @@ -125,7 +125,9 @@ class CreateDraftWorker @WorkerInject constructor( }.fold( onSuccess = { response -> if (response.code != Constants.RESPONSE_CODE_OK) { - return retryOrFail(response.error, createDraftRequest.message.subject) + Timber.e("Create Draft Worker Failed with bad response code: $response") + errorNotifier.showPersistentError(response.error, createDraftRequest.message.subject) + return failureWithError(CreateDraftWorkerErrors.BadResponseCodeError) } val responseDraft = response.message.copy() diff --git a/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorkerErrors.kt b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorkerErrors.kt index b2c33a63b..ac94bc24e 100644 --- a/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorkerErrors.kt +++ b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorkerErrors.kt @@ -21,5 +21,6 @@ package ch.protonmail.android.worker.drafts enum class CreateDraftWorkerErrors { MessageNotFound, - ServerError + ServerError, + BadResponseCodeError } diff --git a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt index f7899426f..972b2fa4d 100644 --- a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt +++ b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt @@ -672,33 +672,76 @@ class CreateDraftWorkerTest : CoroutinesTest { } @Test - fun workerRetriesSavingDraftWhenApiRequestFailsAndMaxTriesWereNotReached() { + fun workerReturnsFailureWithoutRetryingWhenApiRequestSucceedsButReturnsNonSuccessResponseCode() { runBlockingTest { // Given val parentId = "89345" - val messageDbId = 345L + val messageDbId = 3452L val message = Message().apply { dbId = messageDbId messageId = "17575c23-c3d9-4f3a-9188-02dea1321cc6" addressID = "addressId835" messageBody = "messageBody" + subject = "Subject002" } + val errorMessage = "Draft not created because.." val errorAPIResponse = mockk { - every { code } returns 500 - every { error } returns "Internal Error: Draft not created" + every { code } returns 402 + every { error } returns errorMessage } givenMessageIdInput(messageDbId) givenParentIdInput(parentId) givenActionTypeInput(NONE) givenPreviousSenderAddress("") every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message - every { messageFactory.createDraftApiRequest(message) } returns mockk(relaxed = true) + every { messageFactory.createDraftApiRequest(message) } returns mockk(relaxed = true) { + every { this@mockk.message.subject } returns "Subject002" + } coEvery { apiManager.createDraft(any()) } returns errorAPIResponse every { parameters.runAttemptCount } returns 0 // When val result = worker.doWork() + verify { errorNotifier.showPersistentError(errorMessage, "Subject002") } + val expected = ListenableWorker.Result.failure( + Data.Builder().putString( + KEY_OUTPUT_RESULT_SAVE_DRAFT_ERROR_ENUM, + CreateDraftWorkerErrors.BadResponseCodeError.name + ).build() + ) + assertEquals(expected, result) + } + } + + @Test + fun workerRetriesSavingDraftWhenApiRequestFailsAndMaxTriesWereNotReached() { + runBlockingTest { + // Given + val parentId = "89345" + val messageDbId = 345L + val message = Message().apply { + dbId = messageDbId + messageId = "17575c22-c3d9-4f3a-9188-02dea1321cc6" + addressID = "addressId835" + messageBody = "messageBody" + subject = "Subject001" + } + val errorMessage = "Error performing request" + givenMessageIdInput(messageDbId) + givenParentIdInput(parentId) + givenActionTypeInput(NONE) + givenPreviousSenderAddress("") + every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message + every { messageFactory.createDraftApiRequest(message) } returns mockk(relaxed = true) { + every { this@mockk.message.subject } returns "Subject001" + } + coEvery { apiManager.createDraft(any()) } throws IOException(errorMessage) + every { parameters.runAttemptCount } returns 3 + + // When + val result = worker.doWork() + // Then val expected = ListenableWorker.Result.retry() assertEquals(expected, result) From 2ddc2fe86e4088f494205b86300959c3b9a219d9 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Thu, 7 Jan 2021 13:11:14 +0100 Subject: [PATCH 061/145] Reformat MessageResponse class MAILAND-1281 --- .../messages/receive/MessageResponse.kt | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageResponse.kt b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageResponse.kt index 8999d1144..df274fa74 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageResponse.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageResponse.kt @@ -25,20 +25,20 @@ import com.google.gson.annotations.SerializedName class MessageResponse : ResponseBody() { - @SerializedName(Fields.Message.MESSAGE) - private lateinit var serverMessage: ServerMessage + @SerializedName(Fields.Message.MESSAGE) + private lateinit var serverMessage: ServerMessage - val message by lazy { - val attachmentFactory = AttachmentFactory() - val messageSenderFactory = MessageSenderFactory() - val messageFactory = MessageFactory(attachmentFactory, messageSenderFactory) - messageFactory.createMessage(serverMessage) - } + val message by lazy { + val attachmentFactory = AttachmentFactory() + val messageSenderFactory = MessageSenderFactory() + val messageFactory = MessageFactory(attachmentFactory, messageSenderFactory) + messageFactory.createMessage(serverMessage) + } - val messageId: String? - get() = message.messageId + val messageId: String? + get() = message.messageId - val attachments: List - get() = message.Attachments + val attachments: List + get() = message.Attachments } From 075d458f6bec66e585519431bdf144f9c715c9f0 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Thu, 7 Jan 2021 14:19:18 +0100 Subject: [PATCH 062/145] Allow opening draft messages from search screen Same reasons detailed in commit e298da0d4340908 MAILAND-1281 --- .../android/activities/SearchActivity.java | 68 +++++-------------- 1 file changed, 16 insertions(+), 52 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/activities/SearchActivity.java b/app/src/main/java/ch/protonmail/android/activities/SearchActivity.java index 50a3f8930..9c07ce8d1 100644 --- a/app/src/main/java/ch/protonmail/android/activities/SearchActivity.java +++ b/app/src/main/java/ch/protonmail/android/activities/SearchActivity.java @@ -19,7 +19,6 @@ package ch.protonmail.android.activities; import android.content.Intent; -import android.os.AsyncTask; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; @@ -28,7 +27,6 @@ import android.widget.AutoCompleteTextView; import android.widget.ProgressBar; import android.widget.TextView; -import android.widget.Toast; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; @@ -39,7 +37,6 @@ import com.squareup.otto.Subscribe; -import java.lang.ref.WeakReference; import java.lang.reflect.Field; import javax.inject.Inject; @@ -51,13 +48,12 @@ import ch.protonmail.android.activities.messageDetails.MessageDetailsActivity; import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository; import ch.protonmail.android.adapters.messages.MessagesRecyclerViewAdapter; +import ch.protonmail.android.api.models.room.messages.Message; import ch.protonmail.android.api.models.room.messages.MessagesDatabase; import ch.protonmail.android.api.models.room.messages.MessagesDatabaseFactory; import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDatabase; import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDatabaseFactory; -import ch.protonmail.android.api.models.room.pendingActions.PendingUpload; import ch.protonmail.android.api.segments.event.FetchUpdatesJob; -import ch.protonmail.android.core.Constants; import ch.protonmail.android.core.ProtonMailApplication; import ch.protonmail.android.data.ContactsRepository; import ch.protonmail.android.events.LogoutEvent; @@ -65,11 +61,11 @@ import ch.protonmail.android.events.user.MailSettingsEvent; import ch.protonmail.android.jobs.SearchMessagesJob; import ch.protonmail.android.utils.AppUtil; -import ch.protonmail.android.utils.extensions.TextExtensions; import dagger.hilt.android.AndroidEntryPoint; import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_DRAGGING; import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_SETTLING; +import static ch.protonmail.android.core.Constants.MessageLocationType; @AndroidEntryPoint public class SearchActivity extends BaseActivity { @@ -141,11 +137,12 @@ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { }); mAdapter.setItemClick(message -> { - if (Constants.MessageLocationType.Companion.fromInt(message.getLocation()) == Constants.MessageLocationType.ALL_DRAFT || - Constants.MessageLocationType.Companion.fromInt(message.getLocation()) == Constants.MessageLocationType.DRAFT) { - new CheckPendingUploadsAndStartComposeTask( - new WeakReference<>(SearchActivity.this), pendingActionsDatabase, message.getMessageId(), message.isInline()).execute(); - } else { + if (isDraftMessage(message)) { + Intent intent = AppUtil.decorInAppIntent(new Intent(SearchActivity.this, ComposeMessageActivity.class)); + intent.putExtra(ComposeMessageActivity.EXTRA_MESSAGE_ID, message.getMessageId()); + intent.putExtra(ComposeMessageActivity.EXTRA_MESSAGE_RESPONSE_INLINE, message.isInline()); + startActivity(intent); + } else { Intent intent = AppUtil.decorInAppIntent(new Intent(SearchActivity.this, MessageDetailsActivity.class)); intent.putExtra(MessageDetailsActivity.EXTRA_MESSAGE_ID, message.getMessageId()); intent.putExtra(MessageDetailsActivity.EXTRA_TRANSIENT_MESSAGE, true); @@ -160,7 +157,7 @@ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { mAdapter.addAll(messages); setLoadingMore(false); mProgressBar.setVisibility(View.GONE); - mAdapter.setNewLocation(Constants.MessageLocationType.SEARCH); + mAdapter.setNewLocation(MessageLocationType.SEARCH); } }); @@ -256,8 +253,14 @@ private void doSearch(boolean newSearch) { mJobManager.addJobInBackground(new SearchMessagesJob(mQueryText, mCurrentPage, newSearch)); } + private boolean isDraftMessage(Message message) { + MessageLocationType messageLocation = MessageLocationType.Companion.fromInt(message.getLocation()); + return messageLocation == MessageLocationType.ALL_DRAFT || + messageLocation == MessageLocationType.DRAFT; + } + @Subscribe - public void onMailSettingsEvent(MailSettingsEvent event) { + public void onMailSettingsEvent(MailSettingsEvent event) { loadMailSettings(); } @@ -281,43 +284,4 @@ private void setLoadingMore(boolean loadingMore) { mAdapter.setIncludeFooter(loadingMore); } - private static class CheckPendingUploadsAndStartComposeTask - extends AsyncTask { - - private final WeakReference searchActivity; - private final PendingActionsDatabase pendingActionsDatabase; - private final String messageId; - private final boolean isInline; - - CheckPendingUploadsAndStartComposeTask(WeakReference searchActivity, - PendingActionsDatabase pendingActionsDatabase, - String messageId, boolean isInline) { - this.searchActivity = searchActivity; - this.pendingActionsDatabase = pendingActionsDatabase; - this.messageId = messageId; - this.isInline = isInline; - } - - @Override - protected PendingUpload doInBackground(Void... voids) { - return pendingActionsDatabase.findPendingUploadByMessageId(messageId); - } - - @Override - protected void onPostExecute(PendingUpload pendingUpload) { - SearchActivity searchActivity = this.searchActivity.get(); - if (searchActivity == null) { - return; - } - if (pendingUpload != null) { - TextExtensions.showToast(searchActivity, R.string.draft_attachments_uploading, Toast.LENGTH_SHORT); - return; - } - - Intent intent = AppUtil.decorInAppIntent(new Intent(searchActivity, ComposeMessageActivity.class)); - intent.putExtra(ComposeMessageActivity.EXTRA_MESSAGE_ID, messageId); - intent.putExtra(ComposeMessageActivity.EXTRA_MESSAGE_RESPONSE_INLINE, isInline); - searchActivity.startActivity(intent); - } - } } From 7f5bb206b146eb48e6c11d49dc48a3582d6a7791 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Fri, 8 Jan 2021 12:54:22 +0100 Subject: [PATCH 063/145] Encapsulate "pending upload" logic to UploadAttachment use case Over having it shared between SaveDraft and CreateDraftWorker. - Solve issue where attachments upload was executed concurrently for the same message when the user updates a draft while the initial attachments upload is still ongoing - Solve edge case for which "pending upload" was being shown without the upload to actually be in progress MAILAND-1281 --- .../android/attachments/UploadAttachments.kt | 26 +++++++-- .../android/jobs/messages/PostMessageJob.java | 7 ++- .../android/usecase/compose/SaveDraft.kt | 7 --- .../worker/drafts/CreateDraftWorker.kt | 11 ---- .../attachments/UploadAttachmentsTest.kt | 57 ++++++++++++++++--- .../android/usecase/compose/SaveDraftTest.kt | 37 +----------- .../android/worker/CreateDraftWorkerTest.kt | 51 ----------------- 7 files changed, 76 insertions(+), 120 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt b/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt index 155dcc3ca..3865e8292 100644 --- a/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt +++ b/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt @@ -20,6 +20,8 @@ package ch.protonmail.android.attachments import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository import ch.protonmail.android.api.models.room.messages.Message +import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDao +import ch.protonmail.android.api.models.room.pendingActions.PendingUpload import ch.protonmail.android.core.UserManager import ch.protonmail.android.crypto.AddressCrypto import kotlinx.coroutines.runBlocking @@ -31,6 +33,7 @@ import javax.inject.Inject class UploadAttachments @Inject constructor( private val dispatchers: DispatcherProvider, private val attachmentsRepository: AttachmentsRepository, + private val pendingActionsDao: PendingActionsDao, private val messageDetailsRepository: MessageDetailsRepository, private val userManager: UserManager ) { @@ -48,14 +51,23 @@ class UploadAttachments @Inject constructor( suspend operator fun invoke(attachmentIds: List, message: Message, crypto: AddressCrypto): Result = withContext(dispatchers.Io) { - Timber.i("UploadAttachments started for messageId ${message.messageId} - attachmentIds $attachmentIds") + val messageId = requireNotNull(message.messageId) + Timber.i("UploadAttachments started for messageId $messageId - attachmentIds $attachmentIds") + + pendingActionsDao.findPendingUploadByMessageId(messageId)?.let { + Timber.i("UploadAttachments STOPPED for messageId $messageId as already in progress") + return@withContext Result.UploadInProgress + } + + pendingActionsDao.insertPendingForUpload(PendingUpload(messageId)) attachmentIds.forEach { attachmentId -> val attachment = messageDetailsRepository.findAttachmentById(attachmentId) if (attachment?.filePath == null || attachment.isUploaded || attachment.doesFileExist.not()) { Timber.d( - "Skipping attachment: not found, invalid or was already uploaded = ${attachment?.isUploaded}" + "Skipping attachment ${attachment?.attachmentId}: " + + "not found, invalid or was already uploaded = ${attachment?.isUploaded}" ) return@forEach } @@ -65,11 +77,12 @@ class UploadAttachments @Inject constructor( when (result) { is AttachmentsRepository.Result.Success -> { - Timber.d("UploadAttachment $attachmentId to API for messageId ${message.messageId} Succeeded.") + Timber.d("UploadAttachment $attachmentId to API for messageId $messageId Succeeded.") updateMessageWithUploadedAttachment(message, result.uploadedAttachmentId) } is AttachmentsRepository.Result.Failure -> { - Timber.e("UploadAttachment $attachmentId to API for messageId ${message.messageId} FAILED.") + Timber.e("UploadAttachment $attachmentId to API for messageId $messageId FAILED.") + pendingActionsDao.deletePendingUploadByMessageId(messageId) return@withContext Result.Failure(result.error) } } @@ -79,14 +92,16 @@ class UploadAttachments @Inject constructor( val isAttachPublicKey = userManager.getMailSettings(userManager.username)?.getAttachPublicKey() ?: false if (isAttachPublicKey) { - Timber.i("UploadAttachments attaching publicKey for messageId ${message.messageId}") + Timber.i("UploadAttachments attaching publicKey for messageId $messageId") val result = attachmentsRepository.uploadPublicKey(message, crypto) if (result is AttachmentsRepository.Result.Failure) { + pendingActionsDao.deletePendingUploadByMessageId(messageId) return@withContext Result.Failure(result.error) } } + pendingActionsDao.deletePendingUploadByMessageId(messageId) return@withContext Result.Success } @@ -110,6 +125,7 @@ class UploadAttachments @Inject constructor( sealed class Result { object Success : Result() + object UploadInProgress : Result() data class Failure(val error: String) : Result() } } diff --git a/app/src/main/java/ch/protonmail/android/jobs/messages/PostMessageJob.java b/app/src/main/java/ch/protonmail/android/jobs/messages/PostMessageJob.java index 2fc18c2f1..b2332a09c 100644 --- a/app/src/main/java/ch/protonmail/android/jobs/messages/PostMessageJob.java +++ b/app/src/main/java/ch/protonmail/android/jobs/messages/PostMessageJob.java @@ -42,8 +42,8 @@ import ch.protonmail.android.BuildConfig; import ch.protonmail.android.R; import ch.protonmail.android.api.interceptors.RetrofitTag; -import ch.protonmail.android.api.models.MailSettings; import ch.protonmail.android.api.models.DraftBody; +import ch.protonmail.android.api.models.MailSettings; import ch.protonmail.android.api.models.SendPreference; import ch.protonmail.android.api.models.User; import ch.protonmail.android.api.models.factories.PackageFactory; @@ -245,7 +245,7 @@ public void onRun() throws Throwable { MailSettings mailSettings = getUserManager().getMailSettings(mUsername); - UploadAttachments uploadAttachments = buildUploadAttachmentsUseCase(); + UploadAttachments uploadAttachments = buildUploadAttachmentsUseCase(pendingActionsDatabase); UploadAttachments.Result result = uploadAttachments.blocking(mNewAttachments, message, crypto); if (result instanceof UploadAttachments.Result.Failure) { @@ -273,7 +273,7 @@ public void onRun() throws Throwable { * TODO Drop this and just inject the use case when migrating this Job to a worker */ @NotNull - private UploadAttachments buildUploadAttachmentsUseCase() { + private UploadAttachments buildUploadAttachmentsUseCase(PendingActionsDatabase pendingActionsDatabase) { DispatcherProvider dispatchers = new DispatcherProvider() { @Override public @NotNull CoroutineDispatcher getMain() { @@ -300,6 +300,7 @@ private UploadAttachments buildUploadAttachmentsUseCase() { return new UploadAttachments( dispatchers, attachmentsRepository, + pendingActionsDatabase, getMessageDetailsRepository(), getUserManager() ); diff --git a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt index 200fb7844..0e52545c3 100644 --- a/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt +++ b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt @@ -23,7 +23,6 @@ import androidx.work.WorkInfo import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDao -import ch.protonmail.android.api.models.room.pendingActions.PendingUpload import ch.protonmail.android.attachments.UploadAttachments import ch.protonmail.android.core.Constants import ch.protonmail.android.core.Constants.MessageLocationType.ALL_DRAFT @@ -72,10 +71,6 @@ class SaveDraft @Inject constructor( message.messageBody = encryptedBody setMessageAsDraft(message) - if (params.newAttachmentIds.isNotEmpty()) { - pendingActionsDao.insertPendingForUpload(PendingUpload(messageId)) - } - val messageDbId = messageDetailsRepository.saveMessageLocally(message) pendingActionsDao.findPendingSendByDbId(messageDbId)?.let { return@withContext flowOf(SaveDraftResult.SendingInProgressError) @@ -110,7 +105,6 @@ class SaveDraft @Inject constructor( messageDetailsRepository.findMessageById(createdDraftId)?.let { val uploadResult = uploadAttachments(params.newAttachmentIds, it, addressCrypto) - pendingActionsDao.deletePendingUploadByMessageId(localDraftId) if (uploadResult is UploadAttachments.Result.Failure) { errorNotifier.showPersistentError(uploadResult.error, localDraft.subject) @@ -121,7 +115,6 @@ class SaveDraft @Inject constructor( } Timber.e("Saving Draft to API for messageId $localDraftId FAILED.") - pendingActionsDao.deletePendingUploadByMessageId(localDraftId) return@map SaveDraftResult.OnlineDraftCreationFailed } .flowOn(dispatchers.Io) diff --git a/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt index ccd33de8d..47eca2b15 100644 --- a/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt +++ b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt @@ -40,7 +40,6 @@ import ch.protonmail.android.api.models.messages.receive.MessageFactory import ch.protonmail.android.api.models.room.messages.Attachment import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.api.models.room.messages.MessageSender -import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDao import ch.protonmail.android.api.segments.TEN_SECONDS import ch.protonmail.android.core.Constants import ch.protonmail.android.core.Constants.MessageActionType.FORWARD @@ -83,7 +82,6 @@ class CreateDraftWorker @WorkerInject constructor( private val addressCryptoFactory: AddressCrypto.Factory, private val base64: Base64Encoder, private val apiManager: ProtonMailApiManager, - private val pendingActionsDao: PendingActionsDao, private val errorNotifier: ErrorNotifier ) : CoroutineWorker(context, params) { @@ -132,7 +130,6 @@ class CreateDraftWorker @WorkerInject constructor( val responseDraft = response.message.copy() updateStoredLocalDraft(responseDraft, message) - updatePendingUploadMessage(messageId, responseDraft) Timber.i("Create Draft Worker API call succeeded") Result.success( @@ -145,14 +142,6 @@ class CreateDraftWorker @WorkerInject constructor( ) } - private fun updatePendingUploadMessage(messageId: String, responseDraft: Message) { - val pendingForUpload = pendingActionsDao.findPendingUploadByMessageId(messageId) - pendingForUpload?.let { - pendingForUpload.messageId = requireNotNull(responseDraft.messageId) - pendingActionsDao.insertPendingForUpload(pendingForUpload) - } - } - private suspend fun updateStoredLocalDraft(apiDraft: Message, localDraft: Message) { apiDraft.apply { dbId = localDraft.dbId diff --git a/app/src/test/java/ch/protonmail/android/attachments/UploadAttachmentsTest.kt b/app/src/test/java/ch/protonmail/android/attachments/UploadAttachmentsTest.kt index 3847ef11e..c006bbf9b 100644 --- a/app/src/test/java/ch/protonmail/android/attachments/UploadAttachmentsTest.kt +++ b/app/src/test/java/ch/protonmail/android/attachments/UploadAttachmentsTest.kt @@ -22,6 +22,8 @@ package ch.protonmail.android.attachments import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository import ch.protonmail.android.api.models.room.messages.Attachment import ch.protonmail.android.api.models.room.messages.Message +import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDao +import ch.protonmail.android.api.models.room.pendingActions.PendingUpload import ch.protonmail.android.core.UserManager import ch.protonmail.android.crypto.AddressCrypto import io.mockk.MockKAnnotations @@ -44,6 +46,9 @@ import kotlin.test.Test class UploadAttachmentsTest : CoroutinesTest { + @RelaxedMockK + private lateinit var pendingActionsDao: PendingActionsDao + @RelaxedMockK private lateinit var attachmentsRepository: AttachmentsRepository @@ -65,6 +70,36 @@ class UploadAttachmentsTest : CoroutinesTest { coEvery { attachmentsRepository.upload(any(), crypto) } returns AttachmentsRepository.Result.Success coEvery { attachmentsRepository.upload(any(), crypto) } returns AttachmentsRepository.Result.Success("8237423") coEvery { messageDetailsRepository.saveMessageLocally(any()) } returns 823L + every { pendingActionsDao.findPendingUploadByMessageId(any()) } returns null + } + + @Test + fun uploadAttachmentsSetsMessageAsPendingToUploadWhenStartingToUpload() { + runBlockingTest { + val attachmentIds = listOf("1") + val messageId = "message-id-238237" + val message = Message(messageId) + + uploadAttachments(attachmentIds, message, crypto) + + val pendingUpload = PendingUpload(messageId) + verify { pendingActionsDao.insertPendingForUpload(pendingUpload) } + } + } + + @Test + fun uploadAttachmentsIsNotExecutedAgainWhenUploadAlreadyOngoingForTheGivenMessage() { + runBlockingTest { + val attachmentIds = listOf("1") + val messageId = "message-id-123842" + val message = Message(messageId) + every { pendingActionsDao.findPendingUploadByMessageId(messageId) } returns PendingUpload(messageId) + + val result = uploadAttachments(attachmentIds, message, crypto) + + verify(exactly = 0) { pendingActionsDao.insertPendingForUpload(any()) } + assertEquals(UploadAttachments.Result.UploadInProgress, result) + } } @Test @@ -83,17 +118,19 @@ class UploadAttachmentsTest : CoroutinesTest { every { doesFileExist } returns true } val attachmentIds = listOf("1", "2") - val message = Message(messageId = "messageId") + val message = Message(messageId = "messageId8234") every { messageDetailsRepository.findAttachmentById("1") } returns attachment1 every { messageDetailsRepository.findAttachmentById("2") } returns attachment2 val result = uploadAttachments(attachmentIds, message, crypto) coVerifyOrder { + pendingActionsDao.findPendingUploadByMessageId("messageId8234") attachment1.setMessage(message) attachmentsRepository.upload(attachment1, crypto) attachment2.setMessage(message) attachmentsRepository.upload(attachment2, crypto) + pendingActionsDao.deletePendingUploadByMessageId("messageId8234") } assertEquals(UploadAttachments.Result.Success, result) } @@ -115,7 +152,7 @@ class UploadAttachmentsTest : CoroutinesTest { every { doesFileExist } returns true } val attachmentIds = listOf("1", "2") - val message = Message() + val message = Message("messageId8237") every { messageDetailsRepository.findAttachmentById("1") } returns attachment1 every { messageDetailsRepository.findAttachmentById("2") } returns attachment2 coEvery { attachmentsRepository.upload(attachment2, crypto) } answers { @@ -126,6 +163,7 @@ class UploadAttachmentsTest : CoroutinesTest { val expected = UploadAttachments.Result.Failure("Failed to upload attachment2") assertEquals(expected, result) + verify { pendingActionsDao.deletePendingUploadByMessageId("messageId8237") } } } @@ -133,7 +171,7 @@ class UploadAttachmentsTest : CoroutinesTest { fun uploadAttachmentsReturnsFailureIfPublicKeyFailsToBeUploaded() { runBlockingTest { val attachmentIds = listOf("1") - val message = Message() + val message = Message("messageId9273585") val username = "username" every { userManager.username } returns username every { userManager.getMailSettings(username)?.getAttachPublicKey() } returns true @@ -145,6 +183,7 @@ class UploadAttachmentsTest : CoroutinesTest { val expected = UploadAttachments.Result.Failure("Failed to upload public key") assertEquals(expected, result) + verify { pendingActionsDao.deletePendingUploadByMessageId("messageId9273585") } } } @@ -158,7 +197,7 @@ class UploadAttachmentsTest : CoroutinesTest { every { doesFileExist } returns true } val attachmentIds = listOf("1", "2") - val message = Message() + val message = Message("messageId9237") every { messageDetailsRepository.findAttachmentById("1") } returns null every { messageDetailsRepository.findAttachmentById("2") } returns attachment2 @@ -184,7 +223,7 @@ class UploadAttachmentsTest : CoroutinesTest { every { doesFileExist } returns true } val attachmentIds = listOf("1", "2") - val message = Message() + val message = Message("messageId36926543") every { messageDetailsRepository.findAttachmentById("1") } returns attachment1 every { messageDetailsRepository.findAttachmentById("2") } returns attachment2 @@ -211,7 +250,7 @@ class UploadAttachmentsTest : CoroutinesTest { every { doesFileExist } returns true } val attachmentIds = listOf("1", "2") - val message = Message() + val message = Message("messageId0123876") every { messageDetailsRepository.findAttachmentById("1") } returns attachment1 every { messageDetailsRepository.findAttachmentById("2") } returns attachment2 @@ -226,7 +265,7 @@ class UploadAttachmentsTest : CoroutinesTest { fun uploadAttachmentsSkipsUploadingIfAttachmentFileDoesNotExist() { runBlockingTest { val attachmentIds = listOf("1", "2") - val message = Message() + val message = Message("messageId83483") val attachmentMock1 = mockk(relaxed = true) { every { attachmentId } returns "1" every { filePath } returns "filePath1" @@ -253,7 +292,7 @@ class UploadAttachmentsTest : CoroutinesTest { fun uploadAttachmentsCallRepositoryUploadPublicKeyWhenMailSettingsGetAttachPublicKeyIsTrue() { runBlockingTest { val username = "username" - val message = Message() + val message = Message("messageId823762") every { userManager.username } returns username every { userManager.getMailSettings(username)?.getAttachPublicKey() } returns true coEvery { attachmentsRepository.uploadPublicKey(message, crypto) } answers { @@ -270,7 +309,7 @@ class UploadAttachmentsTest : CoroutinesTest { fun uploadAttachmentsDeletesLocalFileAfterSuccessfulUpload() { runBlockingTest { val attachmentIds = listOf("1", "2") - val message = Message() + val message = Message("messageId126943") val attachmentMock1 = mockk(relaxed = true) { every { attachmentId } returns "1" every { filePath } returns "filePath1" diff --git a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt index 7d9fc52f4..8bb052f16 100644 --- a/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt +++ b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt @@ -26,7 +26,6 @@ import ch.protonmail.android.activities.messageDetails.repository.MessageDetails import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDao import ch.protonmail.android.api.models.room.pendingActions.PendingSend -import ch.protonmail.android.api.models.room.pendingActions.PendingUpload import ch.protonmail.android.attachments.UploadAttachments import ch.protonmail.android.attachments.UploadAttachments.Result.Failure import ch.protonmail.android.core.Constants.MessageActionType.FORWARD @@ -127,33 +126,6 @@ class SaveDraftTest : CoroutinesTest { } } - @Test - fun saveDraftsInsertsPendingUploadWhenThereAreNewAttachments() { - runBlockingTest { - // Given - val message = Message().apply { - dbId = 123L - this.messageId = "456" - addressID = "addressId" - decryptedBody = "Message body in plain text" - } - val addressCrypto = mockk { - every { encrypt("Message body in plain text", true).armored } returns "encrypted armored content" - } - every { addressCryptoFactory.create(Id("addressId"), Name(currentUsername)) } returns addressCrypto - coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 123L - - // When - val newAttachments = listOf("attachmentId") - saveDraft.invoke( - SaveDraftParameters(message, newAttachments, "parentId", REPLY, "previousSenderId1273") - ) - - // Then - verify { pendingActionsDao.insertPendingForUpload(PendingUpload("456")) } - } - } - @Test fun saveDraftsDoesNotInsertsPendingUploadWhenThereAreNoNewAttachments() { runBlockingTest { @@ -345,7 +317,7 @@ class SaveDraftTest : CoroutinesTest { } @Test - fun saveDraftsRemovesPendingUploadAndReturnsFailureWhenWorkerFailsCreatingDraftOnAPI() { + fun saveDraftsReturnsFailureWhenWorkerFailsCreatingDraftOnAPI() { runBlockingTest { // Given val localDraftId = "8345" @@ -379,13 +351,12 @@ class SaveDraftTest : CoroutinesTest { ).first() // Then - verify { pendingActionsDao.deletePendingUploadByMessageId("45623") } assertEquals(SaveDraftResult.OnlineDraftCreationFailed, result) } } @Test - fun saveDraftsRemovesPendingUploadAndShowAndReturnsErrorWhenUploadingNewAttachmentsFails() { + fun saveDraftsShowPersistentErrorAndReturnsErrorWhenUploadingNewAttachmentsFails() { runBlockingTest { // Given val localDraftId = "8345" @@ -423,14 +394,13 @@ class SaveDraftTest : CoroutinesTest { ).first() // Then - verify { pendingActionsDao.deletePendingUploadByMessageId("45623") } verify { errorNotifier.showPersistentError(errorMessage, "Message Subject") } assertEquals(SaveDraftResult.UploadDraftAttachmentsFailed, result) } } @Test - fun saveDraftRemovesMessageFromPendingForUploadListAndReturnsSuccessWhenUploadSucceeds() { + fun saveDraftReturnsSuccessWhenBothDraftCreationAndAttachmentsUploadSucceeds() { runBlockingTest { // Given val localDraftId = "8345" @@ -470,7 +440,6 @@ class SaveDraftTest : CoroutinesTest { ).first() // Then - verify { pendingActionsDao.deletePendingUploadByMessageId("45623") } assertEquals(SaveDraftResult.Success("createdDraftMessageId345"), result) } } diff --git a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt index 972b2fa4d..922502e24 100644 --- a/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt +++ b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt @@ -39,7 +39,6 @@ import ch.protonmail.android.api.models.room.messages.Attachment import ch.protonmail.android.api.models.room.messages.Message import ch.protonmail.android.api.models.room.messages.MessageSender import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDao -import ch.protonmail.android.api.models.room.pendingActions.PendingUpload import ch.protonmail.android.core.Constants import ch.protonmail.android.core.Constants.MessageActionType.FORWARD import ch.protonmail.android.core.Constants.MessageActionType.NONE @@ -621,56 +620,6 @@ class CreateDraftWorkerTest : CoroutinesTest { } } - @Test - fun workerUpdatesPendingForUploadActionInDbWithIdOfCreatedDraft() { - runBlockingTest { - // Given - val parentId = "89345" - val messageDbId = 345L - val localMessageId = "ac7b3d53-fc64-4d44-a1f5-39df45b629ef" - val message = Message().apply { - dbId = messageDbId - addressID = "addressId835" - messageId = localMessageId - messageBody = "messageBody" - sender = MessageSender("sender2342", "senderEmail@2340.com") - setLabelIDs(listOf("label", "label1", "label2")) - parsedHeaders = ParsedHeaders("recEncryption", "recAuth") - numAttachments = 3 - } - - val apiDraftRequest = mockk(relaxed = true) - val responseMessage = message.copy( - messageId = "response_message_id", - isDownloaded = true, - localId = localMessageId, - Unread = false - ) - responseMessage.dbId = messageDbId - val apiDraftResponse = mockk { - every { code } returns 1000 - every { messageId } returns "response_message_id" - every { this@mockk.message } returns responseMessage - } - val pendingUpload = PendingUpload(localMessageId) - givenMessageIdInput(messageDbId) - givenParentIdInput(parentId) - givenActionTypeInput(NONE) - givenPreviousSenderAddress("") - every { messageDetailsRepository.findMessageByMessageDbId(messageDbId) } returns message - every { messageFactory.createDraftApiRequest(message) } returns apiDraftRequest - coEvery { apiManager.createDraft(apiDraftRequest) } returns apiDraftResponse - every { pendingActionsDao.findPendingUploadByMessageId(localMessageId) } returns pendingUpload - - // When - worker.doWork() - - // Then - val expectedPendingUpload = PendingUpload("response_message_id") - coVerify { pendingActionsDao.insertPendingForUpload(expectedPendingUpload) } - } - } - @Test fun workerReturnsFailureWithoutRetryingWhenApiRequestSucceedsButReturnsNonSuccessResponseCode() { runBlockingTest { From 242666684c33f18132b384f772fb68bd51b6caa7 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Fri, 8 Jan 2021 16:26:40 +0100 Subject: [PATCH 064/145] Remove uneeded fields from SearchActivity MAILAND-1281 --- .../ch/protonmail/android/activities/SearchActivity.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/activities/SearchActivity.java b/app/src/main/java/ch/protonmail/android/activities/SearchActivity.java index 9c07ce8d1..b918c0b7f 100644 --- a/app/src/main/java/ch/protonmail/android/activities/SearchActivity.java +++ b/app/src/main/java/ch/protonmail/android/activities/SearchActivity.java @@ -49,10 +49,6 @@ import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository; import ch.protonmail.android.adapters.messages.MessagesRecyclerViewAdapter; import ch.protonmail.android.api.models.room.messages.Message; -import ch.protonmail.android.api.models.room.messages.MessagesDatabase; -import ch.protonmail.android.api.models.room.messages.MessagesDatabaseFactory; -import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDatabase; -import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDatabaseFactory; import ch.protonmail.android.api.segments.event.FetchUpdatesJob; import ch.protonmail.android.core.ProtonMailApplication; import ch.protonmail.android.data.ContactsRepository; @@ -70,8 +66,6 @@ @AndroidEntryPoint public class SearchActivity extends BaseActivity { - private PendingActionsDatabase pendingActionsDatabase; - private MessagesRecyclerViewAdapter mAdapter; private TextView noMessagesView; private ProgressBar mProgressBar; @@ -79,7 +73,6 @@ public class SearchActivity extends BaseActivity { private String mQueryText = ""; private int mCurrentPage; private SearchView searchView = null; - private MessagesDatabase searchDatabase; @Inject MessageDetailsRepository messageDetailsRepository; @@ -94,8 +87,6 @@ protected int getLayoutId() { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - searchDatabase = MessagesDatabaseFactory.Companion.getSearchDatabase(getApplicationContext()).getDatabase(); - pendingActionsDatabase= PendingActionsDatabaseFactory.Companion.getInstance(getApplicationContext()).getDatabase(); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); From 14bc3d1a4f10dc472fbfb165015e997e2bd8e1c5 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Thu, 14 Jan 2021 10:32:15 +0100 Subject: [PATCH 065/145] Migrate AndroidErrorNotifierTest to junit4 and solve conflicts Conflict was a leftover of rebasing on 1018 which was rebased on dev MAILAND-1281 --- .../android/attachments/UploadAttachmentsTest.kt | 2 -- .../android/compose/ComposeMessageViewModelTest.kt | 5 ----- .../utils/notifier/AndroidErrorNotifierTest.kt | 12 ++++++++---- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/app/src/test/java/ch/protonmail/android/attachments/UploadAttachmentsTest.kt b/app/src/test/java/ch/protonmail/android/attachments/UploadAttachmentsTest.kt index c006bbf9b..a5e08c364 100644 --- a/app/src/test/java/ch/protonmail/android/attachments/UploadAttachmentsTest.kt +++ b/app/src/test/java/ch/protonmail/android/attachments/UploadAttachmentsTest.kt @@ -40,7 +40,6 @@ import io.mockk.verify import junit.framework.Assert.assertEquals import kotlinx.coroutines.test.runBlockingTest import me.proton.core.test.kotlin.CoroutinesTest -import org.junit.Rule import kotlin.test.BeforeTest import kotlin.test.Test @@ -67,7 +66,6 @@ class UploadAttachmentsTest : CoroutinesTest { @BeforeTest fun setUp() { MockKAnnotations.init(this) - coEvery { attachmentsRepository.upload(any(), crypto) } returns AttachmentsRepository.Result.Success coEvery { attachmentsRepository.upload(any(), crypto) } returns AttachmentsRepository.Result.Success("8237423") coEvery { messageDetailsRepository.saveMessageLocally(any()) } returns 823L every { pendingActionsDao.findPendingUploadByMessageId(any()) } returns null diff --git a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt index abc02e405..60516e9d4 100644 --- a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt +++ b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt @@ -86,14 +86,9 @@ class ComposeMessageViewModelTest : CoroutinesTest { @InjectMockKs lateinit var viewModel: ComposeMessageViewModel -<<<<<<< HEAD @BeforeTest fun setUp() { MockKAnnotations.init(this) -======= - @BeforeEach - fun setUp() { ->>>>>>> d30d7302 (Fix ComposeMessageViewModel tests) // To avoid `EmptyList` to be returned by Mockk automatically as that causes // UnsupportedOperationException: Operation is not supported for read-only collection // when trying to add elements (in prod we ArrayList so this doesn't happen) diff --git a/app/src/test/java/ch/protonmail/android/utils/notifier/AndroidErrorNotifierTest.kt b/app/src/test/java/ch/protonmail/android/utils/notifier/AndroidErrorNotifierTest.kt index 47a71d297..a91699b1f 100644 --- a/app/src/test/java/ch/protonmail/android/utils/notifier/AndroidErrorNotifierTest.kt +++ b/app/src/test/java/ch/protonmail/android/utils/notifier/AndroidErrorNotifierTest.kt @@ -21,16 +21,15 @@ package ch.protonmail.android.utils.notifier import ch.protonmail.android.core.UserManager import ch.protonmail.android.servers.notification.INotificationServer +import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.impl.annotations.InjectMockKs import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.RelaxedMockK -import io.mockk.junit5.MockKExtension import io.mockk.verify -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith +import kotlin.test.BeforeTest +import kotlin.test.Test -@ExtendWith(MockKExtension::class) class AndroidErrorNotifierTest { @RelaxedMockK @@ -42,6 +41,11 @@ class AndroidErrorNotifierTest { @InjectMockKs private lateinit var errorNotifier: AndroidErrorNotifier + @BeforeTest + fun setUp() { + MockKAnnotations.init(this) + } + @Test fun errorNotifierCallsNotificationServerToDisplayErrorInPersistentNotification() { val errorMessage = "Failed uploading attachments" From 1251620a306fcc97ad5dfdc612375f67de93ddee Mon Sep 17 00:00:00 2001 From: proton-ci Date: Mon, 18 Jan 2021 09:26:49 +0000 Subject: [PATCH 066/145] [i18n@] ~ Upgrade translations from crowdin --- app/src/main/res/values-de/strings.xml | 4 +- app/src/main/res/values-es/strings.xml | 6 +- app/src/main/res/values-fr/strings.xml | 90 +++++++++++++------------- app/src/main/res/values-hr/strings.xml | 4 +- 4 files changed, 52 insertions(+), 52 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 0ea10d495..5945e346c 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -328,8 +328,8 @@ das Anmeldepasswort zurückzusetzen, sollten Sie es vergessen haben. Das eingegebene Passwort ist ungültig Bitte %d Sekunden warten, bevor Sie erneut klicken Passwort der Nachricht unvollständig. - Der Zugang zu diesem Konto wurde aufgrund einer unbezahlten Rechnung deaktiviert. Bitte melden Sie sich über protonmail.com an, um Ihre fällige Rechnung zu begleichen. - Mobile Anmeldungen sind vorübergehend deaktiviert. Bitte versuchen Sie es später noch einmal oder melden Sie sich unter protonmail.com mit einem Desktop-Rechner oder Laptop an. + Der Zugriff auf dieses Konto ist aufgrund eines Zahlungsverzugs deaktiviert. Bitte melden Sie sich bei protonmail.com an, um Ihre ausstehende Rechnung zu begleichen. + Mobiles Registrieren ist vorübergehend deaktiviert. Bitte versuchen Sie es später noch einmal oder melden Sie sich unter protonmail.com mit einem Desktop-Rechner oder Laptop an. Wird heruntergeladen … Anhang kann nicht heruntergeladen werden diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index e0f3a18ea..e7683baf3 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -428,7 +428,7 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Enviar mensajes cifrados a destinatarios externos Soporte prioritario 500 MB de almacenamiento - 150 mensajes enviados por día + 150 mensajes al día 20 etiquetas Dominios personalizados: no soportado 1 dirección @@ -661,7 +661,7 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Error de verificación Falló la verificación de la firma de este contenido Error de descifrado - Falló el descifrado de este contenido + El descifrado de este contenido ha fallado Dirección: Calle Ciudad @@ -991,7 +991,7 @@ compre más Imagen no encontrada URL de la imagen mal formada - Error while saving message. Try again. + Se ha producido un error al guardar el mensaje. Inténtalo de nuevo. Permitido Denegado diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0ada1ec90..61af41031 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -57,9 +57,9 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. ProtonMail est actuellement hors-ligne, visitez notre Twitter pour consulter l\'état actuel du service : https://twitter.com/protonmail Contact enregistré Contact enregistré, il sera synchronisé quand une connexion sera disponible - L\'adresse email du contact existe déjà - L\'adresse email n\'est pas valide - L’adresse email est un doublon + L\'adresse électronique du contact existe déjà + L\'adresse électronique n\'est pas valide + L’adresse électronique est un doublon Le code est invalide L\'envoi du message a échoué. Vos messages sont sauvegardés dans le dossier brouillons Ouvrir @@ -121,7 +121,7 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Répondre à tous Transférer Objet - Objet de l\'email + Objet du courriel Mot de passe du message Confirmer le mot de passe Définir un indice (facultatif) @@ -166,8 +166,8 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Confirmer Êtes-vous sûr ? Cette action ne peut être annulée. Créer - Email - Adresse email + Adresse électronique + Adresse électronique Numéro de téléphone Enregister Synchronisation en cours... @@ -203,7 +203,7 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Enregistrer les paramètres Partager Paramètres non enregistrés - mot de passe de connexion incorrect - Paramètres non enregistrés - email de notification invalide + Paramètres non enregistrés - adresse électronique de notification invalide Entrer Annuler Fermer @@ -227,7 +227,7 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Verrouiller l\'App Nom d\'affichage invalide. \'>\' et \'<\' ne sont pas autorisés Continuer - Nous enverrons un code de vérification à l’adresse email ci-dessus. + Nous enverrons un code de vérification à l’adresse électronique ci-dessus. Nous enverrons un code de vérification au numéro de téléphone ci-dessus. Envoyer un code de vérification Renvoyer @@ -245,7 +245,7 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Tenez-moi au courant des nouvelles fonctionnalités Créer un mot de passe Remarque : ceci est utilisé pour chiffrer et déchiffrer vos messages. - Ne perdez pas ce mot de passe, nous ne pouvons pas le récupérer. Si vous perdez votre mot de passe, vous ne pourrez plus lire vos emails. + Ne perdez pas ce mot de passe, nous ne pouvons pas le récupérer. Si vous perdez votre mot de passe, vous ne pourrez plus lire vos courriels. Remarque : le nom d\'utilisateur est également votre adresse ProtonMail En utilisant ProtonMail, vous acceptez nos\n conditions générales d\'utilisation et notre politique de confidentialité @@ -253,9 +253,9 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Afin de prévenir les abus sur ProtonMail,\nnous devons vérifier que vous êtes bien humain. Veuillez sélectionner l\'une des options suivantes : Aucune méthode de vérification trouvée - Vérification par email + Vérification par courriel Vérification par téléphone - Saisissez votre adresse email existante + Saisissez votre adresse électronique existante Saisissez votre numéro de téléphone Configuration du chiffrement Sécurité élevée (2048 bits) @@ -283,7 +283,7 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Label supprimé Erreur lors de la suppression du label Erreur lors de la suppression de certains messages. Certains des messages sélectionnés sont en cours d\'envoi ?! - Répondre dans l\'email + Répondre dans le message Aucune connexion détectée... Aucune connexion détectée… (dépannage) Recherche de connexion en cours... @@ -305,9 +305,9 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. La longueur maximale d\'un nom d\'utilisateur est de %d caractères ! Félicitations ! Votre nouveau compte\nde messagerie sécurisée est prêt. - L\'adresse email de secours optionnelle vous permet de + L\'adresse électronique de secours optionnelle vous permet de réinitialiser votre mot de passe si vous l\'oubliez. - Lorsque vous envoyez un email, c\'est le nom qui + Lorsque vous envoyez un courriel, c\'est le nom qui apparaît dans le champ expéditeur. Invalide fermer la visite guidée @@ -321,7 +321,7 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Votre nouveau compte de messagerie chiffrée a été mis en place et est prêt à envoyer et recevoir des messages chiffrés. Vous pouvez personnaliser les gestes de défilement dans les paramètres de l\'application ProtonMail. Créez et ajoutez des labels pour organiser votre boîte de réception. Appuyez et maintenez sur un message pour afficher toutes les options. - Votre boîte de réception est désormais protégée par un chiffrement de bout en bout. Pour envoyer automatiquement des emails sécurisés à vos contacts, invitez-les à rejoindre ProtonMail ! Vous pouvez également chiffrer manuellement vos messages s\'ils n\'utilisent pas ProtonMail. + Votre boîte de réception est désormais protégée par un chiffrement de bout en bout. Pour envoyer automatiquement des courriels sécurisés à vos contacts, invitez-les à rejoindre ProtonMail ! Vous pouvez également chiffrer manuellement vos messages s\'ils n\'utilisent pas ProtonMail. Il est possible de mettre en place un délai d\'expiration après lequel les messages que vous envoyez s\'effacent automatiquement. Vous pouvez obtenir de l\'aide via protonmail.com/support. Les bugs peuvent également être signalés depuis l\'application. Pièce jointe déjà ajoutée @@ -436,7 +436,7 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Aucune limite d\'envoi Accès anticipé aux nouvelles fonctionnalités Contribuez au choix de nouvelles fonctionnalités à l\'aide de sondages dédiés aux seuls titulaires de l\'offre Visionary - bientôt, les entreprises pourront profiter des emails chiffrés ! + bientôt, les entreprises pourront profiter des courriels chiffrés ! Devise & Durée Nom sur la carte de crédit/débit Code postal @@ -473,7 +473,7 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Créer un compte gratuit %d Go de stockage %d Mo de stockage - Jusqu\'à %d alias d’adresse email + Jusqu\'à %d alias d’adresse électronique [Inline] %d images incorporées Sélectionner @@ -481,7 +481,7 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Passer à une facturation mensuelle Passer à une facturation annuelle Attention - Avertissement : vous n’avez pas défini d\'email de secours, la récupération du compte est donc impossible si vous oubliez votre mot de passe. Voulez-vous poursuivre sans email de récupération ? + Avertissement : vous n’avez pas défini d\'adresse électronique de secours, la récupération du compte est donc impossible si vous oubliez votre mot de passe. Voulez-vous poursuivre sans adresse de récupération ? %1$s (*** %2$s) Date d\'expiration %s Modes de paiements @@ -497,7 +497,7 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Adresses disponibles Adresses inactives Non défini - Modifier l’adresse email de secours + Modifier l’adresse électronique de secours Actions disponibles Vous n\'avez aucune adresse disponible Vous n\'avez aucune adresse inactive @@ -508,11 +508,11 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Supprimer la sélection Code PIN non activé Options disponibles - Adresse email de secours actuelle - Modifier l\'adresse email de secours - Nouvelle adresse email de secours - Confirmer la nouvelle adresse email de secours - L\'adresse email ne correspond pas ou n\'est pas valide + Adresse électronique de secours actuelle + Modifier l\'adresse électronique de secours + Nouvelle adresse électronique de secours + Confirmer la nouvelle adresse électronique de secours + L\'adresse électronique ne correspond pas ou n\'est pas valide S\'authentifier Si cette option est activée, l\'application se synchronisera en arrière-plan avec le serveur. La désactivation de cette fonctionnalité désactivera les notifications push. @@ -553,12 +553,12 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. L\'envoi du message a échoué. %s Importer sur ProtonMail Veuillez sélectionner uniquement les contacts locaux - Saisir une adresse email + Saisir une adresse électronique Saisir un numéro de téléphone Saisir une adresse Saisir des informations Saisir une note - ajouter une adresse email + ajouter une adresse électronique ajouter un numéro de téléphone ajouter une adresse ajouter des informations @@ -583,7 +583,7 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Autre - Email + Adresse électronique Personnel Travail Autre @@ -641,7 +641,7 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Tous les messages TOUS LES MESSAGES Aucun message - Aucune adresse email + Aucune adresse électronique Aucun contact %1$s < %2$s > a écrit : Mettre à jour le label @@ -651,8 +651,8 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Détails copiés dans le presse-papiers Enregistrement en cours... Mettre à jour le dossier - Cet email semble provenir d\'une adresse ProtonMail mais ne provient pas de notre système et a échoué aux exigences d\'authentification. Il a peut-être été falsifié ou transmis de manière incorrecte ! - Cet email a échoué aux tests d\'authentification de son domaine. Il a peut-être été falsifié ou transféré incorrectement ! + Ce courriel semble provenir d\'une adresse ProtonMail, mais ne provient pas de notre système et a échoué aux exigences d\'authentification. Il a peut-être été falsifié ou transmis de manière incorrecte ! + Ce courriel a échoué aux tests d\'authentification de son domaine. Il a peut-être été falsifié ou transféré incorrectement ! Ce message peut être une tentative d’hameçonnage. Veuillez vérifier l’expéditeur et le contenu afin de vous assurer qu’ils sont légitimes. EN SAVOIR PLUS De : %s À : %s;   @@ -675,13 +675,13 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. En savoir plus Information Passez à une offre payante pour envoyer depuis votre adresse %s. - L’adresse email est manquante + L’adresse électronique est manquante Détails chiffrés du contact Télécharger les contacts locaux Ceci va télécharger les contacts présents sur votre appareil vers votre répertoire ProtonMail. Vous pourrez ensuite accéder à ces contacts sur vos autres appareils via ProtonMail. Actualiser Télécharger - L\'adresse email n’est pas valide pour certains de vos contacts + L\'adresse électronique n’est pas valide pour certains de vos contacts Contacts ProtonMail Contacts de l’appareil Date : %s @@ -735,8 +735,8 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. EN SAVOIR PLUS Pièces jointes Informe sur l\'état du téléchargement des pièces jointes - Emails - Notifications par email entrantes + Courriels + Notifications entrantes par courriel Compte État du compte Opérations en cours @@ -755,7 +755,7 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Ajouter une photo \ue91a Nom d\'affichage - \ue914 Ajouter une adresse email + \ue914 Ajouter une adresse électronique Aucun groupe Note Modifier le contact @@ -807,7 +807,7 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Êtes-vous sûr de vouloir supprimer les contacts sélectionnés ? Membres - Nombre maximal d’emails par groupe atteint + Nombre maximal de messages par groupe atteint Tout sélectionner Choisir un nom de groupe Changer la couleur @@ -818,11 +818,11 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Utiliser l\'appareil photo Veuillez fournir un nom de groupe - Limite de stockage atteinte. Impossible d\'envoyer ou de recevoir des emails. + Limite de stockage atteinte. Impossible d\'envoyer ou de recevoir des courriels. Appuyez pour en savoir plus. Avertissement Vous avez atteint 100 % de votre capacité de stockage. Vous - ne pourrez plus envoyer ni recevoir d\'emails jusqu\'à ce que vous supprimiez définitivement quelques emails ou que vous achetiez plus + ne pourrez plus envoyer ni recevoir de messages jusqu\'à ce que vous supprimiez définitivement quelques messages ou que vous achetiez plus de stockage. Vous avez atteint 90% de votre capacité de stockage. Pensez à libérer de l’espace ou à acheter plus de stockage avant de manquer de capacité. @@ -851,11 +851,11 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Offre Mode de paiement Gestion des mots de passe - Email de récupération + Adresse électronique de récupération Taille de la boîte mail %1$s / %2$s Adresses - Adresse email par défaut + Adresse électronique par défaut Adresse par défaut Nom d\'affichage & signature Nom d\'affichage @@ -897,7 +897,7 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Messagerie Confidentialité Téléchargement automatique des messages - Lorsque cette option sera activée, les corps des messages seront automatiquement téléchargés dès que vous recevrez la notification de réception d\'un message (ceci consommera plus de données). Si cette option n\'est pas activée, les corps des messages ne seront téléchargés qu\'à l\'ouverture des emails (note : quelle que soit l\'option choisie, si vous effacez le cache ou si vous vous déconnectez, vous ne pourrez pas lire les corps des messages lorsque vous êtes hors ligne). + Lorsque cette option sera activée, les corps des messages seront automatiquement téléchargés dès que vous recevrez la notification de réception d\'un message (ceci consommera plus de données). Si cette option n\'est pas activée, les corps des messages ne seront téléchargés qu\'à l\'ouverture des courriels (note : quelle que soit l\'option choisie, si vous effacez le cache ou si vous vous déconnectez, vous ne pourrez pas lire les corps des messages lorsque vous êtes hors ligne). Synchronisation en arrière-plan Afficher automatiquement les images distantes Afficher automatiquement les images incorporées @@ -937,7 +937,7 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Notifications push Notifications étendues - Activez cette fonctionnalité pour voir l\'expéditeur\ndes emails dans les notifications push. + Activez cette fonctionnalité pour voir l\'expéditeur\ndes courriels dans les notifications push. Paramètres de notification Verrouillage automatique Verrouillage automatique de l\'application @@ -952,7 +952,7 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Sélectionner la langue de l\'application Détection automatique Contacts combinés - Activez cette fonctionnalité pour compléter automatiquement les adresses email en utilisant les contacts de tous vos comptes connectés. + Activez cette fonctionnalité pour compléter automatiquement les adresses électroniques en utilisant les contacts de tous vos comptes connectés. Gestion du cache local Le cache local est actualisé Vider @@ -978,13 +978,13 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Problèmes de connexion fréquents et solutions - <i>Aucune connexion internet</i> : veuillez vous assurer que votre connexion internet fonctionne. + <i>Aucune connexion Internet</i> : veuillez vous assurer que votre connexion Internet fonctionne. <br/><br/><i>Problème de fournisseur d\'accès Internet (FAI)</i> : essayez de vous connecter à Proton à partir d\'un réseau différent (ou utilisez <a href=\"https://protonvpn.com/\">ProtonVPN</a> ou encore <a href=\"https://www.torproject.org/\">Tor</a>). <br/><br/><i>Blocage gouvernemental</i> : votre pays bloque peut-être l\'accès à Proton. Essayez <a href=\"https://protonvpn.com/\">ProtonVPN</a> (ou tout autre VPN) ou <a href=\"https://www.torproject.org/\">Tor</a> pour accéder à Proton. <br/><br/><i>Interférences antivirus</i> : désactivez ou supprimez temporairement votre logiciel antivirus. <br/><br/><i>Interférences proxy/pare-feu</i> : désactivez tout proxy ou pare-feu, ou contactez votre administrateur réseau. <br/><br/><i>Proton est en panne</i> : vérifiez le <a href=\"http://protonstatus.com/\">Statut Proton</a> pour connaître l\'état de notre système. - <br/><br/><i>Vous ne trouvez pas de solution</i> : contactez-nous directement via notre <a href=\"https://protonmail.com/support-form\">formulaire d’assistance</a>, <a href=\"mailto:support@protonmail.com\">par email</a> (support@protonmail. ), ou via <a href=\"https://twitter.com/ProtonMail\">Twitter</a>. + <br/><br/><i>Vous ne trouvez pas de solution</i> : contactez-nous directement via notre <a href=\"https://protonmail.com/support-form\">formulaire d’assistance</a>, <a href=\"mailto:support@protonmail.com\">par courriel</a> (support@protonmail. ), ou via <a href=\"https://twitter.com/ProtonMail\">Twitter</a>. diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index b22202fdf..4df68664c 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -117,7 +117,7 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Datum: DETALJI SAKRIJ POJEDINOSTI - Odgovor + Odgovori Odgovori svima Proslijedi Predmet @@ -984,7 +984,7 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Odjavili ste se s %1$s i prijavili s %2$s Ograničenje je dosegnuto Ne možete dodati još jedan besplatni ProtonMail račun. Može se dodati samo jedan besplatni ProtonMail račun. Odjavite se sa svog drugog besplatnog računa ili nadogradite jedan od svojih besplatnih računa na plaćeni račun. - Promjeni račun? + Promijeni račun? Prebacite se na %s Račun koji pokušavate dodati nema stvorene ključeve. Dodajte ovaj račun kao prvi račun u aplikaciji ili se prijavite putem web aplikacije da biste stvorili ključeve, a zatim ga dodajte kao dodatni račun u aplikaciji. From a32cd2d30a21770960c74f58768b24101954522b Mon Sep 17 00:00:00 2001 From: Tomasz Giszczak Date: Fri, 18 Dec 2020 15:36:39 +0100 Subject: [PATCH 067/145] Added on text change listener to mobile signature setting trying to prevent it from not saving the mobile signature when user presses e.g. home button MAILAND-1152 --- .../activities/EditSettingsItemActivity.kt | 54 +++++++++++-------- .../settings/BaseSettingsActivity.kt | 10 +++- .../android/adapters/SettingsAdapter.kt | 32 +++++------ .../protonmail/android/api/models/User.java | 6 +-- .../android/uiModel/SettingsItemUiModel.kt | 4 +- .../android/views/SettingsDefaultItemView.kt | 31 +++++++---- 6 files changed, 83 insertions(+), 54 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/activities/EditSettingsItemActivity.kt b/app/src/main/java/ch/protonmail/android/activities/EditSettingsItemActivity.kt index c0aca47d5..5c65eba6d 100644 --- a/app/src/main/java/ch/protonmail/android/activities/EditSettingsItemActivity.kt +++ b/app/src/main/java/ch/protonmail/android/activities/EditSettingsItemActivity.kt @@ -18,12 +18,10 @@ */ package ch.protonmail.android.activities -import android.annotation.SuppressLint import android.app.Activity import android.content.DialogInterface import android.content.Intent import android.os.Bundle -import android.text.TextUtils import android.view.Gravity import android.view.MenuItem import android.view.View @@ -57,6 +55,7 @@ import com.google.gson.Gson import com.squareup.otto.Subscribe import dagger.hilt.android.AndroidEntryPoint import kotlinx.android.synthetic.main.activity_edit_settings_item.* +import timber.log.Timber // region constants const val EXTRA_SETTINGS_ITEM_TYPE = "EXTRA_SETTINGS_ITEM_TYPE" @@ -106,12 +105,11 @@ class EditSettingsItemActivity : BaseSettingsActivity() { renderViews() } - private val isValidNewConfirmEmail: Boolean get() { val newRecoveryEmail = newRecoveryEmail!!.text.toString().trim() val newConfirmRecoveryEmail = newRecoveryEmailConfirm!!.text.toString().trim() - return if (TextUtils.isEmpty(newRecoveryEmail) && TextUtils.isEmpty(newConfirmRecoveryEmail)) { + return if (newRecoveryEmail.isEmpty() && newConfirmRecoveryEmail.isEmpty()) { true } else newRecoveryEmail == newConfirmRecoveryEmail && newRecoveryEmail.isValidEmail() } @@ -145,14 +143,13 @@ class EditSettingsItemActivity : BaseSettingsActivity() { } } - @SuppressLint("LogNotTimber") override fun renderViews() { when (settingsItemType) { SettingsItem.RECOVERY_EMAIL -> { settingsRecyclerViewParent.visibility = View.GONE recoveryEmailValue = settingsItemValue - if (!TextUtils.isEmpty(recoveryEmailValue)) { + if (!recoveryEmailValue.isNullOrEmpty()) { (currentRecoveryEmail as TextView).text = recoveryEmailValue } else { (currentRecoveryEmail as TextView).text = getString(R.string.not_set) @@ -166,9 +163,8 @@ class EditSettingsItemActivity : BaseSettingsActivity() { mSelectedAddress = user.addresses[0] val newAddressId = user.defaultAddress.id - val currentSignature = mSelectedAddress.signature - if (!TextUtils.isEmpty(mDisplayName)) { + if (mDisplayName.isNotEmpty()) { setValue(SettingsEnum.DISPLAY_NAME, mDisplayName) } @@ -191,20 +187,26 @@ class EditSettingsItemActivity : BaseSettingsActivity() { mUserManager.user = user mDisplayName = newDisplayName - val job = UpdateSettingsJob(displayChanged = displayChanged, newDisplayName = newDisplayName, addressId = newAddressId) + val job = UpdateSettingsJob( + displayChanged = displayChanged, + newDisplayName = newDisplayName, + addressId = newAddressId + ) mJobManager.addJobInBackground(job) } } - if (!TextUtils.isEmpty(currentSignature)) { - setValue(SettingsEnum.SIGNATURE, currentSignature!!) + val currentSignature = mSelectedAddress.signature + if (!currentSignature.isNullOrEmpty()) { + setValue(SettingsEnum.SIGNATURE, currentSignature) } setEnabled(SettingsEnum.SIGNATURE, user.isShowSignature) val currentMobileSignature = user.mobileSignature - if (!TextUtils.isEmpty(currentMobileSignature)) { - setValue(SettingsEnum.MOBILE_SIGNATURE, currentMobileSignature!!) + if (!currentMobileSignature.isNullOrEmpty()) { + Timber.v("set mobileSignature $currentMobileSignature") + setValue(SettingsEnum.MOBILE_SIGNATURE, currentMobileSignature) } if (user.isPaidUserSignatureEdit) { setEnabled(SettingsEnum.MOBILE_SIGNATURE, user.isShowMobileSignature) @@ -219,13 +221,17 @@ class EditSettingsItemActivity : BaseSettingsActivity() { setEditTextListener(SettingsEnum.SIGNATURE) { val newSignature = (it as CustomFontEditText).text.toString() - val signatureChanged = newSignature != currentSignature + val isSignatureChanged = newSignature != currentSignature user.save() mUserManager.user = user - if (signatureChanged) { - val job = UpdateSettingsJob(signatureChanged = signatureChanged, newSignature = newSignature, addressId = newAddressId) + if (isSignatureChanged) { + val job = UpdateSettingsJob( + signatureChanged = isSignatureChanged, + newSignature = newSignature, + addressId = newAddressId + ) mJobManager.addJobInBackground(job) } } @@ -239,9 +245,10 @@ class EditSettingsItemActivity : BaseSettingsActivity() { setEditTextListener(SettingsEnum.MOBILE_SIGNATURE) { val newMobileSignature = (it as CustomFontEditText).text.toString() - val mobileSignatureChanged = newMobileSignature != currentMobileSignature + val isMobileSignatureChanged = newMobileSignature != currentMobileSignature - if (mobileSignatureChanged) { + if (isMobileSignatureChanged) { + Timber.v("save mobileSignature $newMobileSignature") user.mobileSignature = newMobileSignature user.save() @@ -249,6 +256,12 @@ class EditSettingsItemActivity : BaseSettingsActivity() { } } + setEditTextChangeListener(SettingsEnum.MOBILE_SIGNATURE) { newMobileSignature -> + Timber.v("text change save mobileSignature $newMobileSignature") + user.mobileSignature = newMobileSignature + user.save() + mUserManager.user = user + } setToggleListener(SettingsEnum.MOBILE_SIGNATURE) { _: View, isChecked: Boolean -> user.isShowMobileSignature = isChecked @@ -257,7 +270,6 @@ class EditSettingsItemActivity : BaseSettingsActivity() { mUserManager.user = user } - actionBarTitle = R.string.display_name_n_signature } SettingsItem.PRIVACY -> { @@ -512,7 +524,7 @@ class EditSettingsItemActivity : BaseSettingsActivity() { val user = mUserManager.user if (settingsItemType == SettingsItem.RECOVERY_EMAIL) { settingsItemValue = recoveryEmailValue - if (TextUtils.isEmpty(recoveryEmailValue)) { + if (recoveryEmailValue.isNullOrEmpty()) { mUserManager.userSettings!!.notificationEmail = resources.getString(R.string.not_set) } else { mUserManager.userSettings!!.notificationEmail = recoveryEmailValue @@ -575,7 +587,7 @@ class EditSettingsItemActivity : BaseSettingsActivity() { if (hasTwoFactor) { twoFactorString = twoFactorCode.text.toString() } - if (TextUtils.isEmpty(passString) || TextUtils.isEmpty(twoFactorString) && hasTwoFactor) { + if (passString.isEmpty() || twoFactorString.isEmpty() && hasTwoFactor) { showToast(R.string.password_not_valid, Toast.LENGTH_SHORT) newRecoveryEmail.setText("") newRecoveryEmailConfirm.setText("") diff --git a/app/src/main/java/ch/protonmail/android/activities/settings/BaseSettingsActivity.kt b/app/src/main/java/ch/protonmail/android/activities/settings/BaseSettingsActivity.kt index 7b86143fb..f702c44e5 100644 --- a/app/src/main/java/ch/protonmail/android/activities/settings/BaseSettingsActivity.kt +++ b/app/src/main/java/ch/protonmail/android/activities/settings/BaseSettingsActivity.kt @@ -348,8 +348,6 @@ abstract class BaseSettingsActivity : BaseConnectivityActivity() { attachmentStorageIntent.putExtra(AttachmentStorageActivity.EXTRA_SETTINGS_ATTACHMENT_STORAGE_VALUE, mAttachmentStorageValue) startActivityForResult(AppUtil.decorInAppIntent(attachmentStorageIntent), SettingsEnum.LOCAL_STORAGE_LIMIT.ordinal) } - - SettingsEnum.PUSH_NOTIFICATION -> { val privateNotificationsIntent = AppUtil.decorInAppIntent(Intent(this, EditSettingsItemActivity::class.java)) privateNotificationsIntent.putExtra(EXTRA_SETTINGS_ITEM_TYPE, SettingsItem.PUSH_NOTIFICATIONS) @@ -404,6 +402,9 @@ abstract class BaseSettingsActivity : BaseConnectivityActivity() { } } } + else -> { + Timber.v("Unhandled setting: ${settingsId.toUpperCase(Locale.ENGLISH)} selection") + } } } @@ -437,6 +438,11 @@ abstract class BaseSettingsActivity : BaseConnectivityActivity() { settingsAdapter.items.find { it.settingId == settingType.name.toLowerCase(Locale.ENGLISH) }?.apply { editTextListener = listener } } + protected fun setEditTextChangeListener(settingType: SettingsEnum, listener: (String) -> Unit) { + settingsAdapter.items.find { it.settingId == settingType.name.toLowerCase(Locale.ENGLISH) } + ?.apply { editTextChangeListener = listener } + } + protected fun setValue(settingType: SettingsEnum, settingValueNew: String) { settingsAdapter.items.find { it.settingId == settingType.name.toLowerCase(Locale.ENGLISH) }?.apply { settingValue = settingValueNew } } diff --git a/app/src/main/java/ch/protonmail/android/adapters/SettingsAdapter.kt b/app/src/main/java/ch/protonmail/android/adapters/SettingsAdapter.kt index 0627f25aa..951c49087 100644 --- a/app/src/main/java/ch/protonmail/android/adapters/SettingsAdapter.kt +++ b/app/src/main/java/ch/protonmail/android/adapters/SettingsAdapter.kt @@ -31,52 +31,51 @@ import ch.protonmail.android.views.CustomFontTextView import ch.protonmail.android.views.SettingsDefaultItemView import ch.protonmail.libs.core.ui.adapter.BaseAdapter import ch.protonmail.libs.core.ui.adapter.ClickableAdapter -import java.util.* +import java.util.Locale // region constants private const val VIEW_TYPE_SECTION = 0 private const val VIEW_TYPE_ITEM = 1 // endregion -internal class SettingsAdapter : BaseAdapter>(ModelsComparator) { +internal class SettingsAdapter : + BaseAdapter>(ModelsComparator) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - return parent.viewHolderForViewType(viewType) - } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = + parent.viewHolderForViewType(viewType) override fun getItemViewType(position: Int) = items[position].viewType private object ModelsComparator : BaseAdapter.ItemsComparator() { - - override fun areItemsTheSame(oldItem: SettingsItemUiModel, newItem: SettingsItemUiModel): Boolean { - return false - } + override fun areItemsTheSame(oldItem: SettingsItemUiModel, newItem: SettingsItemUiModel): Boolean = false } abstract class ViewHolder(itemView: View) : - ClickableAdapter.ViewHolder(itemView) + ClickableAdapter.ViewHolder(itemView) companion object { - fun getHeader(settingsId: String, context: Context): String { - return SettingsEnum.valueOf(settingsId).getHeader(context) - } + fun getHeader(settingsId: String, context: Context): String = + SettingsEnum.valueOf(settingsId).getHeader(context) } private class SectionViewHolder(itemView: View) : ViewHolder(itemView) { override fun onBind(item: SettingsItemUiModel) = with(itemView as CustomFontTextView) { super.onBind(item) //TODO after we receive translations for TURKISH remove excess toUpperCase methods - text = if (item.settingHeader.isNullOrEmpty()) getHeader(item.settingId.toUpperCase(Locale.ENGLISH), context).toUpperCase(Locale.ENGLISH) else item.settingHeader?.toUpperCase(Locale.ENGLISH) + text = if (item.settingHeader.isNullOrEmpty()) + getHeader(item.settingId.toUpperCase(Locale.ENGLISH), context).toUpperCase(Locale.ENGLISH) + else + item.settingHeader?.toUpperCase(Locale.ENGLISH) } } - internal class ItemViewHolder(itemView: View) : ViewHolder(itemView) { + class ItemViewHolder(itemView: View) : ViewHolder(itemView) { lateinit var header: String override fun onBind(item: SettingsItemUiModel) = with(itemView as SettingsDefaultItemView) { super.onBind(item) - header = if (item.settingHeader.isNullOrEmpty()){ + header = if (item.settingHeader.isNullOrEmpty()) { getHeader(item.settingId.toUpperCase(Locale.ENGLISH), context) } else { item.settingHeader!! @@ -96,6 +95,7 @@ internal class SettingsAdapter : BaseAdapter Unit)? = { _: View, _: Boolean -> } var editTextListener: (View) -> Unit = {} + var editTextChangeListener: (String) -> Unit = {} constructor(settingId: String, settingHeader: String, @@ -60,6 +61,7 @@ class SettingsItemUiModel { this.settingDisabled = false this.toggleListener = { _: View, _: Boolean -> } this.editTextListener = {} + this.editTextChangeListener = {} } enum class SettingsItemTypeEnum { @@ -78,4 +80,4 @@ class SettingsItemUiModel { @SerializedName("toggle_n_edit") TOGGLE_N_EDIT } -} \ No newline at end of file +} diff --git a/app/src/main/java/ch/protonmail/android/views/SettingsDefaultItemView.kt b/app/src/main/java/ch/protonmail/android/views/SettingsDefaultItemView.kt index 500cd26b8..4e21a33c0 100644 --- a/app/src/main/java/ch/protonmail/android/views/SettingsDefaultItemView.kt +++ b/app/src/main/java/ch/protonmail/android/views/SettingsDefaultItemView.kt @@ -20,14 +20,18 @@ package ch.protonmail.android.views import android.content.Context import android.util.AttributeSet -import android.view.* +import android.view.Gravity +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup import androidx.appcompat.widget.SwitchCompat import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.core.view.doOnPreDraw +import androidx.core.widget.doAfterTextChanged import ch.protonmail.android.R import kotlinx.android.synthetic.main.settings_item_layout.view.* -import ch.protonmail.android.utils.extensions.ifNullElse // region constants private const val TYPE_INFO = 0 @@ -39,7 +43,11 @@ private const val TYPE_EDIT_TEXT = 5 private const val TYPE_TOGGLE_N_EDIT = 6 // endregion -class SettingsDefaultItemView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr) { +class SettingsDefaultItemView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { private var mAttrs: AttributeSet? = attrs private var mHeading: CharSequence? = "" @@ -132,11 +140,7 @@ class SettingsDefaultItemView @JvmOverloads constructor(context: Context, attrs: } valueText.visibility = View.VISIBLE - description.ifNullElse({ - mDescription = "" - }, { - mDescription = description.toString() - }) + mDescription = description ?: "" valueText.text = mDescription } } @@ -159,6 +163,12 @@ class SettingsDefaultItemView @JvmOverloads constructor(context: Context, attrs: } } + fun setEditTextOnTextChangeListener(listener: ((String) -> Unit)?) { + editText.doAfterTextChanged { + listener?.invoke(it.toString()) + } + } + fun setItemType(type: Int) { mType = type when (mType) { @@ -236,7 +246,10 @@ class SettingsDefaultItemView @JvmOverloads constructor(context: Context, attrs: if (v.id == R.id.editText) { v.parent.requestDisallowInterceptTouchEvent(true) when (event.action and MotionEvent.ACTION_MASK) { - MotionEvent.ACTION_UP -> v.parent.requestDisallowInterceptTouchEvent(false) + MotionEvent.ACTION_UP -> { + v.parent.requestDisallowInterceptTouchEvent(false) + v.performClick() + } } } false From 0357a1b456c05820c6a611df896d458adc91fc75 Mon Sep 17 00:00:00 2001 From: Tomasz Giszczak Date: Wed, 13 Jan 2021 21:16:27 +0000 Subject: [PATCH 068/145] Removed notifyDataSetChanged from onResume in BaseSettingsActivity. MAILAND-1152 --- .../android/activities/settings/BaseSettingsActivity.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/ch/protonmail/android/activities/settings/BaseSettingsActivity.kt b/app/src/main/java/ch/protonmail/android/activities/settings/BaseSettingsActivity.kt index f702c44e5..94c7f671a 100644 --- a/app/src/main/java/ch/protonmail/android/activities/settings/BaseSettingsActivity.kt +++ b/app/src/main/java/ch/protonmail/android/activities/settings/BaseSettingsActivity.kt @@ -151,7 +151,6 @@ abstract class BaseSettingsActivity : BaseConnectivityActivity() { override fun onResume() { super.onResume() user = mUserManager.user - settingsAdapter.notifyDataSetChanged() viewModel.checkConnectivity() } From 09a869e58b9e582bf705abe8f40a6eb441e7134c Mon Sep 17 00:00:00 2001 From: Denys Zelenchuk Date: Wed, 23 Dec 2020 16:57:47 +0100 Subject: [PATCH 069/145] Adds labels and folders ui tests. Affected: UI tests MAILAND-1057 --- .gitlab-ci.yml | 1 + .../robots/contacts/AddContactGroupRobot.kt | 4 +- .../robots/contacts/AddContactRobot.kt | 2 +- .../robots/contacts/ContactDetailsRobot.kt | 4 +- .../uitests/robots/contacts/ContactsRobot.kt | 36 +- .../robots/contacts/GroupDetailsRobot.kt | 4 +- .../robots/contacts/ManageAddressesRobot.kt | 6 +- .../uitests/robots/login/LoginRobot.kt | 6 +- .../robots/login/MailboxPasswordRobot.kt | 4 +- .../mailbox/ApplyLabelRobotInterface.kt | 39 +- .../uitests/robots/mailbox/MailboxMatchers.kt | 34 ++ .../robots/mailbox/MailboxRobotInterface.kt | 53 +- .../mailbox/MoveToFolderRobotInterface.kt | 2 +- .../mailbox/SelectionStateRobotInterface.kt | 7 +- .../robots/mailbox/composer/ComposerRobot.kt | 8 +- .../composer/MessageAttachmentsRobot.kt | 2 +- .../robots/mailbox/drafts/DraftsRobot.kt | 2 +- .../robots/mailbox/inbox/InboxRobot.kt | 2 +- .../mailbox/labelfolder/LabelFolderRobot.kt | 9 +- .../mailbox/messagedetail/MessageRobot.kt | 137 ++++- .../mailbox/messagedetail/ViewHeadersRobot.kt | 2 +- .../robots/mailbox/search/SearchRobot.kt | 19 +- .../uitests/robots/mailbox/sent/SentRobot.kt | 23 +- .../uitests/robots/mailbox/spam/SpamRobot.kt | 2 +- .../robots/mailbox/trash/TrashRobot.kt | 2 +- .../manageaccounts/AccountManagerRobot.kt | 22 +- .../manageaccounts/ConnectAccountRobot.kt | 4 +- .../android/uitests/robots/menu/MenuRobot.kt | 16 +- .../robots/reportbugs/ReportBugsRobot.kt | 2 +- .../uitests/robots/settings/SettingsRobot.kt | 22 +- .../settings/account/AccountSettingsRobot.kt | 11 +- .../account/ChooseSwipeActionRobot.kt | 3 +- .../account/DefaultEmailAddressRobot.kt | 2 +- .../account/DisplayNameAndSignatureRobot.kt | 2 +- .../settings/account/FoldersManagerRobot.kt | 73 ++- .../settings/account/LabelsAndFoldersRobot.kt | 2 +- .../settings/account/LabelsManagerRobot.kt | 47 +- .../account/PasswordManagementRobot.kt | 3 +- .../settings/account/RecoveryEmailRobot.kt | 4 +- .../settings/account/SubscriptionRobot.kt | 2 +- .../account/SwipingGesturesSettingsRobot.kt | 7 +- .../robots/settings/autolock/AutoLockRobot.kt | 4 +- .../robots/settings/autolock/PinRobot.kt | 4 +- .../upgradedonate/UpgradeDonateRobot.kt | 2 +- .../android/uitests/tests/inbox/InboxTests.kt | 2 +- .../tests/labelsfolders/LabelsFoldersTests.kt | 136 +++++ .../tests/settings/AccountSettingsTests.kt | 7 +- .../android/uitests/tests/spam/SpamTests.kt | 2 +- .../uitests/tests/suites/SmokeSuite.kt | 2 + .../testsHelper/MockAddAttachmentIntent.kt | 10 +- .../uitests/testsHelper/StringUtils.kt | 2 +- .../android/uitests/testsHelper/UIActions.kt | 470 ------------------ .../testsHelper/UICustomViewActions.kt | 3 +- .../uitests/testsHelper/uiactions/AllOf.kt | 91 ++++ .../uitests/testsHelper/uiactions/Check.kt | 88 ++++ .../uiactions/ContentDescription.kt | 31 ++ .../testsHelper/uiactions/Extensions.kt | 39 ++ .../uitests/testsHelper/uiactions/Hint.kt | 31 ++ .../uitests/testsHelper/uiactions/Id.kt | 57 +++ .../uitests/testsHelper/uiactions/List.kt | 63 +++ .../uitests/testsHelper/uiactions/Recycler.kt | 169 +++++++ .../uitests/testsHelper/uiactions/System.kt | 57 +++ .../uitests/testsHelper/uiactions/Tag.kt | 33 ++ .../uitests/testsHelper/uiactions/Text.kt | 36 ++ .../testsHelper/uiactions/UIActions.kt | 39 ++ .../uitests/testsHelper/uiactions/Wait.kt | 112 +++++ 66 files changed, 1473 insertions(+), 649 deletions(-) create mode 100644 app/src/uiTest/kotlin/ch/protonmail/android/uitests/tests/labelsfolders/LabelsFoldersTests.kt delete mode 100644 app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/UIActions.kt create mode 100644 app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/AllOf.kt create mode 100644 app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Check.kt create mode 100644 app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/ContentDescription.kt create mode 100644 app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Extensions.kt create mode 100644 app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Hint.kt create mode 100644 app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Id.kt create mode 100644 app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/List.kt create mode 100644 app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Recycler.kt create mode 100644 app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/System.kt create mode 100644 app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Tag.kt create mode 100644 app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Text.kt create mode 100644 app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/UIActions.kt create mode 100644 app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Wait.kt diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ec76303b3..c779d5812 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -91,6 +91,7 @@ build debug develop: - job: detekt analysis only: - develop + - schedules script: - ./gradlew clean - ./gradlew assembleBetaDebug diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/contacts/AddContactGroupRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/contacts/AddContactGroupRobot.kt index bcd6cf90a..d273f5e67 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/contacts/AddContactGroupRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/contacts/AddContactGroupRobot.kt @@ -20,8 +20,8 @@ package ch.protonmail.android.uitests.robots.contacts import ch.protonmail.android.R -import ch.protonmail.android.uitests.testsHelper.UIActions -import ch.protonmail.android.uitests.testsHelper.insert +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.insert /** * [AddContactGroupRobot] class contains actions and verifications for Add/Edit Contact Groups. diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/contacts/AddContactRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/contacts/AddContactRobot.kt index dce901877..f2ecbcca4 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/contacts/AddContactRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/contacts/AddContactRobot.kt @@ -20,7 +20,7 @@ package ch.protonmail.android.uitests.robots.contacts import ch.protonmail.android.R -import ch.protonmail.android.uitests.testsHelper.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions /** * [AddContactRobot] class contains actions and verifications for Add/Edit Contacts. diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/contacts/ContactDetailsRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/contacts/ContactDetailsRobot.kt index 5a83c731e..733e792da 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/contacts/ContactDetailsRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/contacts/ContactDetailsRobot.kt @@ -20,8 +20,8 @@ package ch.protonmail.android.uitests.robots.contacts import androidx.appcompat.widget.AppCompatImageButton import ch.protonmail.android.R -import ch.protonmail.android.uitests.testsHelper.UIActions -import ch.protonmail.android.uitests.testsHelper.click +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.click /** * [ContactDetailsRobot] class contains actions and verifications for Contacts functionality. diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/contacts/ContactsRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/contacts/ContactsRobot.kt index 9ce77b616..6df8d0ce8 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/contacts/ContactsRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/contacts/ContactsRobot.kt @@ -25,8 +25,8 @@ import ch.protonmail.android.uitests.robots.contacts.ContactsMatchers.withContac import ch.protonmail.android.uitests.robots.contacts.ContactsMatchers.withContactNameAndEmail import ch.protonmail.android.uitests.robots.mailbox.composer.ComposerRobot import ch.protonmail.android.uitests.robots.mailbox.inbox.InboxRobot -import ch.protonmail.android.uitests.testsHelper.UIActions -import ch.protonmail.android.uitests.testsHelper.click +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.click import com.github.clans.fab.FloatingActionButton import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.instanceOf @@ -83,15 +83,15 @@ class ContactsRobot { fun clickContactByEmail(email: String): ContactDetailsRobot { UIActions.wait.forViewWithId(contactsRecyclerView) UIActions.recyclerView - .waitForBeingPopulated(contactsRecyclerView) - .clickContactItemWithRetry(contactsRecyclerView, email) + .common.waitForBeingPopulated(contactsRecyclerView) + .contacts.clickContactItemWithRetry(contactsRecyclerView, email) return ContactDetailsRobot() } inner class ContactsView { fun clickContact(withEmail: String): ContactDetailsRobot { - UIActions.recyclerView.clickContactItem(contactsRecyclerView, withEmail) + UIActions.recyclerView.contacts.clickContactItem(contactsRecyclerView, withEmail) return ContactDetailsRobot() } @@ -103,8 +103,8 @@ class ContactsRobot { fun clickSendMessageToContact(contactName: String): ComposerRobot { UIActions.recyclerView - .waitForBeingPopulated(contactsRecyclerView) - .clickContactItemView( + .common.waitForBeingPopulated(contactsRecyclerView) + .contacts.clickContactItemView( contactsRecyclerView, contactName, R.id.writeButton @@ -123,16 +123,16 @@ class ContactsRobot { fun clickGroup(withName: String): GroupDetailsRobot { UIActions.recyclerView - .waitForBeingPopulated(R.id.contactGroupsRecyclerView) - .clickContactsGroupItem(R.id.contactGroupsRecyclerView, withName) + .common.waitForBeingPopulated(R.id.contactGroupsRecyclerView) + .contacts.clickContactsGroupItem(R.id.contactGroupsRecyclerView, withName) return GroupDetailsRobot() } fun clickGroupWithMembersCount(name: String, membersCount: String): GroupDetailsRobot { UIActions.wait.forViewWithId(contactGroupsRecyclerView) UIActions.recyclerView - .waitForBeingPopulated(contactGroupsRecyclerView) - .clickOnRecyclerViewMatchedItemWithRetry( + .common.waitForBeingPopulated(contactGroupsRecyclerView) + .common.clickOnRecyclerViewMatchedItemWithRetry( contactGroupsRecyclerView, withContactGroupNameAndMembersCount(name, membersCount) ) @@ -140,7 +140,7 @@ class ContactsRobot { } fun clickSendMessageToGroup(groupName: String): ComposerRobot { - UIActions.recyclerView.clickContactsGroupItemView( + UIActions.recyclerView.contacts.clickContactsGroupItemView( R.id.contactGroupsRecyclerView, groupName, R.id.writeButton) @@ -150,8 +150,8 @@ class ContactsRobot { class Verify { fun groupWithMembersCountExists(name: String, membersCount: String) { UIActions.recyclerView - .waitForBeingPopulated(contactGroupsRecyclerView) - .scrollToRecyclerViewMatchedItem( + .common.waitForBeingPopulated(contactGroupsRecyclerView) + .common.scrollToRecyclerViewMatchedItem( contactGroupsRecyclerView, withContactGroupNameAndMembersCount(name, membersCount) ) @@ -159,8 +159,8 @@ class ContactsRobot { fun groupDoesNotExists(name: String, membersCount: String) { UIActions.recyclerView - .waitForBeingPopulated(contactGroupsRecyclerView) - .scrollToRecyclerViewMatchedItem( + .common.waitForBeingPopulated(contactGroupsRecyclerView) + .common.scrollToRecyclerViewMatchedItem( contactGroupsRecyclerView, withContactGroupNameAndMembersCount(name, membersCount) ) @@ -189,13 +189,13 @@ class ContactsRobot { fun contactExists(name: String, email: String) { UIActions.recyclerView - .scrollToRecyclerViewMatchedItem(contactsRecyclerView, withContactNameAndEmail(name, email)) + .common.scrollToRecyclerViewMatchedItem(contactsRecyclerView, withContactNameAndEmail(name, email)) } fun contactDoesNotExists(name: String, email: String) { UIActions.wait.forViewWithId(contactsRecyclerView) UIActions.recyclerView - .checkDoesNotContainContact(contactsRecyclerView, name, email) + .contacts.checkDoesNotContainContact(contactsRecyclerView, name, email) } fun contactsRefreshed() { diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/contacts/GroupDetailsRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/contacts/GroupDetailsRobot.kt index 7f1c64b89..44360b083 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/contacts/GroupDetailsRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/contacts/GroupDetailsRobot.kt @@ -19,8 +19,8 @@ package ch.protonmail.android.uitests.robots.contacts import ch.protonmail.android.R -import ch.protonmail.android.uitests.testsHelper.UIActions -import ch.protonmail.android.uitests.testsHelper.click +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.click /** * [GroupDetailsRobot] class contains actions and verifications for Contacts functionality. diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/contacts/ManageAddressesRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/contacts/ManageAddressesRobot.kt index 3f5d3e4e8..ae061831d 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/contacts/ManageAddressesRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/contacts/ManageAddressesRobot.kt @@ -20,7 +20,7 @@ package ch.protonmail.android.uitests.robots.contacts import ch.protonmail.android.R -import ch.protonmail.android.uitests.testsHelper.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions /** * [ManageAddressesRobot] class contains actions and verifications for Adding a Contact to Group. @@ -31,8 +31,8 @@ class ManageAddressesRobot { private fun selectAddress(email: String): ManageAddressesRobot { UIActions.recyclerView - .waitForBeingPopulated(contactsRecyclerView) - .selectContactsInManageAddresses(contactsRecyclerView, email) + .common.waitForBeingPopulated(contactsRecyclerView) + .contacts.selectContactsInManageAddresses(contactsRecyclerView, email) return this } diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/login/LoginRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/login/LoginRobot.kt index dc4b5b332..81fec04f1 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/login/LoginRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/login/LoginRobot.kt @@ -20,10 +20,10 @@ package ch.protonmail.android.uitests.robots.login import ch.protonmail.android.R import ch.protonmail.android.uitests.robots.mailbox.inbox.InboxRobot -import ch.protonmail.android.uitests.testsHelper.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions import ch.protonmail.android.uitests.testsHelper.User -import ch.protonmail.android.uitests.testsHelper.click -import ch.protonmail.android.uitests.testsHelper.insert +import ch.protonmail.android.uitests.testsHelper.uiactions.click +import ch.protonmail.android.uitests.testsHelper.uiactions.insert /** * [LoginRobot] class contains actions and verifications for login functionality. diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/login/MailboxPasswordRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/login/MailboxPasswordRobot.kt index 4a7089535..32194f8de 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/login/MailboxPasswordRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/login/MailboxPasswordRobot.kt @@ -21,8 +21,8 @@ package ch.protonmail.android.uitests.robots.login import ch.protonmail.android.R import ch.protonmail.android.uitests.robots.mailbox.inbox.InboxRobot -import ch.protonmail.android.uitests.testsHelper.UIActions -import ch.protonmail.android.uitests.testsHelper.insert +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.insert class MailboxPasswordRobot { diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/ApplyLabelRobotInterface.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/ApplyLabelRobotInterface.kt index 8b0c00a26..31750c3be 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/ApplyLabelRobotInterface.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/ApplyLabelRobotInterface.kt @@ -18,21 +18,44 @@ */ package ch.protonmail.android.uitests.robots.mailbox +import ch.protonmail.android.R +import ch.protonmail.android.uitests.robots.mailbox.MailboxMatchers.withLabelName +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.click +import ch.protonmail.android.uitests.testsHelper.uiactions.type + interface ApplyLabelRobotInterface { - fun labelName(name: String) { - //TODO add implementation + fun addLabel(name: String): Any { + labelName(name) + .add() + return this + } + + fun labelName(name: String): ApplyLabelRobotInterface { + UIActions.wait + .forViewWithIdAndParentId(R.id.label_name, R.id.add_label_container) + .type(name) + return this } - fun selectExistingByName(name: String) { - //TODO add implementation + fun selectLabelByName(name: String): ApplyLabelRobotInterface { + UIActions.wait.forViewWithId(R.id.labels_list_view) + UIActions.wait.forViewWithText(name) + UIActions.listView.clickListItemChildByTextAndId( + withLabelName(name), + R.id.label_check, + R.id.labels_list_view + ) + return this } - fun selectAlsoArchive() { - //TODO add implementation + fun apply(): Any { + UIActions.wait.forViewWithId(R.id.done).click() + return this } - fun apply() { - //TODO add implementation + fun add() { + UIActions.wait.forViewWithId(R.id.done).click() } } diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/MailboxMatchers.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/MailboxMatchers.kt index 8161ded11..92bde95cb 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/MailboxMatchers.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/MailboxMatchers.kt @@ -22,10 +22,14 @@ import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import androidx.test.espresso.matcher.BoundedMatcher import ch.protonmail.android.R +import ch.protonmail.android.adapters.FoldersAdapter +import ch.protonmail.android.adapters.LabelsAdapter import ch.protonmail.android.adapters.messages.MessagesListViewHolder import ch.protonmail.android.views.messagesList.MessagesListItemView +import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.Description import org.hamcrest.Matcher +import org.hamcrest.TypeSafeMatcher /** * Matchers that are used by Mailbox features like Inbox, Sent, Drafts, Trash, etc. @@ -161,4 +165,34 @@ object MailboxMatchers { } } } + + fun withFolderName(name: String): TypeSafeMatcher = + withFolderName(equalTo(name)) + + fun withFolderName(nameMatcher: Matcher): TypeSafeMatcher { + return object : TypeSafeMatcher(FoldersAdapter.FolderItem::class.java) { + override fun matchesSafely(item: FoldersAdapter.FolderItem): Boolean { + return nameMatcher.matches(item.name) + } + + override fun describeTo(description: Description) { + description.appendText("with item content: ") + } + } + } + + fun withLabelName(name: String): TypeSafeMatcher = + withLabelName(equalTo(name)) + + fun withLabelName(nameMatcher: Matcher): TypeSafeMatcher { + return object : TypeSafeMatcher(LabelsAdapter.LabelItem::class.java) { + override fun matchesSafely(item: LabelsAdapter.LabelItem): Boolean { + return nameMatcher.matches(item.name) + } + + override fun describeTo(description: Description) { + description.appendText("with item content: ") + } + } + } } diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/MailboxRobotInterface.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/MailboxRobotInterface.kt index 902d3e070..30d056812 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/MailboxRobotInterface.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/MailboxRobotInterface.kt @@ -33,9 +33,9 @@ import ch.protonmail.android.uitests.robots.mailbox.inbox.InboxRobot import ch.protonmail.android.uitests.robots.mailbox.messagedetail.MessageRobot import ch.protonmail.android.uitests.robots.mailbox.search.SearchRobot import ch.protonmail.android.uitests.robots.menu.MenuRobot -import ch.protonmail.android.uitests.testsHelper.UIActions -import ch.protonmail.android.uitests.testsHelper.click -import ch.protonmail.android.uitests.testsHelper.swipeViewDown +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.click +import ch.protonmail.android.uitests.testsHelper.uiactions.swipeViewDown import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.instanceOf @@ -43,25 +43,25 @@ interface MailboxRobotInterface { fun swipeLeftMessageAtPosition(position: Int): Any { UIActions.recyclerView - .waitForBeingPopulated(messagesRecyclerViewId) - .saveMessageSubjectAtPosition(messagesRecyclerViewId, position, (::SetSwipeLeftMessage)()) - UIActions.recyclerView.swipeRightToLeftObjectWithIdAtPosition(messagesRecyclerViewId, position) + .common.waitForBeingPopulated(messagesRecyclerViewId) + .messages.saveMessageSubjectAtPosition(messagesRecyclerViewId, position, (::SetSwipeLeftMessage)()) + UIActions.recyclerView.common.swipeRightToLeftObjectWithIdAtPosition(messagesRecyclerViewId, position) return Any() } fun longClickMessageOnPosition(position: Int): Any { UIActions.recyclerView - .waitForBeingPopulated(messagesRecyclerViewId) - .saveMessageSubjectAtPosition(messagesRecyclerViewId, position, (::SetLongClickMessage)()) - UIActions.recyclerView.longClickItemInRecyclerView(messagesRecyclerViewId, position) + .common.waitForBeingPopulated(messagesRecyclerViewId) + .messages.saveMessageSubjectAtPosition(messagesRecyclerViewId, position, (::SetLongClickMessage)()) + UIActions.recyclerView.common.longClickItemInRecyclerView(messagesRecyclerViewId, position) return Any() } fun deleteMessageWithSwipe(position: Int): Any { UIActions.recyclerView - .waitForBeingPopulated(messagesRecyclerViewId) - .saveMessageSubjectAtPosition(messagesRecyclerViewId, position, (::SetDeleteWithSwipeMessage)()) - UIActions.recyclerView.swipeItemLeftToRightOnPosition(messagesRecyclerViewId, position) + .common.waitForBeingPopulated(messagesRecyclerViewId) + .messages.saveMessageSubjectAtPosition(messagesRecyclerViewId, position, (::SetDeleteWithSwipeMessage)()) + UIActions.recyclerView.common.swipeItemLeftToRightOnPosition(messagesRecyclerViewId, position) return Any() } @@ -83,8 +83,8 @@ interface MailboxRobotInterface { fun clickMessageByPosition(position: Int): MessageRobot { UIActions.wait.forViewWithId(messagesRecyclerViewId) - UIActions.recyclerView.saveMessageSubjectAtPosition(messagesRecyclerViewId, position, (::SetSelectMessage)()) - UIActions.recyclerView.clickOnRecyclerViewItemByPosition(messagesRecyclerViewId, 1) + UIActions.recyclerView.messages.saveMessageSubjectAtPosition(messagesRecyclerViewId, position, (::SetSelectMessage)()) + UIActions.recyclerView.common.clickOnRecyclerViewItemByPosition(messagesRecyclerViewId, 1) return MessageRobot() } @@ -99,8 +99,8 @@ interface MailboxRobotInterface { ) UIActions.wait.forViewWithId(messagesRecyclerViewId) UIActions.recyclerView - .waitForBeingPopulated(messagesRecyclerViewId) - .clickOnRecyclerViewMatchedItem(messagesRecyclerViewId, withMessageSubject(subject)) + .common.waitForBeingPopulated(messagesRecyclerViewId) + .common.clickOnRecyclerViewMatchedItem(messagesRecyclerViewId, withMessageSubject(subject)) return MessageRobot() } @@ -115,28 +115,36 @@ interface MailboxRobotInterface { @Suppress("ClassName") open class verify { + fun messageExists(messageSubject: String) { + UIActions.wait.forViewWithIdAndText(messageTitleTextViewId, messageSubject) + } + + fun draftWithAttachmentSaved(draftSubject: String) { + UIActions.wait.forViewWithIdAndText(messageTitleTextViewId, draftSubject) + } + fun mailboxLayoutShown() { UIActions.wait.forViewWithId(R.id.swipe_refresh_layout) } fun messageDeleted(subject: String, date: String) { UIActions.recyclerView - .waitForBeingPopulated(messagesRecyclerViewId) - .checkDoesNotContainMessage(messagesRecyclerViewId, subject, date) + .common.waitForBeingPopulated(messagesRecyclerViewId) + .messages.checkDoesNotContainMessage(messagesRecyclerViewId, subject, date) } fun messageWithSubjectExists(subject: String) { - UIActions.recyclerView.waitForBeingPopulated(messagesRecyclerViewId) + UIActions.recyclerView.common.waitForBeingPopulated(messagesRecyclerViewId) UIActions.wait.forViewWithText(subject) UIActions.recyclerView - .scrollToRecyclerViewMatchedItem(messagesRecyclerViewId, withFirstInstanceMessageSubject(subject)) + .common.scrollToRecyclerViewMatchedItem(messagesRecyclerViewId, withFirstInstanceMessageSubject(subject)) } fun messageWithSubjectAndRecipientExists(subject: String, to: String) { - UIActions.recyclerView.waitForBeingPopulated(messagesRecyclerViewId) + UIActions.recyclerView.common.waitForBeingPopulated(messagesRecyclerViewId) UIActions.wait.forViewWithText(subject) UIActions.recyclerView - .scrollToRecyclerViewMatchedItem(messagesRecyclerViewId, withMessageSubjectAndRecipient(subject, to)) + .common.scrollToRecyclerViewMatchedItem(messagesRecyclerViewId, withMessageSubjectAndRecipient(subject, to)) } } @@ -179,6 +187,7 @@ interface MailboxRobotInterface { var deletedMessageDate = "" private const val messagesRecyclerViewId = R.id.messages_list_view + private const val messageTitleTextViewId = R.id.messageTitleTextView private const val drawerLayoutId = R.id.drawer_layout } } diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/MoveToFolderRobotInterface.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/MoveToFolderRobotInterface.kt index 7b1efd26c..514120b7e 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/MoveToFolderRobotInterface.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/MoveToFolderRobotInterface.kt @@ -19,7 +19,7 @@ package ch.protonmail.android.uitests.robots.mailbox import ch.protonmail.android.R -import ch.protonmail.android.uitests.testsHelper.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions interface MoveToFolderRobotInterface { diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/SelectionStateRobotInterface.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/SelectionStateRobotInterface.kt index 3259182ad..335fb217d 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/SelectionStateRobotInterface.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/SelectionStateRobotInterface.kt @@ -19,8 +19,8 @@ package ch.protonmail.android.uitests.robots.mailbox import ch.protonmail.android.R -import ch.protonmail.android.uitests.testsHelper.UIActions -import ch.protonmail.android.uitests.testsHelper.click +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.click interface SelectionStateRobotInterface { @@ -35,6 +35,7 @@ interface SelectionStateRobotInterface { } fun addLabel(): Any { + UIActions.wait.forViewWithId(R.id.add_label).click() return Any() } @@ -44,7 +45,7 @@ interface SelectionStateRobotInterface { } fun selectMessage(position: Int): Any { - UIActions.recyclerView.clickOnRecyclerViewItemByPosition(R.id.messages_list_view, position) + UIActions.recyclerView.common.clickOnRecyclerViewItemByPosition(R.id.messages_list_view, position) return Any() } } diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/composer/ComposerRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/composer/ComposerRobot.kt index 61d84f805..5a1b2adcc 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/composer/ComposerRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/composer/ComposerRobot.kt @@ -30,12 +30,12 @@ import ch.protonmail.android.uitests.robots.mailbox.drafts.DraftsRobot import ch.protonmail.android.uitests.robots.mailbox.inbox.InboxRobot import ch.protonmail.android.uitests.robots.mailbox.messagedetail.MessageRobot import ch.protonmail.android.uitests.testsHelper.TestData -import ch.protonmail.android.uitests.testsHelper.UIActions import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.TIMEOUT_15S import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.setValueInNumberPicker -import ch.protonmail.android.uitests.testsHelper.click -import ch.protonmail.android.uitests.testsHelper.insert -import ch.protonmail.android.uitests.testsHelper.type +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.click +import ch.protonmail.android.uitests.testsHelper.uiactions.insert +import ch.protonmail.android.uitests.testsHelper.uiactions.type import org.hamcrest.CoreMatchers.`is` import org.hamcrest.CoreMatchers.allOf diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/composer/MessageAttachmentsRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/composer/MessageAttachmentsRobot.kt index 3318ba0f8..018791537 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/composer/MessageAttachmentsRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/composer/MessageAttachmentsRobot.kt @@ -22,7 +22,7 @@ import androidx.annotation.IdRes import androidx.appcompat.widget.AppCompatImageButton import ch.protonmail.android.R import ch.protonmail.android.uitests.testsHelper.MockAddAttachmentIntent -import ch.protonmail.android.uitests.testsHelper.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions /** * Class represents Message Attachments. diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/drafts/DraftsRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/drafts/DraftsRobot.kt index 1e69bdbd5..ab463de9e 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/drafts/DraftsRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/drafts/DraftsRobot.kt @@ -22,7 +22,7 @@ import ch.protonmail.android.R import ch.protonmail.android.uitests.robots.mailbox.MailboxRobotInterface import ch.protonmail.android.uitests.robots.mailbox.composer.ComposerRobot import ch.protonmail.android.uitests.robots.menu.MenuRobot -import ch.protonmail.android.uitests.testsHelper.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions /** * [DraftsRobot] implements [MailboxRobotInterface], diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/inbox/InboxRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/inbox/InboxRobot.kt index acdadc35c..a18ea19e8 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/inbox/InboxRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/inbox/InboxRobot.kt @@ -22,7 +22,7 @@ import ch.protonmail.android.R import ch.protonmail.android.uitests.robots.mailbox.MailboxRobotInterface import ch.protonmail.android.uitests.robots.mailbox.MoveToFolderRobotInterface import ch.protonmail.android.uitests.robots.mailbox.SelectionStateRobotInterface -import ch.protonmail.android.uitests.testsHelper.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions /** * [InboxRobot] class implements [MailboxRobotInterface], diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/labelfolder/LabelFolderRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/labelfolder/LabelFolderRobot.kt index 6a1402a11..710866ef8 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/labelfolder/LabelFolderRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/labelfolder/LabelFolderRobot.kt @@ -20,7 +20,7 @@ package ch.protonmail.android.uitests.robots.mailbox.labelfolder import ch.protonmail.android.R import ch.protonmail.android.uitests.robots.mailbox.MailboxRobotInterface -import ch.protonmail.android.uitests.testsHelper.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions /** * [LabelFolderRobot] class implements [MailboxRobotInterface], @@ -28,12 +28,17 @@ import ch.protonmail.android.uitests.testsHelper.UIActions */ class LabelFolderRobot : MailboxRobotInterface { + override fun refreshMessageList(): LabelFolderRobot { + super.refreshMessageList() + return this + } + /** * Contains all the validations that can be performed by [LabelFolderRobot]. */ open class Verify { - fun messageMoved(messageSubject: String) { + fun messageExists(messageSubject: String) { UIActions.wait.forViewWithIdAndText(R.id.messageTitleTextView, messageSubject) } } diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/messagedetail/MessageRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/messagedetail/MessageRobot.kt index 78020c6ce..b250be7f2 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/messagedetail/MessageRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/messagedetail/MessageRobot.kt @@ -24,15 +24,20 @@ import androidx.test.espresso.intent.matcher.IntentMatchers.hasData import androidx.test.espresso.intent.matcher.IntentMatchers.hasType import androidx.test.espresso.intent.matcher.UriMatchers.hasPath import ch.protonmail.android.R +import ch.protonmail.android.uitests.robots.mailbox.ApplyLabelRobotInterface +import ch.protonmail.android.uitests.robots.mailbox.MailboxMatchers.withFolderName import ch.protonmail.android.uitests.robots.mailbox.composer.ComposerRobot import ch.protonmail.android.uitests.robots.mailbox.drafts.DraftsRobot import ch.protonmail.android.uitests.robots.mailbox.inbox.InboxRobot import ch.protonmail.android.uitests.robots.mailbox.search.SearchRobot import ch.protonmail.android.uitests.robots.mailbox.sent.SentRobot import ch.protonmail.android.uitests.robots.mailbox.spam.SpamRobot -import ch.protonmail.android.uitests.testsHelper.TestData -import ch.protonmail.android.uitests.testsHelper.UIActions -import ch.protonmail.android.uitests.testsHelper.click +import ch.protonmail.android.uitests.testsHelper.StringUtils.stringFromResource +import ch.protonmail.android.uitests.testsHelper.TestData.pgpEncryptedTextDecrypted +import ch.protonmail.android.uitests.testsHelper.TestData.pgpSignedTextDecrypted +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.click +import ch.protonmail.android.uitests.testsHelper.uiactions.type import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.containsString @@ -69,9 +74,16 @@ class MessageRobot { return InboxRobot() } - fun openFoldersModal(): MessageRobot { + fun openFoldersModal(): FoldersDialogRobot { + UIActions.wait.forViewWithId(R.id.messageWebViewContainer) UIActions.wait.forViewWithId(R.id.add_folder).click() - return this + return FoldersDialogRobot() + } + + fun openLabelsModal(): LabelsDialogRobot { + UIActions.wait.forViewWithId(R.id.messageWebViewContainer) + UIActions.wait.forViewWithId(R.id.add_label).click() + return LabelsDialogRobot() } fun reply(): ComposerRobot { @@ -109,6 +121,12 @@ class MessageRobot { return SentRobot() } + fun navigateUpToInbox(): InboxRobot { + UIActions.wait.forViewWithId(R.id.reply_all) + UIActions.system.clickHamburgerOrUpButton() + return InboxRobot() + } + fun clickSendButtonFromDrafts(): DraftsRobot { UIActions.id.clickViewWithId(sendMessageId) UIActions.wait.forViewWithText(R.string.message_sent) @@ -117,12 +135,101 @@ class MessageRobot { fun clickLoadEmbeddedImagesButton(): MessageRobot { UIActions.wait.forViewWithId(R.id.messageWebViewContainer) - UIActions.wait.forViewWithIdAndAncestorId(R.id.loadContentButton, R.id.containerLoadEmbeddedImagesContainer) + UIActions.wait + .forViewWithIdAndAncestorId( + R.id.loadContentButton, + R.id.containerLoadEmbeddedImagesContainer + ) .click() return this } - inner class MessageMoreOptions { + class LabelsDialogRobot : ApplyLabelRobotInterface { + + override fun addLabel(name: String): LabelsDialogRobot { + super.addLabel(name) + return this + } + + override fun selectLabelByName(name: String): LabelsDialogRobot { + super.selectLabelByName(name) + return this + } + + override fun apply(): MessageRobot { + super.apply() + return MessageRobot() + } + } + + class FoldersDialogRobot { + + fun clickCreateFolder(): AddFolderRobot { + UIActions.wait.forViewWithId(R.id.folders_list_view) + UIActions.listView + .clickListItemByText( + withFolderName(stringFromResource(R.string.create_new_folder)), + R.id.folders_list_view + ) + return AddFolderRobot() + } + + fun moveMessageFromSpamToFolder(folderName: String): SpamRobot { + selectFolder(folderName) + return SpamRobot() + } + + fun moveMessageFromSentToFolder(folderName: String): SentRobot { + selectFolder(folderName) + return SentRobot() + } + + fun moveMessageFromInboxToFolder(folderName: String): InboxRobot { + selectFolder(folderName) + return InboxRobot() + } + + fun moveMessageFromMessageToFolder(folderName: String): MessageRobot { + selectFolder(folderName) + return MessageRobot() + } + + private fun selectFolder(folderName: String) { + UIActions.wait.forViewWithId(R.id.folders_list_view) + UIActions.listView + .clickListItemByText( + withFolderName(folderName), + R.id.folders_list_view + ) + } + + class Verify { + + fun folderExistsInFoldersList(folderName: String) { + UIActions.wait.forViewWithId(R.id.folders_list_view) + UIActions.listView.checkItemWithTextExists(R.id.folders_list_view, folderName) + } + } + + inline fun verify(block: Verify.() -> Unit) = Verify().apply(block) + } + + class AddFolderRobot { + + fun addFolderWithName(name: String): FoldersDialogRobot = typeName(name).saveNewFolder() + + private fun saveNewFolder(): FoldersDialogRobot { + UIActions.wait.forViewWithId(R.id.save_new_label).click() + return FoldersDialogRobot() + } + + private fun typeName(folderName: String): AddFolderRobot { + UIActions.wait.forViewWithId(R.id.label_name).type(folderName) + return this + } + } + + class MessageMoreOptions { fun viewHeaders(): ViewHeadersRobot { UIActions.allOf.clickViewWithIdAndText(R.id.title, R.string.view_headers) @@ -152,12 +259,12 @@ class MessageRobot { } fun pgpEncryptedMessageDecrypted() { - UIActions.wait.forViewWithTextByUiAutomator(TestData.pgpEncryptedTextDecrypted) + UIActions.wait.forViewWithTextByUiAutomator(pgpEncryptedTextDecrypted) } fun pgpSignedMessageDecrypted() { - UIActions.wait.forViewWithTextByUiAutomator(TestData.pgpSignedTextDecrypted) + UIActions.wait.forViewWithTextByUiAutomator(pgpSignedTextDecrypted) } fun messageWebViewContainerShown() { @@ -170,11 +277,13 @@ class MessageRobot { } fun intentWithActionFileNameAndMimeTypeSent(fileName: String, mimeType: String) { - UIActions.wait.forIntent(allOf( - hasAction(Intent.ACTION_VIEW), - hasData(hasPath(containsString(fileName.split('.')[0]))), - hasData(hasPath(containsString(fileName.split('.')[1]))), - hasType(mimeType)) + UIActions.wait.forIntent( + allOf( + hasAction(Intent.ACTION_VIEW), + hasData(hasPath(containsString(fileName.split('.')[0]))), + hasData(hasPath(containsString(fileName.split('.')[1]))), + hasType(mimeType) + ) ) } } diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/messagedetail/ViewHeadersRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/messagedetail/ViewHeadersRobot.kt index 9fd638fc8..3017722a5 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/messagedetail/ViewHeadersRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/messagedetail/ViewHeadersRobot.kt @@ -20,7 +20,7 @@ package ch.protonmail.android.uitests.robots.mailbox.messagedetail import ch.protonmail.android.R import ch.protonmail.android.uitests.robots.mailbox.sent.SentRobot -import ch.protonmail.android.uitests.testsHelper.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions /** * [ViewHeadersRobot] class contains actions and verifications for View Headers functionality. diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/search/SearchRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/search/SearchRobot.kt index cb30f80ad..cb8453fdf 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/search/SearchRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/search/SearchRobot.kt @@ -26,7 +26,7 @@ import ch.protonmail.android.uitests.robots.mailbox.composer.ComposerRobot import ch.protonmail.android.uitests.robots.mailbox.inbox.InboxRobot import ch.protonmail.android.uitests.robots.mailbox.messagedetail.MessageRobot import ch.protonmail.android.uitests.testsHelper.TestData -import ch.protonmail.android.uitests.testsHelper.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions /** * [SearchRobot] class contains actions and verifications for Search functionality. @@ -40,15 +40,14 @@ class SearchRobot { fun clickSearchedMessageBySubject(subject: String): MessageRobot { UIActions.recyclerView - .waitForBeingPopulated(messagesRecyclerViewId) - .clickOnRecyclerViewMatchedItem(messagesRecyclerViewId, withMessageSubject(subject)) + .common.waitForBeingPopulated(messagesRecyclerViewId) + .common.clickOnRecyclerViewMatchedItem(messagesRecyclerViewId, withMessageSubject(subject)) return MessageRobot() } fun clickSearchedDraftBySubject(subject: String): ComposerRobot { - UIActions.recyclerView.waitForBeingPopulated(messagesRecyclerViewId) - UIActions.recyclerView - .clickOnRecyclerViewMatchedItem(messagesRecyclerViewId, withMessageSubject(subject)) + UIActions.recyclerView.common.waitForBeingPopulated(messagesRecyclerViewId) + UIActions.recyclerView.common.clickOnRecyclerViewMatchedItem(messagesRecyclerViewId, withMessageSubject(subject)) return ComposerRobot() } @@ -59,8 +58,8 @@ class SearchRobot { fun clickSearchedMessageBySubjectPart(subject: String): MessageRobot { UIActions.recyclerView - .waitForBeingPopulated(messagesRecyclerViewId) - .clickOnRecyclerViewMatchedItem(messagesRecyclerViewId, withMessageSubjectContaining(subject)) + .common.waitForBeingPopulated(messagesRecyclerViewId) + .common.clickOnRecyclerViewMatchedItem(messagesRecyclerViewId, withMessageSubjectContaining(subject)) return MessageRobot() } @@ -71,8 +70,8 @@ class SearchRobot { fun searchedMessageFound() { UIActions.recyclerView - .waitForBeingPopulated(messagesRecyclerViewId) - .scrollToRecyclerViewMatchedItem( + .common.waitForBeingPopulated(messagesRecyclerViewId) + .common.scrollToRecyclerViewMatchedItem( R.id.messages_list_view, withFirstInstanceMessageSubject(TestData.searchMessageSubject) ) diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/sent/SentRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/sent/SentRobot.kt index 4f03b26ff..aa6467fc1 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/sent/SentRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/sent/SentRobot.kt @@ -19,11 +19,12 @@ package ch.protonmail.android.uitests.robots.mailbox.sent import ch.protonmail.android.R +import ch.protonmail.android.uitests.robots.mailbox.ApplyLabelRobotInterface import ch.protonmail.android.uitests.robots.mailbox.MailboxRobotInterface import ch.protonmail.android.uitests.robots.mailbox.MoveToFolderRobotInterface import ch.protonmail.android.uitests.robots.mailbox.SelectionStateRobotInterface import ch.protonmail.android.uitests.robots.mailbox.inbox.InboxRobot -import ch.protonmail.android.uitests.testsHelper.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions /** * [SentRobot] class implements [MailboxRobotInterface], @@ -66,9 +67,9 @@ class SentRobot : MailboxRobotInterface { return this } - override fun addLabel(): SentRobot { + override fun addLabel(): ApplyLabelRobot { super.addLabel() - return SentRobot() + return ApplyLabelRobot() } override fun addFolder(): MoveToFolderRobot { @@ -93,6 +94,22 @@ class SentRobot : MailboxRobotInterface { } } + /** + * Handles Move to folder dialog actions. + */ + class ApplyLabelRobot : ApplyLabelRobotInterface { + + override fun selectLabelByName(name: String): ApplyLabelRobot { + super.selectLabelByName(name) + return ApplyLabelRobot() + } + + override fun apply(): SentRobot { + super.apply() + return SentRobot() + } + } + /** * Contains all the validations that can be performed by [SentRobot]. */ diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/spam/SpamRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/spam/SpamRobot.kt index 8697d4b94..1716d5be6 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/spam/SpamRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/spam/SpamRobot.kt @@ -19,7 +19,7 @@ package ch.protonmail.android.uitests.robots.mailbox.spam import ch.protonmail.android.uitests.robots.mailbox.MailboxRobotInterface -import ch.protonmail.android.uitests.testsHelper.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions /** * [SpamRobot] class implements [MailboxRobotInterface], diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/trash/TrashRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/trash/TrashRobot.kt index a54452f33..8bb8b5e25 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/trash/TrashRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/mailbox/trash/TrashRobot.kt @@ -20,7 +20,7 @@ package ch.protonmail.android.uitests.robots.mailbox.trash import ch.protonmail.android.R import ch.protonmail.android.uitests.robots.mailbox.MailboxRobotInterface -import ch.protonmail.android.uitests.testsHelper.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions /** * [TrashRobot] class implements [MailboxRobotInterface], diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/manageaccounts/AccountManagerRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/manageaccounts/AccountManagerRobot.kt index 11f2d59db..1f275f439 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/manageaccounts/AccountManagerRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/manageaccounts/AccountManagerRobot.kt @@ -5,8 +5,8 @@ import ch.protonmail.android.uitests.robots.login.LoginRobot import ch.protonmail.android.uitests.robots.mailbox.inbox.InboxRobot import ch.protonmail.android.uitests.robots.manageaccounts.ManageAccountsMatchers.withPrimaryAccountInAccountManager import ch.protonmail.android.uitests.testsHelper.StringUtils.stringFromResource -import ch.protonmail.android.uitests.testsHelper.UIActions -import ch.protonmail.android.uitests.testsHelper.click +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.click /** * [AccountManagerRobot] class contains actions and verifications for Account Manager functionality. @@ -42,7 +42,7 @@ open class AccountManagerRobot { } private fun accountMoreMenu(email: String): AccountManagerRobot { - UIActions.recyclerView.clickAccountManagerViewItem( + UIActions.recyclerView.manageAccounts.clickAccountManagerViewItem( accountsRecyclerViewId, email, R.id.accUserMoreMenu @@ -91,17 +91,15 @@ open class AccountManagerRobot { } fun switchedToAccount(username: String) { - UIActions - .recyclerView - .scrollToRecyclerViewMatchedItem( - accountsRecyclerViewId, - withPrimaryAccountInAccountManager( - stringFromResource( - R.string.manage_accounts_user_primary, - username - ) + UIActions.recyclerView.common.scrollToRecyclerViewMatchedItem( + accountsRecyclerViewId, + withPrimaryAccountInAccountManager( + stringFromResource( + R.string.manage_accounts_user_primary, + username ) ) + ) } } diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/manageaccounts/ConnectAccountRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/manageaccounts/ConnectAccountRobot.kt index 5cd186a9d..e56b5c4f8 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/manageaccounts/ConnectAccountRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/manageaccounts/ConnectAccountRobot.kt @@ -20,9 +20,9 @@ package ch.protonmail.android.uitests.robots.manageaccounts import ch.protonmail.android.R import ch.protonmail.android.uitests.robots.mailbox.inbox.InboxRobot -import ch.protonmail.android.uitests.testsHelper.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions import ch.protonmail.android.uitests.testsHelper.User -import ch.protonmail.android.uitests.testsHelper.type +import ch.protonmail.android.uitests.testsHelper.uiactions.type /** * [ConnectAccountRobot] class contains actions and verifications for Connect Account functionality. diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/menu/MenuRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/menu/MenuRobot.kt index 4b65c948a..60614bac0 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/menu/MenuRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/menu/MenuRobot.kt @@ -35,8 +35,8 @@ import ch.protonmail.android.uitests.robots.reportbugs.ReportBugsRobot import ch.protonmail.android.uitests.robots.settings.SettingsRobot import ch.protonmail.android.uitests.robots.upgradedonate.UpgradeDonateRobot import ch.protonmail.android.uitests.testsHelper.StringUtils.stringFromResource -import ch.protonmail.android.uitests.testsHelper.UIActions -import ch.protonmail.android.uitests.testsHelper.click +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.click /** * [MenuRobot] class contains actions and verifications for menu functionality. @@ -111,10 +111,10 @@ class MenuRobot { } private fun selectMenuItem(@IdRes menuItemName: String) = UIActions.recyclerView - .clickOnRecyclerViewMatchedItem(leftDrawerNavigationId, withMenuItemTag(menuItemName)) + .common.clickOnRecyclerViewMatchedItem(leftDrawerNavigationId, withMenuItemTag(menuItemName)) private fun selectMenuLabelOrFolder(@IdRes labelOrFolderName: String) = UIActions.recyclerView - .clickOnRecyclerViewMatchedItem(leftDrawerNavigationId, withLabelOrFolderName(labelOrFolderName)) + .common.clickOnRecyclerViewMatchedItem(leftDrawerNavigationId, withLabelOrFolderName(labelOrFolderName)) /** * Contains all the validations that can be performed by [MenuRobot]. @@ -138,12 +138,12 @@ class MenuRobot { } fun switchToAccount(accountPosition: Int): MenuRobot { - UIActions.recyclerView.clickOnRecyclerViewItemByPosition(menuDrawerUserList, accountPosition) + UIActions.recyclerView.common.clickOnRecyclerViewItemByPosition(menuDrawerUserList, accountPosition) return MenuRobot() } fun switchToAccount(email: String): MenuRobot { - UIActions.recyclerView.clickOnRecyclerViewMatchedItem(menuDrawerUserList, withAccountEmailInDrawer(email)) + UIActions.recyclerView.common.clickOnRecyclerViewMatchedItem(menuDrawerUserList, withAccountEmailInDrawer(email)) return MenuRobot() } @@ -154,10 +154,10 @@ class MenuRobot { fun accountsListOpened() = UIActions.check.viewWithIdIsDisplayed(menuDrawerUserList) fun accountAdded(email: String) = UIActions.recyclerView - .scrollToRecyclerViewMatchedItem(menuDrawerUserList, withAccountEmailInDrawer(email)) + .common.scrollToRecyclerViewMatchedItem(menuDrawerUserList, withAccountEmailInDrawer(email)) fun accountLoggedOut(email: String) = UIActions.recyclerView - .scrollToRecyclerViewMatchedItem(menuDrawerUserList, withLoggedOutAccountNameInDrawer(email)) + .common.scrollToRecyclerViewMatchedItem(menuDrawerUserList, withLoggedOutAccountNameInDrawer(email)) fun accountRemoved(username: String) = UIActions.check .viewWithIdAndTextDoesNotExist(menuDrawerUsernameId, username) diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/reportbugs/ReportBugsRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/reportbugs/ReportBugsRobot.kt index 922620c4c..f5ef90786 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/reportbugs/ReportBugsRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/reportbugs/ReportBugsRobot.kt @@ -19,7 +19,7 @@ package ch.protonmail.android.uitests.robots.reportbugs import ch.protonmail.android.R -import ch.protonmail.android.uitests.testsHelper.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions /** * [ReportBugsRobot] class contains actions and verifications for Bug report functionality. diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/SettingsRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/SettingsRobot.kt index 4ee029126..1a4fd9add 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/SettingsRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/SettingsRobot.kt @@ -19,13 +19,15 @@ package ch.protonmail.android.uitests.robots.settings import ch.protonmail.android.R -import ch.protonmail.android.uitests.actions.settings.account.AccountSettingsRobot import ch.protonmail.android.uitests.robots.mailbox.inbox.InboxRobot +import ch.protonmail.android.uitests.robots.settings.account.AccountSettingsRobot +import ch.protonmail.android.uitests.robots.menu.MenuRobot import ch.protonmail.android.uitests.robots.settings.SettingsMatchers.withSettingsHeader import ch.protonmail.android.uitests.robots.settings.SettingsMatchers.withSettingsValue import ch.protonmail.android.uitests.robots.settings.autolock.AutoLockRobot import ch.protonmail.android.uitests.testsHelper.StringUtils -import ch.protonmail.android.uitests.testsHelper.UIActions +import ch.protonmail.android.uitests.testsHelper.User +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions /** * [SettingsRobot] class contains actions and verifications for Settings view. @@ -42,6 +44,16 @@ class SettingsRobot { return this } + fun menuDrawer(): MenuRobot { + UIActions.system.clickHamburgerOrUpButton() + return MenuRobot() + } + + fun openUserAccountSettings(user: User): AccountSettingsRobot { + selectSettingsItemByValue(user.email) + return AccountSettingsRobot() + } + fun selectAutoLock(): AutoLockRobot { selectItemByHeader(autoLockText) return AutoLockRobot() @@ -49,13 +61,15 @@ class SettingsRobot { fun selectSettingsItemByValue(value: String): AccountSettingsRobot { UIActions.wait.forViewWithId(R.id.settingsRecyclerView) - UIActions.recyclerView.clickOnRecyclerViewMatchedItem(R.id.settingsRecyclerView, withSettingsValue(value)) + UIActions.recyclerView + .common.clickOnRecyclerViewMatchedItem(R.id.settingsRecyclerView, withSettingsValue(value)) return AccountSettingsRobot() } fun selectItemByHeader(header: String) { UIActions.wait.forViewWithId(R.id.settingsRecyclerView) - UIActions.recyclerView.clickOnRecyclerViewMatchedItem(R.id.settingsRecyclerView, withSettingsHeader(header)) + UIActions.recyclerView + .common.clickOnRecyclerViewMatchedItem(R.id.settingsRecyclerView, withSettingsHeader(header)) } /** diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/AccountSettingsRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/AccountSettingsRobot.kt index d56c6291f..1e1e49bda 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/AccountSettingsRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/AccountSettingsRobot.kt @@ -16,7 +16,7 @@ * You should have received a copy of the GNU General Public License * along with ProtonMail. If not, see https://www.gnu.org/licenses/. */ -package ch.protonmail.android.uitests.actions.settings.account +package ch.protonmail.android.uitests.robots.settings.account import androidx.annotation.IdRes import androidx.test.espresso.Espresso.onView @@ -26,15 +26,8 @@ import androidx.test.espresso.matcher.ViewMatchers.withId import ch.protonmail.android.R import ch.protonmail.android.uitests.robots.settings.SettingsMatchers.withSettingsHeader import ch.protonmail.android.uitests.robots.settings.SettingsRobot -import ch.protonmail.android.uitests.robots.settings.account.DefaultEmailAddressRobot -import ch.protonmail.android.uitests.robots.settings.account.DisplayNameAndSignatureRobot -import ch.protonmail.android.uitests.robots.settings.account.LabelsAndFoldersRobot -import ch.protonmail.android.uitests.robots.settings.account.PasswordManagementRobot -import ch.protonmail.android.uitests.robots.settings.account.RecoveryEmailRobot -import ch.protonmail.android.uitests.robots.settings.account.SubscriptionRobot -import ch.protonmail.android.uitests.robots.settings.account.SwipingGesturesSettingsRobot import ch.protonmail.android.uitests.testsHelper.StringUtils.stringFromResource -import ch.protonmail.android.uitests.testsHelper.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions /** * [AccountSettingsRobot] class contains actions and verifications for diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/ChooseSwipeActionRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/ChooseSwipeActionRobot.kt index 5e9cfa663..6e9035ec3 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/ChooseSwipeActionRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/ChooseSwipeActionRobot.kt @@ -20,8 +20,7 @@ package ch.protonmail.android.uitests.robots.settings.account import ch.protonmail.android.R -import ch.protonmail.android.uitests.actions.settings.account.AccountSettingsRobot -import ch.protonmail.android.uitests.testsHelper.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions class ChooseSwipeActionRobot { diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/DefaultEmailAddressRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/DefaultEmailAddressRobot.kt index 019cc6267..71aa2d19b 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/DefaultEmailAddressRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/DefaultEmailAddressRobot.kt @@ -19,7 +19,7 @@ package ch.protonmail.android.uitests.robots.settings.account import ch.protonmail.android.R -import ch.protonmail.android.uitests.testsHelper.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions /** * Class represents Default Email Address view. diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/DisplayNameAndSignatureRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/DisplayNameAndSignatureRobot.kt index 6b9497783..46c13eb78 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/DisplayNameAndSignatureRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/DisplayNameAndSignatureRobot.kt @@ -23,7 +23,7 @@ import androidx.recyclerview.widget.RecyclerView import ch.protonmail.android.R import ch.protonmail.android.uitests.testsHelper.ActivityProvider.currentActivity import ch.protonmail.android.uitests.testsHelper.StringUtils.stringFromResource -import ch.protonmail.android.uitests.testsHelper.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions import ch.protonmail.android.views.SettingsDefaultItemView /** diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/FoldersManagerRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/FoldersManagerRobot.kt index 072902296..652f82a68 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/FoldersManagerRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/FoldersManagerRobot.kt @@ -23,7 +23,10 @@ import androidx.test.espresso.contrib.RecyclerViewActions.scrollToHolder import androidx.test.espresso.matcher.ViewMatchers.withId import ch.protonmail.android.R import ch.protonmail.android.uitests.robots.settings.SettingsMatchers.withLabelName -import ch.protonmail.android.uitests.testsHelper.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.click +import ch.protonmail.android.uitests.testsHelper.uiactions.insert +import ch.protonmail.android.uitests.testsHelper.uiactions.type /** * [FoldersManagerRobot] class contains actions and verifications for Folders functionality. @@ -32,20 +35,76 @@ class FoldersManagerRobot { fun addFolder(name: String): FoldersManagerRobot { folderName(name) - .saveNewFolder() + .saveFolder() return this } - private fun folderName(name: String): FoldersManagerRobot { - UIActions.id.typeTextIntoFieldWithId(R.id.label_name, name) + fun deleteFolder(name: String): FoldersManagerRobot { + selectFolderCheckbox(name) + .clickDeleteSelectedButton() + .confirmDeletion() + return this + } + + fun editFolder(name: String, newName: String, colorPosition: Int): FoldersManagerRobot { + selectFolder(name) + .updateFolderName(newName) + .saveFolder() return this } - private fun saveNewFolder(): FoldersManagerRobot { + fun navigateUpToLabelsAndFolders(): LabelsAndFoldersRobot { + UIActions.system.clickHamburgerOrUpButton() + return LabelsAndFoldersRobot() + } + + private fun clickDeleteSelectedButton(): DeleteSelectedFoldersDialogRobot { + UIActions.id.clickViewWithId(R.id.delete_labels) + return DeleteSelectedFoldersDialogRobot() + } + + private fun clickFolder(name: String): FoldersManagerRobot { UIActions.id.clickViewWithId(R.id.save_new_label) return this } + private fun folderName(name: String): FoldersManagerRobot { + UIActions.wait.forViewWithIdAndParentId(R.id.label_name, R.id.add_label_container).type(name) + return this + } + + private fun updateFolderName(name: String): FoldersManagerRobot { + UIActions.wait.forViewWithIdAndParentId(R.id.label_name, R.id.add_label_container).insert(name) + return this + } + + private fun saveFolder(): FoldersManagerRobot { + UIActions.wait.forViewWithId(R.id.save_new_label).click() + return this + } + + private fun selectFolder(name: String): FoldersManagerRobot { + UIActions.wait.forViewWithId(R.id.labels_recycler_view) + UIActions.recyclerView.common.clickOnRecyclerViewMatchedItem(R.id.labels_recycler_view, withLabelName(name)) + return this + } + + private fun selectFolderCheckbox(name: String): FoldersManagerRobot { + UIActions.wait.forViewWithId(R.id.labels_recycler_view) + UIActions.recyclerView.common + .clickOnRecyclerViewItemChild(R.id.labels_recycler_view, withLabelName(name), R.id.label_check) + return this + } + + + class DeleteSelectedFoldersDialogRobot { + + fun confirmDeletion(): FoldersManagerRobot { + UIActions.system.clickPositiveDialogButton() + return FoldersManagerRobot() + } + } + /** * Contains all the validations that can be performed by [FoldersManagerRobot]. */ @@ -54,6 +113,10 @@ class FoldersManagerRobot { fun folderWithNameShown(name: String) { onView(withId(R.id.labels_recycler_view)).perform(scrollToHolder(withLabelName(name))) } + + fun folderWithNameDoesNotExist(name: String) { + UIActions.wait.untilViewWithTextIsGone(name) + } } inline fun verify(block: Verify.() -> Unit) = Verify().apply(block) diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/LabelsAndFoldersRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/LabelsAndFoldersRobot.kt index 174632528..4196445f0 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/LabelsAndFoldersRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/LabelsAndFoldersRobot.kt @@ -19,7 +19,7 @@ package ch.protonmail.android.uitests.robots.settings.account import ch.protonmail.android.R -import ch.protonmail.android.uitests.testsHelper.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions /** * [LabelsAndFoldersRobot] class contains actions and verifications for diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/LabelsManagerRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/LabelsManagerRobot.kt index 80af66c38..0680bec4c 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/LabelsManagerRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/LabelsManagerRobot.kt @@ -23,7 +23,8 @@ import androidx.test.espresso.contrib.RecyclerViewActions.scrollToHolder import androidx.test.espresso.matcher.ViewMatchers.withId import ch.protonmail.android.R import ch.protonmail.android.uitests.robots.settings.SettingsMatchers.withLabelName -import ch.protonmail.android.uitests.testsHelper.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.insert /** * [LabelsManagerRobot] class contains actions and verifications for Labels functionality. @@ -35,6 +36,45 @@ class LabelsManagerRobot { .saveNewLabel() } + fun editLabel(name: String, newName: String, colorPosition: Int): LabelsManagerRobot { + selectLabel(name) + .updateLabelName(newName) + .saveNewLabel() + return this + } + + fun deleteLabel(name: String): LabelsManagerRobot { + selectFolderCheckbox(name) + .clickDeleteSelectedButton() + .confirmDeletion() + return this + } + + private fun clickDeleteSelectedButton(): FoldersManagerRobot.DeleteSelectedFoldersDialogRobot { + UIActions.id.clickViewWithId(R.id.delete_labels) + return FoldersManagerRobot.DeleteSelectedFoldersDialogRobot() + } + + private fun selectFolderCheckbox(name: String): LabelsManagerRobot { + UIActions.wait.forViewWithId(R.id.labels_recycler_view) + UIActions.recyclerView.common + .clickOnRecyclerViewItemChild(R.id.labels_recycler_view, withLabelName(name), R.id.label_check) + return this + } + + + private fun selectLabel(name: String): LabelsManagerRobot { + UIActions.wait.forViewWithId(R.id.labels_recycler_view) + UIActions.recyclerView.common.clickOnRecyclerViewMatchedItem(R.id.labels_recycler_view, withLabelName(name)) + return this + } + + private fun updateLabelName(name: String): LabelsManagerRobot { + UIActions.wait.forViewWithIdAndParentId(R.id.label_name, R.id.add_label_container) + .insert(name) + return this + } + private fun labelName(name: String): LabelsManagerRobot { UIActions.id.typeTextIntoFieldWithId(R.id.label_name, name) return this @@ -53,6 +93,11 @@ class LabelsManagerRobot { fun labelWithNameShown(name: String) { onView(withId(R.id.labels_recycler_view)).perform(scrollToHolder(withLabelName(name))) } + + fun labelWithNameDoesNotExist(name: String) { + UIActions.wait.untilViewWithTextIsGone(name) + } + } inline fun verify(block: Verify.() -> Unit) = Verify().apply(block) diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/PasswordManagementRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/PasswordManagementRobot.kt index ca7d4381c..31d942c6f 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/PasswordManagementRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/PasswordManagementRobot.kt @@ -19,8 +19,7 @@ package ch.protonmail.android.uitests.robots.settings.account import ch.protonmail.android.R -import ch.protonmail.android.uitests.actions.settings.account.AccountSettingsRobot -import ch.protonmail.android.uitests.testsHelper.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions import ch.protonmail.android.uitests.testsHelper.User /** diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/RecoveryEmailRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/RecoveryEmailRobot.kt index 57acff831..e3ccc3994 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/RecoveryEmailRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/RecoveryEmailRobot.kt @@ -19,9 +19,9 @@ package ch.protonmail.android.uitests.robots.settings.account import ch.protonmail.android.R -import ch.protonmail.android.uitests.testsHelper.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions import ch.protonmail.android.uitests.testsHelper.User -import ch.protonmail.android.uitests.testsHelper.insert +import ch.protonmail.android.uitests.testsHelper.uiactions.insert /** * Class represents Email recovery view. diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/SubscriptionRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/SubscriptionRobot.kt index 0b6245848..903a0970e 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/SubscriptionRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/SubscriptionRobot.kt @@ -18,7 +18,7 @@ */ package ch.protonmail.android.uitests.robots.settings.account -import ch.protonmail.android.uitests.testsHelper.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions /** * Class represents Subscription view. diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/SwipingGesturesSettingsRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/SwipingGesturesSettingsRobot.kt index 37f1162cd..005eb445c 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/SwipingGesturesSettingsRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/account/SwipingGesturesSettingsRobot.kt @@ -20,22 +20,21 @@ package ch.protonmail.android.uitests.robots.settings.account import ch.protonmail.android.R -import ch.protonmail.android.uitests.actions.settings.account.AccountSettingsRobot import ch.protonmail.android.uitests.robots.settings.SettingsMatchers.withSettingsHeader -import ch.protonmail.android.uitests.testsHelper.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions class SwipingGesturesSettingsRobot { fun selectSwipeRight(): ChooseSwipeActionRobot { UIActions.wait.forViewWithId(R.id.settingsRecyclerView) - UIActions.recyclerView + UIActions.recyclerView.common .clickOnRecyclerViewMatchedItem(R.id.settingsRecyclerView, withSettingsHeader(R.string.swipe_action_right)) return ChooseSwipeActionRobot() } fun selectSwipeLeft(): ChooseSwipeActionRobot { UIActions.wait.forViewWithId(R.id.settingsRecyclerView) - UIActions.recyclerView + UIActions.recyclerView.common .clickOnRecyclerViewMatchedItem(R.id.settingsRecyclerView, withSettingsHeader(R.string.swipe_action_left)) return ChooseSwipeActionRobot() } diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/autolock/AutoLockRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/autolock/AutoLockRobot.kt index b84970e99..dc7b0aaa1 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/autolock/AutoLockRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/autolock/AutoLockRobot.kt @@ -23,8 +23,8 @@ import androidx.appcompat.widget.AppCompatImageButton import ch.protonmail.android.R import ch.protonmail.android.uitests.robots.settings.SettingsRobot import ch.protonmail.android.uitests.tests.BaseTest.Companion.targetContext -import ch.protonmail.android.uitests.testsHelper.UIActions -import ch.protonmail.android.uitests.testsHelper.click +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.click class AutoLockRobot { diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/autolock/PinRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/autolock/PinRobot.kt index 15211fe78..152a93b8b 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/autolock/PinRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/settings/autolock/PinRobot.kt @@ -21,8 +21,8 @@ package ch.protonmail.android.uitests.robots.settings.autolock import ch.protonmail.android.R import ch.protonmail.android.uitests.robots.mailbox.composer.ComposerRobot -import ch.protonmail.android.uitests.testsHelper.UIActions -import ch.protonmail.android.uitests.testsHelper.click +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.click import junit.framework.Assert.fail class PinRobot { diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/upgradedonate/UpgradeDonateRobot.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/upgradedonate/UpgradeDonateRobot.kt index 47b76f5c6..40a1bd007 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/upgradedonate/UpgradeDonateRobot.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/upgradedonate/UpgradeDonateRobot.kt @@ -19,7 +19,7 @@ package ch.protonmail.android.uitests.robots.upgradedonate import ch.protonmail.android.R -import ch.protonmail.android.uitests.testsHelper.UIActions +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions /** * [UpgradeDonateRobot] class contains actions and verifications for Upgrade / Donate view. diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/tests/inbox/InboxTests.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/tests/inbox/InboxTests.kt index f97809371..914cd01aa 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/tests/inbox/InboxTests.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/tests/inbox/InboxTests.kt @@ -178,7 +178,7 @@ class InboxTests : BaseTest() { .moveToExistingFolder(folder) .menuDrawer() .labelOrFolder(folder) - .verify { messageMoved(longClickedMessageSubject) } + .verify { messageExists(longClickedMessageSubject) } } @Test diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/tests/labelsfolders/LabelsFoldersTests.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/tests/labelsfolders/LabelsFoldersTests.kt new file mode 100644 index 000000000..0e824faaa --- /dev/null +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/tests/labelsfolders/LabelsFoldersTests.kt @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * + * This file is part of ProtonMail. + * + * ProtonMail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonMail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonMail. If not, see https://www.gnu.org/licenses/. + */ + +package ch.protonmail.android.uitests.tests.labelsfolders + +import ch.protonmail.android.uitests.robots.login.LoginRobot +import ch.protonmail.android.uitests.robots.mailbox.MailboxRobotInterface.Companion.longClickedMessageSubject +import ch.protonmail.android.uitests.robots.mailbox.MailboxRobotInterface.Companion.selectedMessageSubject +import ch.protonmail.android.uitests.tests.BaseTest +import ch.protonmail.android.uitests.testsHelper.StringUtils +import ch.protonmail.android.uitests.testsHelper.TestData.onePassUser +import ch.protonmail.android.uitests.testsHelper.annotations.SmokeTest +import org.junit.Test +import org.junit.experimental.categories.Category + +class LabelsFoldersTests : BaseTest() { + + private val loginRobot = LoginRobot() + + @Test + fun createRenameAndDeleteFolderFromInbox() { + val folderName = StringUtils.getEmailString() + val newFolderName = StringUtils.getEmailString() + loginRobot + .loginUser(onePassUser) + .clickMessageByPosition(1) + .openFoldersModal() + .clickCreateFolder() + .addFolderWithName(folderName) + .moveMessageFromInboxToFolder(folderName) + .menuDrawer() + .settings() + .openUserAccountSettings(onePassUser) + .foldersAndLabels() + .foldersManager() + .editFolder(folderName, newFolderName, 2) + .navigateUpToLabelsAndFolders() + .foldersManager() + .deleteFolder(newFolderName) + .verify { folderWithNameDoesNotExist(newFolderName) } + } + + @Category(SmokeTest::class) + @Test + fun addMessageToCustomFolderFromSent() { + val folderName = "Folder 1" + loginRobot + .loginUser(onePassUser) + .menuDrawer() + .sent() + .clickMessageByPosition(1) + .openFoldersModal() + .moveMessageFromSentToFolder(folderName) + .menuDrawer() + .labelOrFolder(folderName) + .verify { messageExists(selectedMessageSubject) } + } + + @Test + fun createRenameAndDeleteLabelFromInbox() { + val labelName = StringUtils.getAlphaNumericStringWithSpecialCharacters() + val newLabelName = StringUtils.getAlphaNumericStringWithSpecialCharacters() + loginRobot + .loginUser(onePassUser) + .clickMessageByPosition(1) + .openLabelsModal() + .addLabel(labelName) + .selectLabelByName(labelName) + .apply() + .navigateUpToInbox() + .menuDrawer() + .settings() + .openUserAccountSettings(onePassUser) + .foldersAndLabels() + .labelsManager() + .editLabel(labelName, newLabelName, 2) + .deleteLabel(labelName) + .verify { labelWithNameDoesNotExist(labelName) } + } + + @Category(SmokeTest::class) + @Test + fun applyLabelToMessageFromSent() { + val labelName = "Label 1" + loginRobot + .loginUser(onePassUser) + .menuDrawer() + .sent() + .clickMessageByPosition(1) + .openLabelsModal() + .selectLabelByName(labelName) + .apply() + .navigateUpToSent() + .menuDrawer() + .labelOrFolder(labelName) + .refreshMessageList() + .verify { messageExists(selectedMessageSubject) } + } + + // Enable after MAILAND-1280 is fixed + fun applyLabelToMultipleMessagesFromSent() { + val labelName = "Label 1" + loginRobot + .loginUser(onePassUser) + .menuDrawer() + .sent() + .longClickMessageOnPosition(1) + .selectMessage(2) + .addLabel() + .selectLabelByName(labelName) + .apply() + .menuDrawer() + .labelOrFolder(labelName) + .refreshMessageList() + .verify { + messageExists(longClickedMessageSubject) + messageExists(selectedMessageSubject) + } + } +} diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/tests/settings/AccountSettingsTests.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/tests/settings/AccountSettingsTests.kt index 4eda5c951..1393f087a 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/tests/settings/AccountSettingsTests.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/tests/settings/AccountSettingsTests.kt @@ -18,10 +18,11 @@ */ package ch.protonmail.android.uitests.tests.settings -import ch.protonmail.android.uitests.actions.settings.account.AccountSettingsRobot +import ch.protonmail.android.uitests.robots.settings.account.AccountSettingsRobot import ch.protonmail.android.uitests.robots.login.LoginRobot import ch.protonmail.android.uitests.tests.BaseTest import ch.protonmail.android.uitests.testsHelper.TestData +import ch.protonmail.android.uitests.testsHelper.TestData.onePassUser import ch.protonmail.android.uitests.testsHelper.annotations.SmokeTest import org.junit.experimental.categories.Category import kotlin.test.BeforeTest @@ -36,10 +37,10 @@ class AccountSettingsTests : BaseTest() { override fun setUp() { super.setUp() loginRobot - .loginUser(TestData.onePassUser) + .loginUser(onePassUser) .menuDrawer() .settings() - .selectSettingsItemByValue(TestData.onePassUser.email) + .openUserAccountSettings(onePassUser) } @Test diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/tests/spam/SpamTests.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/tests/spam/SpamTests.kt index ef0c5e1e6..0405e6d77 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/tests/spam/SpamTests.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/tests/spam/SpamTests.kt @@ -55,7 +55,7 @@ class SpamTests : BaseTest() { .trash() .clickMessageBySubject(subject) .openFoldersModal() - .moveFromSpamToFolder(stringFromResource(R.string.inbox)) + .moveMessageFromSpamToFolder(stringFromResource(R.string.inbox)) .menuDrawer() .inbox() .verify { messageWithSubjectExists(subject) } diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/tests/suites/SmokeSuite.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/tests/suites/SmokeSuite.kt index 2d901c54c..3ccc6d0cc 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/tests/suites/SmokeSuite.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/tests/suites/SmokeSuite.kt @@ -24,6 +24,7 @@ import ch.protonmail.android.uitests.tests.composer.SendNewMessageTests import ch.protonmail.android.uitests.tests.contacts.ContactsTests import ch.protonmail.android.uitests.tests.inbox.InboxTests import ch.protonmail.android.uitests.tests.inbox.SearchTests +import ch.protonmail.android.uitests.tests.labelsfolders.LabelsFoldersTests import ch.protonmail.android.uitests.tests.login.LoginTests import ch.protonmail.android.uitests.tests.manageaccounts.MultiuserManagementTests import ch.protonmail.android.uitests.tests.menu.MenuTests @@ -43,6 +44,7 @@ import org.junit.runners.Suite ReplyToMessageTests::class, ContactsTests::class, InboxTests::class, + LabelsFoldersTests::class, LoginTests::class, MultiuserManagementTests::class, MenuTests::class, diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/MockAddAttachmentIntent.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/MockAddAttachmentIntent.kt index e0091e391..9294784e0 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/MockAddAttachmentIntent.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/MockAddAttachmentIntent.kt @@ -33,20 +33,19 @@ import androidx.test.espresso.intent.matcher.IntentMatchers import androidx.test.platform.app.InstrumentationRegistry import androidx.test.runner.intent.IntentCallback import androidx.test.runner.intent.IntentMonitorRegistry +import ch.protonmail.android.uitests.testsHelper.uiactions.UIActions import org.jetbrains.annotations.Contract import java.io.File import java.io.FileOutputStream import java.io.IOException import java.util.ArrayList -/** - * Created by Nikola Nolchevski on 13-May-20. - */ object MockAddAttachmentIntent { private fun createImage(@IdRes resourceId: Int) { val icon = BitmapFactory.decodeResource( InstrumentationRegistry.getInstrumentation().targetContext.resources, - resourceId) + resourceId + ) val file = File(InstrumentationRegistry.getInstrumentation().targetContext.externalCacheDir, "pickImageResult.jpeg") try { val fos = FileOutputStream(file) @@ -95,7 +94,8 @@ object MockAddAttachmentIntent { val imageUri = intent.getParcelableExtra(MediaStore.EXTRA_OUTPUT)!! val image = BitmapFactory.decodeResource( InstrumentationRegistry.getInstrumentation().targetContext.resources, - resourceId) + resourceId + ) val out = InstrumentationRegistry.getInstrumentation().targetContext.contentResolver.openOutputStream(imageUri) image.compress(Bitmap.CompressFormat.JPEG, 100, out) assert(out != null) diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/StringUtils.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/StringUtils.kt index bfa05f97a..b47374079 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/StringUtils.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/StringUtils.kt @@ -35,7 +35,7 @@ object StringUtils { } fun getAlphaNumericStringWithSpecialCharacters(length: Long = 10): String { - val source = "aäbcdeëfghijklmnoöpqrstuuüvwxyz1234567890!@+_)(*&^%$#@!" + val source = "abcdefghijklmnopqrstuuvwxyz1234567890!@+_)(*&^%$#@!" return Random().ints(length, 0, source.length) .toArray() .map(source::get) diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/UIActions.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/UIActions.kt deleted file mode 100644 index 0afe3ba68..000000000 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/UIActions.kt +++ /dev/null @@ -1,470 +0,0 @@ -/* - * Copyright (c) 2020 Proton Technologies AG - * - * This file is part of ProtonMail. - * - * ProtonMail is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ProtonMail is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ProtonMail. If not, see https://www.gnu.org/licenses/. - */ -package ch.protonmail.android.uitests.testsHelper - -import android.content.Context -import android.content.Intent -import android.view.View -import android.widget.ListView -import androidx.annotation.IdRes -import androidx.annotation.StringRes -import androidx.appcompat.widget.ActionMenuView -import androidx.appcompat.widget.AppCompatImageButton -import androidx.appcompat.widget.AppCompatImageView -import androidx.recyclerview.widget.RecyclerView -import androidx.test.espresso.Espresso.onData -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.ViewInteraction -import androidx.test.espresso.action.ViewActions -import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.action.ViewActions.closeSoftKeyboard -import androidx.test.espresso.action.ViewActions.longClick -import androidx.test.espresso.action.ViewActions.pressImeActionButton -import androidx.test.espresso.action.ViewActions.replaceText -import androidx.test.espresso.action.ViewActions.swipeDown -import androidx.test.espresso.action.ViewActions.swipeLeft -import androidx.test.espresso.action.ViewActions.swipeRight -import androidx.test.espresso.action.ViewActions.typeText -import androidx.test.espresso.assertion.ViewAssertions.doesNotExist -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.contrib.DrawerActions.close -import androidx.test.espresso.contrib.DrawerActions.open -import androidx.test.espresso.contrib.RecyclerViewActions.actionOnHolderItem -import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition -import androidx.test.espresso.contrib.RecyclerViewActions.scrollToHolder -import androidx.test.espresso.matcher.RootMatchers.isDialog -import androidx.test.espresso.matcher.RootMatchers.isPlatformPopup -import androidx.test.espresso.matcher.ViewMatchers.Visibility -import androidx.test.espresso.matcher.ViewMatchers.isChecked -import androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.isEnabled -import androidx.test.espresso.matcher.ViewMatchers.isNotChecked -import androidx.test.espresso.matcher.ViewMatchers.withContentDescription -import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility -import androidx.test.espresso.matcher.ViewMatchers.withHint -import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withParent -import androidx.test.espresso.matcher.ViewMatchers.withTagValue -import androidx.test.espresso.matcher.ViewMatchers.withText -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.uiautomator.By -import androidx.test.uiautomator.UiDevice -import androidx.test.uiautomator.Until -import ch.protonmail.android.R -import ch.protonmail.android.uitests.robots.contacts.ContactsMatchers.withContactEmail -import ch.protonmail.android.uitests.robots.contacts.ContactsMatchers.withContactEmailInManageAddressesView -import ch.protonmail.android.uitests.robots.contacts.ContactsMatchers.withContactGroupName -import ch.protonmail.android.uitests.robots.manageaccounts.ManageAccountsMatchers.withAccountEmailInAccountManager -import ch.protonmail.android.uitests.testsHelper.StringUtils.stringFromResource -import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.checkContactDoesNotExist -import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.checkMessageDoesNotExist -import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.clickOnChildWithId -import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.performActionWithRetry -import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.saveMessageSubject -import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.waitForAdapterItemWithIdAndText -import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.waitUntilIntentMatcherFulfilled -import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.waitUntilMatcherFulfilled -import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.waitUntilRecyclerViewPopulated -import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.waitUntilViewAppears -import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.waitUntilViewIsGone -import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.waitUntilViewIsNotDisplayed -import org.hamcrest.CoreMatchers.`is` -import org.hamcrest.CoreMatchers.allOf -import org.hamcrest.CoreMatchers.anything -import org.hamcrest.CoreMatchers.containsString -import org.hamcrest.CoreMatchers.instanceOf -import org.hamcrest.CoreMatchers.not -import org.hamcrest.Matcher -import org.junit.Assert - -fun ViewInteraction.click(): ViewInteraction = - this.perform(ViewActions.click()) - -fun ViewInteraction.insert(text: String): ViewInteraction = - this.perform(replaceText(text), closeSoftKeyboard()) - -fun ViewInteraction.type(text: String): ViewInteraction = - this.perform(typeText(text), closeSoftKeyboard()) - -fun ViewInteraction.swipeViewDown(): ViewInteraction = - this.perform(swipeDown()) - -object UIActions { - - private val targetContext: Context = InstrumentationRegistry.getInstrumentation().targetContext - private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) - - val allOf = AllOf() - - class AllOf { - fun clickMatchedView(viewMatcher: Matcher): ViewInteraction = - onView(viewMatcher).perform(click()) - - fun clickViewWithIdAndAncestorTag(@IdRes id: Int, ancestorTag: String): ViewInteraction = - onView(allOf(withId(id), isDescendantOfA(withTagValue(`is`(ancestorTag))))) - .perform(click()) - - fun clickViewWithIdAndText(@IdRes id: Int, text: String): ViewInteraction = - onView(allOf(withId(id), withText(text))).perform(click()) - - fun clickVisibleViewWithId(@IdRes id: Int): ViewInteraction = - onView(allOf(withId(id), withEffectiveVisibility(Visibility.VISIBLE))).perform(click()) - - fun clickViewWithParentIdAndClass(@IdRes id: Int, clazz: Class<*>): ViewInteraction = - onView(allOf(instanceOf(clazz), withParent(withId(id)))).perform(click()) - - fun clickViewByClassAndParentClass(clazz: Class<*>, parentClazz: Class<*>): ViewInteraction = - onView(allOf(instanceOf(clazz), withParent(instanceOf(parentClazz)))).perform(click())!! - - fun clickViewWithIdAndText(@IdRes id: Int, @StringRes stringRes: Int): ViewInteraction = - onView(allOf(withId(id), withText(stringRes))) - .check(matches(isDisplayed())) - .perform(click()) - - fun setTextIntoFieldWithIdAndAncestorTag( - @IdRes id: Int, - ancestorTag: String, - text: String - ): ViewInteraction = - onView(allOf(withId(id), isDescendantOfA(withTagValue(`is`(ancestorTag))))) - .perform(replaceText(text)) - - fun setTextIntoFieldWithIdAndHint( - @IdRes id: Int, - @StringRes stringId: Int, - text: String - ): ViewInteraction = - onView(allOf(withId(id), withHint(stringId))).perform(replaceText(text)) - - fun setTextIntoFieldByIdAndParent( - @IdRes id: Int, - @IdRes ancestorId: Int, - text: String - ): ViewInteraction = - onView(allOf(withId(id), withEffectiveVisibility(Visibility.VISIBLE), isDescendantOfA(withId(ancestorId)))) - .perform(replaceText(text)) - } - - val check = Check() - - class Check { - fun viewWithIdAndTextIsDisplayed(@IdRes id: Int, text: String): ViewInteraction = - onView(allOf(withId(id), withText(text))).check(matches(isDisplayed())) - - fun viewWithIdIsNotDisplayed(@IdRes id: Int): ViewInteraction = - onView(withId(id)).check(matches(not(isDisplayed()))) - - fun viewWithIdIsContainsText(@IdRes id: Int, text: String): ViewInteraction = - onView(withId(id)).check(matches(withText(containsString(text)))) - - fun viewWithIdAndTextDoesNotExist(@IdRes id: Int, text: String): ViewInteraction = - onView(allOf(withId(id), withText(text))).check(doesNotExist()) - - fun viewWithTextIsChecked(@StringRes textId: Int): ViewInteraction = - onView(withText(textId)).check(matches(isChecked())) - - fun viewWithIdAndAncestorTagIsChecked( - @IdRes id: Int, - ancestorTag: String, - state: Boolean - ): ViewInteraction { - return when (state) { - true -> - onView(allOf(withId(id), isDescendantOfA(withTagValue(`is`(ancestorTag))))) - .check(matches(isChecked())) - false -> - onView(allOf(withId(id), isDescendantOfA(withTagValue(`is`(ancestorTag))))) - .check(matches(isNotChecked())) - } - } - - fun viewWithIdIsDisplayed(@IdRes id: Int): ViewInteraction = onView(withId(id)).check(matches(isDisplayed())) - - fun viewWithTextIsDisplayed(text: String): ViewInteraction = - onView(withText(text)).check(matches(isDisplayed())) - - fun viewWithTextDoesNotExist(@StringRes textId: Int): ViewInteraction = - onView(withText(stringFromResource(textId))).check(doesNotExist()) - - fun viewWithIdAndTextIsDisplayed(@IdRes id: Int, @StringRes text: Int): ViewInteraction = - onView(allOf(withId(id), withText(text))).check(matches(isDisplayed())) - - fun alertDialogWithTextIsDisplayed(@StringRes textId: Int): ViewInteraction = - onView(withText(textId)).inRoot(isDialog()).check(matches(isDisplayed())) - } - - val contentDescription = ContentDescription() - - class ContentDescription { - fun clickViewWithContentDescSubstring(contDesc: String) { - onView(withContentDescription(containsString(contDesc))).perform(click()) - } - } - - val hint = Hint() - - class Hint { - fun insertTextIntoFieldWithHint(@IdRes hintText: Int, text: String) { - onView(withHint(hintText)).perform(replaceText(text)) - } - } - - val id = Id() - - class Id { - fun clickViewWithId(@IdRes id: Int): ViewInteraction = onView(withId(id)).perform(click()) - - fun insertTextIntoFieldWithId(@IdRes id: Int, text: String): ViewInteraction = - onView(withId(id)).perform(replaceText(text), closeSoftKeyboard()) - - fun insertTextInFieldWithIdAndPressImeAction(@IdRes id: Int, text: String): ViewInteraction = - onView(withId(id)).check(matches(isDisplayed())).perform(replaceText(text), pressImeActionButton()) - - fun openMenuDrawerWithId(@IdRes id: Int): ViewInteraction = onView(withId(id)).perform(close(), open()) - - fun swipeLeftViewWithId(@IdRes id: Int): ViewInteraction = onView(withId(id)).perform(swipeLeft()) - - fun swipeDownViewWithId(@IdRes id: Int): ViewInteraction = onView(withId(id)).perform(swipeDown()) - - fun typeTextIntoFieldWithIdAndPressImeAction(@IdRes id: Int, text: String): ViewInteraction = - onView(withId(id)).perform(click(), typeText(text), pressImeActionButton()) - - fun typeTextIntoFieldWithId(@IdRes id: Int, text: String): ViewInteraction = - onView(withId(id)).perform(click(), typeText(text), closeSoftKeyboard()) - } - - val listView = List() - - class List { - fun clickListItemByPosition(position: Int) { - onData(anything()) - .inAdapterView(instanceOf(ListView::class.java)) - .inRoot(isPlatformPopup()) - .atPosition(position) - .perform(click()) - } - } - - val recyclerView = Recycler() - - class Recycler { - - fun clickOnRecyclerViewMatchedItem( - @IdRes recyclerViewId: Int, - withMatcher: Matcher - ): ViewInteraction = - onView(withId(recyclerViewId)).perform(actionOnHolderItem(withMatcher, click())) - - fun clickOnRecyclerViewMatchedItemWithRetry( - @IdRes recyclerViewId: Int, - withMatcher: Matcher - ): ViewInteraction = - performActionWithRetry(onView(withId(recyclerViewId)), actionOnHolderItem(withMatcher, click())) - - fun clickContactItem(@IdRes recyclerViewId: Int, withEmail: String): ViewInteraction = - onView(withId(recyclerViewId)).perform(actionOnHolderItem(withContactEmail(withEmail), click())) - - fun clickContactItemWithRetry(@IdRes recyclerViewId: Int, withEmail: String): ViewInteraction = - performActionWithRetry( - onView(withId(recyclerViewId)), - actionOnHolderItem(withContactEmail(withEmail), click()) - ) - - fun clickContactItemView( - @IdRes recyclerViewId: Int, - withEmail: String, - @IdRes childViewId: Int - ): ViewInteraction = onView(withId(recyclerViewId)) - .perform(actionOnHolderItem(withContactEmail(withEmail), clickOnChildWithId(childViewId))) - - fun clickContactsGroupItemView( - @IdRes recyclerViewId: Int, - withName: String, - @IdRes childViewId: Int - ): ViewInteraction = onView(withId(recyclerViewId)) - .perform(actionOnHolderItem(withContactGroupName(withName), clickOnChildWithId(childViewId))) - - fun checkDoesNotContainMessage(@IdRes recyclerViewId: Int, subject: String, date: String): ViewInteraction = - onView(withId(recyclerViewId)).perform(checkMessageDoesNotExist(subject, date)) - - fun checkDoesNotContainContact(@IdRes recyclerViewId: Int, name: String, email: String): ViewInteraction = - onView(withId(recyclerViewId)).perform(checkContactDoesNotExist(name, email)) - - fun clickContactsGroupItem(@IdRes recyclerViewId: Int, withName: String): ViewInteraction = - onView(withId(recyclerViewId)).perform(actionOnHolderItem(withContactGroupName(withName), click())) - - fun selectContactsInManageAddresses(@IdRes recyclerViewId: Int, withEmail: String): ViewInteraction = - onView(withId(recyclerViewId)) - .perform(actionOnHolderItem(withContactEmailInManageAddressesView(withEmail), click())) - - fun clickAccountManagerViewItem( - @IdRes recyclerViewId: Int, - email: String, - @IdRes childViewId: Int - ): ViewInteraction = onView(withId(recyclerViewId)) - .perform(actionOnHolderItem(withAccountEmailInAccountManager(email), clickOnChildWithId(childViewId))) - - fun clickOnRecyclerViewItemByPosition(@IdRes recyclerViewId: Int, position: Int): ViewInteraction = - onView(withId(recyclerViewId)).perform(actionOnItemAtPosition(position, click())) - - fun longClickItemInRecyclerView(@IdRes recyclerViewId: Int, childPosition: Int): ViewInteraction = - onView(withId(recyclerViewId)) - .perform(actionOnItemAtPosition(childPosition, longClick())) - - fun saveMessageSubjectAtPosition( - @IdRes recyclerViewId: Int, - position: Int, - method: (String, String) -> Unit - ): ViewInteraction = onView(withId(recyclerViewId)).perform(saveMessageSubject(position, method)) - - fun scrollToRecyclerViewMatchedItem( - @IdRes recyclerViewId: Int, - withMatcher: Matcher - ): ViewInteraction = - onView(withId(recyclerViewId)).perform(scrollToHolder(withMatcher)) - - fun swipeItemLeftToRightOnPosition(@IdRes recyclerViewId: Int, childPosition: Int): ViewInteraction = - onView(withId(recyclerViewId)) - .perform(actionOnItemAtPosition(childPosition, swipeRight())) - - fun swipeDownToRightOnPosition(@IdRes recyclerViewId: Int, childPosition: Int): ViewInteraction = - onView(withId(recyclerViewId)) - .perform(actionOnItemAtPosition(childPosition, swipeDown())) - - fun swipeRightToLeftObjectWithIdAtPosition(@IdRes recyclerViewId: Int, childPosition: Int): ViewInteraction = - onView(withId(recyclerViewId)) - .perform(actionOnItemAtPosition(childPosition, swipeLeft())) - - fun waitForBeingPopulated(@IdRes recyclerViewId: Int): Recycler { - waitUntilRecyclerViewPopulated(recyclerViewId) - return this - } - - fun waitForItemWithIdAndText(@IdRes recyclerViewId: Int, @IdRes viewId: Int, text: String): Recycler { - waitForAdapterItemWithIdAndText(recyclerViewId, viewId, text) - return this - } - } - - val system = System() - - class System { - fun clickHamburgerOrUpButton(): ViewInteraction = - allOf.clickViewWithParentIdAndClass(R.id.toolbar, AppCompatImageButton::class.java) - - fun clickHamburgerOrUpButtonInAnimatedToolbar(): ViewInteraction = - allOf.clickViewWithParentIdAndClass(R.id.animToolbar, AppCompatImageButton::class.java) - - fun waitForMoreOptionsButton(): ViewInteraction = - wait.forViewByViewInteraction( - onView( - allOf( - instanceOf(AppCompatImageView::class.java), - withParent(instanceOf(ActionMenuView::class.java)) - ) - ) - ) - - fun clickMoreOptionsButton(): ViewInteraction = - allOf.clickViewByClassAndParentClass(AppCompatImageView::class.java, ActionMenuView::class.java) - - fun clickNegativeDialogButton(): ViewInteraction = id.clickViewWithId(android.R.id.button2) - - fun clickPositiveDialogButton(): ViewInteraction = id.clickViewWithId(android.R.id.button1) - - fun clickPositiveButtonInDialogRoot(): ViewInteraction = id.clickViewWithId(android.R.id.button1) - } - - val tag = Tag() - - class Tag { - fun clickViewWithTag(@StringRes tagStringId: Int): ViewInteraction = - onView(withTagValue(`is`(targetContext.resources.getString(tagStringId)))).perform(click()) - } - - val text = Text() - - class Text { - fun clickViewWithText(@IdRes text: Int): ViewInteraction = - onView(withText(text)).check(matches(isDisplayed())).perform(click()) - - fun clickViewWithText(text: String): ViewInteraction = - onView(withText(text)).check(matches(isDisplayed())).perform(click()) - } - - val wait = Wait() - - class Wait { - - fun forIntent(matcher: Matcher) = waitUntilIntentMatcherFulfilled(matcher) - - fun forViewWithIdAndText(@IdRes id: Int, text: String): ViewInteraction = - waitUntilViewAppears(onView(allOf(withId(id), withText(text)))) - - fun forViewWithIdAndText(@IdRes id: Int, textId: Int, timeout: Long = 5000): ViewInteraction = - waitUntilViewAppears(onView(allOf(withId(id), withText(textId))), timeout) - - fun forViewWithContentDescription(@StringRes textId: Int): ViewInteraction = - waitUntilViewAppears(onView(withContentDescription(containsString(stringFromResource(textId))))) - - fun forViewWithText(@StringRes textId: Int): ViewInteraction = - waitUntilViewAppears(onView(withText(stringFromResource(textId)))) - - fun forViewWithText(text: String): ViewInteraction = - waitUntilViewAppears(onView(withText(text))) - - fun forViewWithTextByUiAutomator(text: String) { - Assert.assertTrue(device.wait(Until.hasObject(By.text(text)), 5000)) - } - - fun forViewWithId(@IdRes id: Int, timeout: Long = 10_000L): ViewInteraction = - waitUntilViewAppears(onView(withId(id)), timeout) - - fun forViewWithTextAndParentId(@StringRes text: Int, @IdRes parentId: Int): ViewInteraction = - waitUntilViewAppears(onView(allOf(withText(text), withParent(withId(parentId))))) - - fun forViewWithIdAndAncestorId(@IdRes id: Int, @IdRes parentId: Int): ViewInteraction = - waitUntilViewAppears(onView(allOf(withId(id), isDescendantOfA(withId(parentId))))) - - fun forViewOfInstanceWithParentId(@IdRes id: Int, clazz: Class<*>, timeout: Long = 5000): ViewInteraction = - waitUntilViewAppears(onView(allOf(instanceOf(clazz), withParent(withId(id)))), timeout) - - fun forViewWithTextAndParentId(text: String, @IdRes parentId: Int): ViewInteraction = - waitUntilViewAppears(onView(allOf(withText(text), withParent(withId(parentId))))) - - fun forViewByViewInteraction(interaction: ViewInteraction): ViewInteraction = - waitUntilViewAppears(interaction) - - fun untilViewWithIdEnabled(@IdRes id: Int): ViewInteraction = - waitUntilMatcherFulfilled(onView(withId(id)), matches(isEnabled())) - - fun untilViewWithIdDisabled(@IdRes id: Int): ViewInteraction = - waitUntilMatcherFulfilled(onView(withId(id)), matches(not(isEnabled()))) - - fun untilViewWithIdIsGone(@IdRes id: Int): ViewInteraction = - waitUntilViewIsGone(onView(withId(id))) - - fun untilViewWithIdIsNotShown(@IdRes id: Int): ViewInteraction = - waitUntilViewIsNotDisplayed(onView(withId(id))) - - fun untilViewByViewInteractionIsGone(interaction: ViewInteraction): ViewInteraction = - waitUntilViewIsGone(interaction) - } -} diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/UICustomViewActions.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/UICustomViewActions.kt index 1d5ec0391..caa9a484e 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/UICustomViewActions.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/UICustomViewActions.kt @@ -411,6 +411,7 @@ object UICustomViewActions { } } + fun checkGroupDoesNotExist(name: String, email: String): PositionableRecyclerViewAction = CheckGroupDoesNotExist(name, email) @@ -449,7 +450,7 @@ object UICustomViewActions { fun clickOnChildWithId(@IdRes id: Int): ViewAction { return object : ViewAction { override fun perform(uiController: UiController, view: View) { - view.findViewById(id).callOnClick() + view.findViewById(id).performClick() } override fun getDescription(): String = "Click child view with id." diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/AllOf.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/AllOf.kt new file mode 100644 index 000000000..f2697edfd --- /dev/null +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/AllOf.kt @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * + * This file is part of ProtonMail. + * + * ProtonMail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonMail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonMail. If not, see https://www.gnu.org/licenses/. + */ + +package ch.protonmail.android.uitests.testsHelper.uiactions + +import android.view.View +import androidx.annotation.IdRes +import androidx.annotation.StringRes +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.ViewInteraction +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.replaceText +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility +import androidx.test.espresso.matcher.ViewMatchers.withHint +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withParent +import androidx.test.espresso.matcher.ViewMatchers.withTagValue +import androidx.test.espresso.matcher.ViewMatchers.withText +import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.CoreMatchers.allOf +import org.hamcrest.CoreMatchers.instanceOf +import org.hamcrest.Matcher + +object AllOf { + fun clickMatchedView(viewMatcher: Matcher): ViewInteraction = + onView(viewMatcher).perform(click()) + + fun clickViewWithIdAndAncestorTag(@IdRes id: Int, ancestorTag: String): ViewInteraction = + onView(allOf(withId(id), isDescendantOfA(withTagValue(`is`(ancestorTag))))) + .perform(click()) + + fun clickViewWithIdAndText(@IdRes id: Int, text: String): ViewInteraction = + onView(allOf(withId(id), withText(text))).perform(click()) + + fun clickVisibleViewWithId(@IdRes id: Int): ViewInteraction = + onView(allOf(withId(id), withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))).perform(click()) + + fun clickViewWithParentIdAndClass(@IdRes id: Int, clazz: Class<*>): ViewInteraction = + onView(allOf(instanceOf(clazz), withParent(withId(id)))).perform(click()) + + fun clickViewByClassAndParentClass(clazz: Class<*>, parentClazz: Class<*>): ViewInteraction = + onView(allOf(instanceOf(clazz), withParent(instanceOf(parentClazz)))).perform(click())!! + + fun clickViewWithIdAndText(@IdRes id: Int, @StringRes stringRes: Int): ViewInteraction = + onView(allOf(withId(id), withText(stringRes))) + .check(matches(isDisplayed())) + .perform(click()) + + fun setTextIntoFieldWithIdAndAncestorTag( + @IdRes id: Int, + ancestorTag: String, + text: String + ): ViewInteraction = + onView(allOf(withId(id), isDescendantOfA(withTagValue(`is`(ancestorTag))))) + .perform(replaceText(text)) + + fun setTextIntoFieldWithIdAndHint( + @IdRes id: Int, + @StringRes stringId: Int, + text: String + ): ViewInteraction = + onView(allOf(withId(id), withHint(stringId))).perform(replaceText(text)) + + fun setTextIntoFieldByIdAndParent( + @IdRes id: Int, + @IdRes ancestorId: Int, + text: String + ): ViewInteraction = + onView(allOf(withId(id), withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE), isDescendantOfA(withId(ancestorId)))) + .perform(replaceText(text)) +} diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Check.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Check.kt new file mode 100644 index 000000000..605450512 --- /dev/null +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Check.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * + * This file is part of ProtonMail. + * + * ProtonMail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonMail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonMail. If not, see https://www.gnu.org/licenses/. + */ + +package ch.protonmail.android.uitests.testsHelper.uiactions + +import androidx.annotation.IdRes +import androidx.annotation.StringRes +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.ViewInteraction +import androidx.test.espresso.assertion.ViewAssertions.doesNotExist +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.RootMatchers +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.isChecked +import androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isNotChecked +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withTagValue +import androidx.test.espresso.matcher.ViewMatchers.withText +import ch.protonmail.android.uitests.testsHelper.StringUtils +import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.CoreMatchers.allOf +import org.hamcrest.CoreMatchers.containsString +import org.hamcrest.CoreMatchers.not + +object Check { + + fun viewWithIdAndTextIsDisplayed(@IdRes id: Int, text: String): ViewInteraction = + onView(allOf(withId(id), withText(text))).check(matches(isDisplayed())) + + fun viewWithIdIsNotDisplayed(@IdRes id: Int): ViewInteraction = + onView(withId(id)).check(matches(not(isDisplayed()))) + + fun viewWithIdIsContainsText(@IdRes id: Int, text: String): ViewInteraction = + onView(withId(id)).check(matches(withText(containsString(text)))) + + fun viewWithIdAndTextDoesNotExist(@IdRes id: Int, text: String): ViewInteraction = + onView(allOf(withId(id), withText(text))).check(doesNotExist()) + + fun viewWithIdAndAncestorTagIsChecked( + @IdRes id: Int, + ancestorTag: String, + state: Boolean + ): ViewInteraction { + return when (state) { + true -> + onView(allOf(withId(id), isDescendantOfA(withTagValue(`is`(ancestorTag))))) + .check(matches(isChecked())) + false -> + onView(allOf(withId(id), isDescendantOfA(withTagValue(`is`(ancestorTag))))) + .check(matches(isNotChecked())) + } + } + + fun viewWithIdIsDisplayed(@IdRes id: Int): ViewInteraction = onView(withId(id)).check(matches(isDisplayed())) + + fun viewWithTextIsDisplayed(text: String): ViewInteraction = + onView(withText(text)).check(matches(isDisplayed())) + + fun viewWithTextDoesNotExist(@StringRes textId: Int): ViewInteraction = + onView(withText(StringUtils.stringFromResource(textId))).check(doesNotExist()) + + fun viewWithIdAndTextIsDisplayed(@IdRes id: Int, @StringRes text: Int): ViewInteraction = + onView(allOf(withId(id), withText(text))).check(matches(isDisplayed())) + + fun alertDialogWithTextIsDisplayed(@StringRes textId: Int): ViewInteraction = + onView(withText(textId)).inRoot(RootMatchers.isDialog()).check(matches(isDisplayed())) + + fun viewWithTextIsChecked(@StringRes textId: Int): ViewInteraction = + onView(withText(textId)).inRoot(RootMatchers.isDialog()).check(matches(isChecked())) +} diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/ContentDescription.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/ContentDescription.kt new file mode 100644 index 000000000..0dbf9592e --- /dev/null +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/ContentDescription.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * + * This file is part of ProtonMail. + * + * ProtonMail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonMail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonMail. If not, see https://www.gnu.org/licenses/. + */ + +package ch.protonmail.android.uitests.testsHelper.uiactions + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.matcher.ViewMatchers.withContentDescription +import org.hamcrest.CoreMatchers.containsString + +object ContentDescription { + fun clickViewWithContentDescSubstring(contDesc: String) { + onView(withContentDescription(containsString(contDesc))).perform(click()) + } +} diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Extensions.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Extensions.kt new file mode 100644 index 000000000..717641410 --- /dev/null +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Extensions.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * + * This file is part of ProtonMail. + * + * ProtonMail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonMail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonMail. If not, see https://www.gnu.org/licenses/. + */ + +package ch.protonmail.android.uitests.testsHelper.uiactions + +import androidx.test.espresso.ViewInteraction +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.action.ViewActions.closeSoftKeyboard +import androidx.test.espresso.action.ViewActions.replaceText +import androidx.test.espresso.action.ViewActions.swipeDown +import androidx.test.espresso.action.ViewActions.typeText + +fun ViewInteraction.click(): ViewInteraction = + this.perform(ViewActions.click()) + +fun ViewInteraction.insert(text: String): ViewInteraction = + this.perform(replaceText(text), closeSoftKeyboard()) + +fun ViewInteraction.type(text: String): ViewInteraction = + this.perform(typeText(text), closeSoftKeyboard()) + +fun ViewInteraction.swipeViewDown(): ViewInteraction = + this.perform(swipeDown()) diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Hint.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Hint.kt new file mode 100644 index 000000000..aae90bb64 --- /dev/null +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Hint.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * + * This file is part of ProtonMail. + * + * ProtonMail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonMail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonMail. If not, see https://www.gnu.org/licenses/. + */ + +package ch.protonmail.android.uitests.testsHelper.uiactions + +import androidx.annotation.IdRes +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.replaceText +import androidx.test.espresso.matcher.ViewMatchers.withHint + +object Hint { + fun insertTextIntoFieldWithHint(@IdRes hintText: Int, text: String) { + onView(withHint(hintText)).perform(replaceText(text)) + } +} diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Id.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Id.kt new file mode 100644 index 000000000..5d2fda73c --- /dev/null +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Id.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * + * This file is part of ProtonMail. + * + * ProtonMail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonMail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonMail. If not, see https://www.gnu.org/licenses/. + */ + +package ch.protonmail.android.uitests.testsHelper.uiactions + +import androidx.annotation.IdRes +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.ViewInteraction +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.closeSoftKeyboard +import androidx.test.espresso.action.ViewActions.pressImeActionButton +import androidx.test.espresso.action.ViewActions.replaceText +import androidx.test.espresso.action.ViewActions.swipeDown +import androidx.test.espresso.action.ViewActions.swipeLeft +import androidx.test.espresso.action.ViewActions.typeText +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.contrib.DrawerActions +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId + +object Id { + fun clickViewWithId(@IdRes id: Int): ViewInteraction = onView(withId(id)).perform(click()) + + fun insertTextIntoFieldWithId(@IdRes id: Int, text: String): ViewInteraction = + onView(withId(id)).perform(replaceText(text), closeSoftKeyboard()) + + fun insertTextInFieldWithIdAndPressImeAction(@IdRes id: Int, text: String): ViewInteraction = + onView(withId(id)).check(matches(isDisplayed())).perform(replaceText(text), pressImeActionButton()) + + fun openMenuDrawerWithId(@IdRes id: Int): ViewInteraction = onView(withId(id)).perform(DrawerActions.close(), DrawerActions.open()) + + fun swipeLeftViewWithId(@IdRes id: Int): ViewInteraction = onView(withId(id)).perform(swipeLeft()) + + fun swipeDownViewWithId(@IdRes id: Int): ViewInteraction = onView(withId(id)).perform(swipeDown()) + + fun typeTextIntoFieldWithIdAndPressImeAction(@IdRes id: Int, text: String): ViewInteraction = + onView(withId(id)).perform(click(), typeText(text), pressImeActionButton()) + + fun typeTextIntoFieldWithId(@IdRes id: Int, text: String): ViewInteraction = + onView(withId(id)).perform(click(), typeText(text), closeSoftKeyboard()) +} diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/List.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/List.kt new file mode 100644 index 000000000..c8c890bfd --- /dev/null +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/List.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * + * This file is part of ProtonMail. + * + * ProtonMail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonMail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonMail. If not, see https://www.gnu.org/licenses/. + */ + +package ch.protonmail.android.uitests.testsHelper.uiactions + +import android.widget.ListView +import androidx.annotation.IdRes +import androidx.test.espresso.Espresso.onData +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.RootMatchers.isPlatformPopup +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import org.hamcrest.CoreMatchers.anything +import org.hamcrest.CoreMatchers.instanceOf +import org.hamcrest.Matcher + +object List { + + fun checkItemWithTextExists(@IdRes adapterId: Int, text: String) { + onData(withText(text)) + .inAdapterView(withId(adapterId)) + .check(matches(isDisplayed())) + } + + fun clickListItemByPosition(position: Int) { + onData(anything()) + .inAdapterView(instanceOf(ListView::class.java)) + .inRoot(isPlatformPopup()) + .atPosition(position) + .perform(click()) + } + + fun clickListItemByText(matcher: Matcher?, @IdRes adapterId: Int) { + onData(matcher) + .inAdapterView(withId(adapterId)) + .perform(click()) + } + + fun clickListItemChildByTextAndId(matcher: Matcher?, @IdRes childId: Int, @IdRes adapterId: Int) { + onData(matcher) + .inAdapterView(withId(adapterId)) + .onChildView(withId(childId)) + .perform(click()) + } +} diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Recycler.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Recycler.kt new file mode 100644 index 000000000..e82da582d --- /dev/null +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Recycler.kt @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * + * This file is part of ProtonMail. + * + * ProtonMail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonMail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonMail. If not, see https://www.gnu.org/licenses/. + */ + +package ch.protonmail.android.uitests.testsHelper.uiactions + +import androidx.annotation.IdRes +import androidx.recyclerview.widget.RecyclerView +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.ViewInteraction +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.longClick +import androidx.test.espresso.action.ViewActions.swipeDown +import androidx.test.espresso.action.ViewActions.swipeLeft +import androidx.test.espresso.action.ViewActions.swipeRight +import androidx.test.espresso.contrib.RecyclerViewActions.actionOnHolderItem +import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition +import androidx.test.espresso.contrib.RecyclerViewActions.scrollToHolder +import androidx.test.espresso.matcher.ViewMatchers.withId +import ch.protonmail.android.uitests.robots.contacts.ContactsMatchers.withContactEmail +import ch.protonmail.android.uitests.robots.contacts.ContactsMatchers.withContactEmailInManageAddressesView +import ch.protonmail.android.uitests.robots.contacts.ContactsMatchers.withContactGroupName +import ch.protonmail.android.uitests.robots.manageaccounts.ManageAccountsMatchers.withAccountEmailInAccountManager +import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.checkContactDoesNotExist +import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.checkMessageDoesNotExist +import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.clickOnChildWithId +import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.performActionWithRetry +import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.saveMessageSubject +import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.waitForAdapterItemWithIdAndText +import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.waitUntilRecyclerViewPopulated +import org.hamcrest.Matcher + +object Recycler { + + val common = Common() + val contacts = Contacts() + val manageAccounts = ManageAccounts() + val messages = Messages() + + class Common { + + fun clickOnRecyclerViewMatchedItem( + @IdRes recyclerViewId: Int, + withMatcher: Matcher + ): ViewInteraction = + onView(withId(recyclerViewId)).perform(actionOnHolderItem(withMatcher, click())) + + fun clickOnRecyclerViewItemChild( + @IdRes recyclerViewId: Int, + withMatcher: Matcher, + @IdRes childViewId: Int + ): ViewInteraction = onView(withId(recyclerViewId)) + .perform(actionOnHolderItem(withMatcher, clickOnChildWithId(childViewId))) + + fun clickOnRecyclerViewMatchedItemWithRetry( + @IdRes recyclerViewId: Int, + withMatcher: Matcher + ): ViewInteraction = + performActionWithRetry(onView(withId(recyclerViewId)), actionOnHolderItem(withMatcher, click())) + + fun clickOnRecyclerViewItemByPosition(@IdRes recyclerViewId: Int, position: Int): ViewInteraction = + onView(withId(recyclerViewId)).perform(actionOnItemAtPosition(position, click())) + + fun longClickItemInRecyclerView(@IdRes recyclerViewId: Int, childPosition: Int): ViewInteraction = + onView(withId(recyclerViewId)) + .perform(actionOnItemAtPosition(childPosition, longClick())) + + fun scrollToRecyclerViewMatchedItem( + @IdRes recyclerViewId: Int, + withMatcher: Matcher + ): ViewInteraction = + onView(withId(recyclerViewId)).perform(scrollToHolder(withMatcher)) + + fun swipeItemLeftToRightOnPosition(@IdRes recyclerViewId: Int, childPosition: Int): ViewInteraction = + onView(withId(recyclerViewId)) + .perform(actionOnItemAtPosition(childPosition, swipeRight())) + + fun swipeDownToRightOnPosition(@IdRes recyclerViewId: Int, childPosition: Int): ViewInteraction = + onView(withId(recyclerViewId)) + .perform(actionOnItemAtPosition(childPosition, swipeDown())) + + fun swipeRightToLeftObjectWithIdAtPosition(@IdRes recyclerViewId: Int, childPosition: Int): ViewInteraction = + onView(withId(recyclerViewId)) + .perform(actionOnItemAtPosition(childPosition, swipeLeft())) + + fun waitForBeingPopulated(@IdRes recyclerViewId: Int): Recycler { + waitUntilRecyclerViewPopulated(recyclerViewId) + return Recycler + } + + fun waitForItemWithIdAndText(@IdRes recyclerViewId: Int, @IdRes viewId: Int, text: String): Recycler { + waitForAdapterItemWithIdAndText(recyclerViewId, viewId, text) + return Recycler + } + } + + class Contacts { + + fun clickContactItem(@IdRes recyclerViewId: Int, withEmail: String): ViewInteraction = + onView(withId(recyclerViewId)).perform(actionOnHolderItem(withContactEmail(withEmail), click())) + + fun clickContactItemWithRetry(@IdRes recyclerViewId: Int, withEmail: String): ViewInteraction = + performActionWithRetry( + onView(withId(recyclerViewId)), + actionOnHolderItem(withContactEmail(withEmail), click()) + ) + + fun clickContactItemView( + @IdRes recyclerViewId: Int, + withEmail: String, + @IdRes childViewId: Int + ): ViewInteraction = onView(withId(recyclerViewId)) + .perform(actionOnHolderItem(withContactEmail(withEmail), clickOnChildWithId(childViewId))) + + fun clickContactsGroupItemView( + @IdRes recyclerViewId: Int, + withName: String, + @IdRes childViewId: Int + ): ViewInteraction = onView(withId(recyclerViewId)) + .perform(actionOnHolderItem(withContactGroupName(withName), clickOnChildWithId(childViewId))) + + fun selectContactsInManageAddresses(@IdRes recyclerViewId: Int, withEmail: String): ViewInteraction = + onView(withId(recyclerViewId)) + .perform(actionOnHolderItem(withContactEmailInManageAddressesView(withEmail), click())) + + fun checkDoesNotContainContact(@IdRes recyclerViewId: Int, name: String, email: String): + ViewInteraction = onView(withId(recyclerViewId)).perform(checkContactDoesNotExist(name, email)) + + fun clickContactsGroupItem(@IdRes recyclerViewId: Int, withName: String): ViewInteraction = + onView(withId(recyclerViewId)).perform(actionOnHolderItem(withContactGroupName(withName), click())) + } + + class ManageAccounts { + + fun clickAccountManagerViewItem( + @IdRes recyclerViewId: Int, + email: String, + @IdRes childViewId: Int + ): ViewInteraction = onView(withId(recyclerViewId)) + .perform(actionOnHolderItem(withAccountEmailInAccountManager(email), clickOnChildWithId(childViewId))) + } + + class Messages { + + fun checkDoesNotContainMessage(@IdRes recyclerViewId: Int, subject: String, date: String): + ViewInteraction = onView(withId(recyclerViewId)).perform(checkMessageDoesNotExist(subject, date)) + + fun saveMessageSubjectAtPosition( + @IdRes recyclerViewId: Int, + position: Int, + method: (String, String) -> Unit + ): ViewInteraction = onView(withId(recyclerViewId)).perform(saveMessageSubject(position, method)) + } +} diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/System.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/System.kt new file mode 100644 index 000000000..9db2a160c --- /dev/null +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/System.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * + * This file is part of ProtonMail. + * + * ProtonMail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonMail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonMail. If not, see https://www.gnu.org/licenses/. + */ + +package ch.protonmail.android.uitests.testsHelper.uiactions + +import androidx.appcompat.widget.ActionMenuView +import androidx.appcompat.widget.AppCompatImageButton +import androidx.appcompat.widget.AppCompatImageView +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.ViewInteraction +import androidx.test.espresso.matcher.ViewMatchers.withParent +import ch.protonmail.android.R +import org.hamcrest.CoreMatchers.allOf +import org.hamcrest.CoreMatchers.instanceOf + +object System { + fun clickHamburgerOrUpButton(): ViewInteraction = + UIActions.allOf.clickViewWithParentIdAndClass(R.id.toolbar, AppCompatImageButton::class.java) + + fun clickHamburgerOrUpButtonInAnimatedToolbar(): ViewInteraction = + UIActions.allOf.clickViewWithParentIdAndClass(R.id.animToolbar, AppCompatImageButton::class.java) + + fun waitForMoreOptionsButton(): ViewInteraction = + UIActions.wait.forViewByViewInteraction( + onView( + allOf( + instanceOf(AppCompatImageView::class.java), + withParent(instanceOf(ActionMenuView::class.java)) + ) + ) + ) + + fun clickMoreOptionsButton(): ViewInteraction = + UIActions.allOf.clickViewByClassAndParentClass(AppCompatImageView::class.java, ActionMenuView::class.java) + + fun clickNegativeDialogButton(): ViewInteraction = UIActions.id.clickViewWithId(android.R.id.button2) + + fun clickPositiveDialogButton(): ViewInteraction = UIActions.id.clickViewWithId(android.R.id.button1) + + fun clickPositiveButtonInDialogRoot(): ViewInteraction = UIActions.id.clickViewWithId(android.R.id.button1) +} diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Tag.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Tag.kt new file mode 100644 index 000000000..ee3ff4b90 --- /dev/null +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Tag.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * + * This file is part of ProtonMail. + * + * ProtonMail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonMail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonMail. If not, see https://www.gnu.org/licenses/. + */ + +package ch.protonmail.android.uitests.testsHelper.uiactions + +import androidx.annotation.StringRes +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.ViewInteraction +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.matcher.ViewMatchers.withTagValue +import ch.protonmail.android.uitests.tests.BaseTest.Companion.targetContext +import org.hamcrest.CoreMatchers.`is` + +object Tag { + fun clickViewWithTag(@StringRes tagStringId: Int): ViewInteraction = + onView(withTagValue(`is`(targetContext.resources.getString(tagStringId)))).perform(click()) +} diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Text.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Text.kt new file mode 100644 index 000000000..c515e9ebd --- /dev/null +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Text.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * + * This file is part of ProtonMail. + * + * ProtonMail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonMail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonMail. If not, see https://www.gnu.org/licenses/. + */ + +package ch.protonmail.android.uitests.testsHelper.uiactions + +import androidx.annotation.IdRes +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.ViewInteraction +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withText + +object Text { + fun clickViewWithText(@IdRes text: Int): ViewInteraction = + onView(withText(text)).check(matches(isDisplayed())).perform(click()) + + fun clickViewWithText(text: String): ViewInteraction = + onView(withText(text)).check(matches(isDisplayed())).perform(click()) +} diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/UIActions.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/UIActions.kt new file mode 100644 index 000000000..beb8ba1a4 --- /dev/null +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/UIActions.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * + * This file is part of ProtonMail. + * + * ProtonMail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonMail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonMail. If not, see https://www.gnu.org/licenses/. + */ +package ch.protonmail.android.uitests.testsHelper.uiactions + +import android.content.Context +import androidx.test.platform.app.InstrumentationRegistry + +object UIActions { + + private val targetContext: Context = InstrumentationRegistry.getInstrumentation().targetContext + + val allOf = AllOf + val check = Check + val contentDescription = ContentDescription + val hint = Hint + val id = Id + val listView = List + val recyclerView = Recycler + val system = System + val tag = Tag + val text = Text + val wait = Wait +} diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Wait.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Wait.kt new file mode 100644 index 000000000..57d69d838 --- /dev/null +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/testsHelper/uiactions/Wait.kt @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * + * This file is part of ProtonMail. + * + * ProtonMail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonMail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonMail. If not, see https://www.gnu.org/licenses/. + */ + +package ch.protonmail.android.uitests.testsHelper.uiactions + +import android.content.Intent +import androidx.annotation.IdRes +import androidx.annotation.StringRes +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.ViewInteraction +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.isEnabled +import androidx.test.espresso.matcher.ViewMatchers.withContentDescription +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withParent +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.uiautomator.By +import androidx.test.uiautomator.Until +import ch.protonmail.android.uitests.tests.BaseTest.Companion.device +import ch.protonmail.android.uitests.testsHelper.StringUtils +import ch.protonmail.android.uitests.testsHelper.UICustomViewActions +import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.waitUntilMatcherFulfilled +import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.waitUntilViewAppears +import ch.protonmail.android.uitests.testsHelper.UICustomViewActions.waitUntilViewIsGone +import org.hamcrest.CoreMatchers.allOf +import org.hamcrest.CoreMatchers.containsString +import org.hamcrest.CoreMatchers.instanceOf +import org.hamcrest.Matcher +import org.junit.Assert + +object Wait { + + fun forViewWithTextByUiAutomator(text: String) { + Assert.assertTrue(device.wait(Until.hasObject(By.text(text)), 5000)) + } + + fun forViewByViewInteraction(interaction: ViewInteraction): ViewInteraction = + waitUntilViewAppears(interaction) + + fun forViewWithContentDescription(@StringRes textId: Int): ViewInteraction = + waitUntilViewAppears(onView(withContentDescription(containsString(StringUtils.stringFromResource(textId))))) + + fun forViewWithId(@IdRes id: Int, timeout: Long = 10_000L): ViewInteraction = + waitUntilViewAppears(onView(withId(id)), timeout) + + fun forViewWithIdAndText(@IdRes id: Int, text: String): ViewInteraction = + waitUntilViewAppears(onView(allOf(withId(id), withText(text)))) + + fun forViewWithIdAndText(@IdRes id: Int, textId: Int, timeout: Long = 5000): ViewInteraction = + waitUntilViewAppears(onView(allOf(withId(id), withText(textId))), timeout) + + fun forViewWithIdAndParentId(@IdRes id: Int, @IdRes parentId: Int): ViewInteraction = + waitUntilViewAppears(onView(allOf(withId(id), withParent(withId(parentId))))) + + fun untilViewWithIdDisabled(@IdRes id: Int): ViewInteraction = + waitUntilMatcherFulfilled(onView(withId(id)), matches(isEnabled())) + + fun untilViewWithIdIsGone(@IdRes id: Int): ViewInteraction = + waitUntilViewIsGone(onView(withId(id))) + + fun forViewOfInstanceWithParentId(@IdRes id: Int, clazz: Class<*>, timeout: Long = 5000): ViewInteraction = + waitUntilViewAppears(onView(allOf(instanceOf(clazz), withParent(withId(id)))), timeout) + + fun forViewWithText(@StringRes textId: Int): ViewInteraction = + waitUntilViewAppears(onView(withText(StringUtils.stringFromResource(textId)))) + + fun forViewWithText(text: String): ViewInteraction = + waitUntilViewAppears(onView(withText(text))) + + fun forViewWithTextAndParentId(@StringRes text: Int, @IdRes parentId: Int): ViewInteraction = + waitUntilViewAppears(onView(allOf(withText(text), withParent(withId(parentId))))) + + fun forViewWithTextAndParentId(text: String, @IdRes parentId: Int): ViewInteraction = + waitUntilViewAppears(onView(allOf(withText(text), withParent(withId(parentId))))) + + fun untilViewWithIdEnabled(@IdRes id: Int): ViewInteraction = + waitUntilMatcherFulfilled(onView(withId(id)), matches(isEnabled())) + + fun untilViewByViewInteractionIsGone(interaction: ViewInteraction): ViewInteraction = + waitUntilViewIsGone(interaction) + + fun untilViewWithTextIsGone(@StringRes textId: Int): ViewInteraction = + waitUntilViewIsGone(onView(withText(StringUtils.stringFromResource(textId)))) + + fun untilViewWithTextIsGone(text: String): ViewInteraction = + waitUntilViewIsGone(onView(withText(text))) + + fun forViewWithIdAndAncestorId(@IdRes id: Int, @IdRes parentId: Int): ViewInteraction = + waitUntilViewAppears(onView(allOf(withId(id), ViewMatchers.isDescendantOfA(withId(parentId))))) + + fun untilViewWithIdIsNotShown(@IdRes id: Int): ViewInteraction = + UICustomViewActions.waitUntilViewIsNotDisplayed(onView(withId(id))) + + fun forIntent(matcher: Matcher) = UICustomViewActions.waitUntilIntentMatcherFulfilled(matcher) +} From c484a5e1a116d2e628e539bb21bc19aa0044ad64 Mon Sep 17 00:00:00 2001 From: Dimitar Solev Date: Mon, 18 Jan 2021 11:53:44 +0100 Subject: [PATCH 070/145] Use a blocking api call for refreshing tokens. Affected: Token refresh logic. MAILAND-1364 --- .../android/api/ProtonMailApiManager.kt | 5 +++++ .../api/interceptors/ProtonMailAuthenticator.kt | 12 ++---------- .../segments/authentication/AuthenticationApi.kt | 4 ++++ .../authentication/AuthenticationApiSpec.kt | 3 +++ .../authentication/AuthenticationPubService.kt | 7 +++++++ .../interceptors/ProtonMailAuthenticatorTest.kt | 16 ++++++++-------- 6 files changed, 29 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/api/ProtonMailApiManager.kt b/app/src/main/java/ch/protonmail/android/api/ProtonMailApiManager.kt index 6c5b5b029..38517c9e1 100644 --- a/app/src/main/java/ch/protonmail/android/api/ProtonMailApiManager.kt +++ b/app/src/main/java/ch/protonmail/android/api/ProtonMailApiManager.kt @@ -200,6 +200,11 @@ class ProtonMailApiManager @Inject constructor(var api: ProtonMailApi) : retrofitTag: RetrofitTag? ): RefreshResponse = api.refreshAuth(refreshBody, retrofitTag) + override fun refreshAuthBlocking( + refreshBody: RefreshBody, + retrofitTag: RetrofitTag? + ): RefreshResponse = api.refreshAuthBlocking(refreshBody, retrofitTag) + override fun twoFactor(twoFABody: TwoFABody): TwoFAResponse = api.twoFactor(twoFABody) override suspend fun pingAsync(): ResponseBody = api.pingAsync() diff --git a/app/src/main/java/ch/protonmail/android/api/interceptors/ProtonMailAuthenticator.kt b/app/src/main/java/ch/protonmail/android/api/interceptors/ProtonMailAuthenticator.kt index acd9f53e3..7fdb8ca49 100644 --- a/app/src/main/java/ch/protonmail/android/api/interceptors/ProtonMailAuthenticator.kt +++ b/app/src/main/java/ch/protonmail/android/api/interceptors/ProtonMailAuthenticator.kt @@ -33,7 +33,6 @@ import ch.protonmail.android.core.UserManager import ch.protonmail.android.utils.AppUtil import com.birbit.android.jobqueue.JobManager import com.birbit.android.jobqueue.TagConstraint -import kotlinx.coroutines.runBlocking import okhttp3.Authenticator import okhttp3.Request import okhttp3.Response @@ -85,10 +84,8 @@ class ProtonMailAuthenticator @Inject constructor( if (tokenManager != null && !originalRequest.url().encodedPath().contains(REFRESH_PATH)) { val refreshBody = tokenManager.createRefreshBody() - val refreshResponse = runBlocking { - ProtonMailApplication.getApplication().api - .refreshAuth(refreshBody, RetrofitTag(usernameAuth)) - } + val refreshResponse = + ProtonMailApplication.getApplication().api.refreshAuthBlocking(refreshBody, RetrofitTag(usernameAuth)) if (refreshResponse.error.isNullOrEmpty() && refreshResponse.accessToken != null) { Timber.i( "access token expired: got correct refresh response, handle refresh in token manager" @@ -155,9 +152,4 @@ class ProtonMailAuthenticator @Inject constructor( .header(HEADER_LOCALE, ProtonMailApplication.getApplication().currentLocale) .build() } - - // stub for migrating header application from BaseRequestInterceptor.kt to here - private fun applyHeadersToRequest(tokenManager: TokenManager, originalRequest: Request): Request? { - return null - } } diff --git a/app/src/main/java/ch/protonmail/android/api/segments/authentication/AuthenticationApi.kt b/app/src/main/java/ch/protonmail/android/api/segments/authentication/AuthenticationApi.kt index ccc937f5e..d2d2c3a68 100644 --- a/app/src/main/java/ch/protonmail/android/api/segments/authentication/AuthenticationApi.kt +++ b/app/src/main/java/ch/protonmail/android/api/segments/authentication/AuthenticationApi.kt @@ -78,4 +78,8 @@ class AuthenticationApi( override suspend fun refreshAuth(refreshBody: RefreshBody, retrofitTag: RetrofitTag?): RefreshResponse = pubService.refreshAuth(refreshBody, retrofitTag) + @Throws(IOException::class) + override fun refreshAuthBlocking(refreshBody: RefreshBody, retrofitTag: RetrofitTag?): RefreshResponse = + ParseUtils.parse(pubService.refreshAuthBlocking(refreshBody, retrofitTag).execute()) + } diff --git a/app/src/main/java/ch/protonmail/android/api/segments/authentication/AuthenticationApiSpec.kt b/app/src/main/java/ch/protonmail/android/api/segments/authentication/AuthenticationApiSpec.kt index 1b4ca8cc0..f0f35c211 100644 --- a/app/src/main/java/ch/protonmail/android/api/segments/authentication/AuthenticationApiSpec.kt +++ b/app/src/main/java/ch/protonmail/android/api/segments/authentication/AuthenticationApiSpec.kt @@ -54,5 +54,8 @@ interface AuthenticationApiSpec { suspend fun refreshAuth(refreshBody: RefreshBody, retrofitTag: RetrofitTag?): RefreshResponse + @Throws(IOException::class) + fun refreshAuthBlocking(refreshBody: RefreshBody, retrofitTag: RetrofitTag?): RefreshResponse + fun twoFactor(twoFABody: TwoFABody): TwoFAResponse } diff --git a/app/src/main/java/ch/protonmail/android/api/segments/authentication/AuthenticationPubService.kt b/app/src/main/java/ch/protonmail/android/api/segments/authentication/AuthenticationPubService.kt index b9264b0e9..15f6d46b9 100644 --- a/app/src/main/java/ch/protonmail/android/api/segments/authentication/AuthenticationPubService.kt +++ b/app/src/main/java/ch/protonmail/android/api/segments/authentication/AuthenticationPubService.kt @@ -48,4 +48,11 @@ interface AuthenticationPubService { @Body refreshBody: RefreshBody, @Tag retrofitTag: RetrofitTag? = null ): RefreshResponse + + @POST("auth/refresh") + @Headers(RetrofitConstants.CONTENT_TYPE, RetrofitConstants.ACCEPT_HEADER_V1) + fun refreshAuthBlocking( + @Body refreshBody: RefreshBody, + @Tag retrofitTag: RetrofitTag? = null + ): Call } diff --git a/app/src/test/java/ch/protonmail/android/api/interceptors/ProtonMailAuthenticatorTest.kt b/app/src/test/java/ch/protonmail/android/api/interceptors/ProtonMailAuthenticatorTest.kt index cd4c48b87..7b29fe19c 100644 --- a/app/src/test/java/ch/protonmail/android/api/interceptors/ProtonMailAuthenticatorTest.kt +++ b/app/src/test/java/ch/protonmail/android/api/interceptors/ProtonMailAuthenticatorTest.kt @@ -146,8 +146,8 @@ class ProtonMailAuthenticatorTest { every { error } answers { NON_NULL_ERROR_MESSAGE } } - coEvery { - ProtonMailApplication.getApplication().api.refreshAuth(any(), any()) + every { + ProtonMailApplication.getApplication().api.refreshAuthBlocking(any(), any()) } returns authResponseMock // when @@ -176,8 +176,8 @@ class ProtonMailAuthenticatorTest { every { error } answers { NON_NULL_ERROR_MESSAGE } } - coEvery { - ProtonMailApplication.getApplication().api.refreshAuth(any(), any()) + every { + ProtonMailApplication.getApplication().api.refreshAuthBlocking(any(), any()) } returns authResponseMock // when @@ -206,8 +206,8 @@ class ProtonMailAuthenticatorTest { every { error } answers { NON_NULL_ERROR_MESSAGE } } - coEvery { - ProtonMailApplication.getApplication().api.refreshAuth(any(), any()) + every { + ProtonMailApplication.getApplication().api.refreshAuthBlocking(any(), any()) } returns authResponseMock // when @@ -238,8 +238,8 @@ class ProtonMailAuthenticatorTest { every { accessToken } answers { "correct_access_token" } } - coEvery { - ProtonMailApplication.getApplication().api.refreshAuth(any(), any()) + every { + ProtonMailApplication.getApplication().api.refreshAuthBlocking(any(), any()) } returns authResponseMock // when From b23b60eddfaa6b315efd088b5b41d6735311dd53 Mon Sep 17 00:00:00 2001 From: stefanija Date: Mon, 18 Jan 2021 12:06:55 +0100 Subject: [PATCH 071/145] Change build version code #comment Increased the build version code for a new beta Affected: nothing --- buildSrc/src/main/kotlin/ProtonMail.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/ProtonMail.kt b/buildSrc/src/main/kotlin/ProtonMail.kt index 5ff8ee782..b98a6d6c5 100644 --- a/buildSrc/src/main/kotlin/ProtonMail.kt +++ b/buildSrc/src/main/kotlin/ProtonMail.kt @@ -23,7 +23,7 @@ */ object ProtonMail { const val versionName = "1.13.25" - const val versionCode = 752 + const val versionCode = 753 const val targetSdk = 30 const val minSdk = 21 From ad4efbca72a337db5baf48107cdc3ae4e0c92eac Mon Sep 17 00:00:00 2001 From: stefanija Date: Wed, 20 Jan 2021 17:35:22 +0100 Subject: [PATCH 072/145] Revert "Replace FcmIntentService with ProcessPushNotificationDataWorker" This reverts commit ccd05619 --- .../protonmail/android/crypto/UserCrypto.kt | 5 +- .../android/di/ApplicationModule.kt | 10 - .../android/fcm/FcmIntentService.java | 327 +++++++++++++ .../android/fcm/PMFirebaseMessagingService.kt | 8 +- .../fcm/ProcessPushNotificationDataWorker.kt | 243 --------- ...ificationSender.kt => NotificationData.kt} | 14 +- .../fcm/models/NotificationEncryptedData.kt | 35 ++ ...hNotification.kt => NotificationSender.kt} | 13 +- .../fcm/models/PushNotificationData.kt | 37 -- .../ProcessPushNotificationDataWorkerTest.kt | 463 ------------------ 10 files changed, 380 insertions(+), 775 deletions(-) create mode 100644 app/src/main/java/ch/protonmail/android/fcm/FcmIntentService.java delete mode 100644 app/src/main/java/ch/protonmail/android/fcm/ProcessPushNotificationDataWorker.kt rename app/src/main/java/ch/protonmail/android/fcm/models/{PushNotificationSender.kt => NotificationData.kt} (73%) create mode 100644 app/src/main/java/ch/protonmail/android/fcm/models/NotificationEncryptedData.kt rename app/src/main/java/ch/protonmail/android/fcm/models/{PushNotification.kt => NotificationSender.kt} (74%) delete mode 100644 app/src/main/java/ch/protonmail/android/fcm/models/PushNotificationData.kt delete mode 100644 app/src/test/java/ch/protonmail/android/fcm/ProcessPushNotificationDataWorkerTest.kt diff --git a/app/src/main/java/ch/protonmail/android/crypto/UserCrypto.kt b/app/src/main/java/ch/protonmail/android/crypto/UserCrypto.kt index 0ff09f153..04ae994ee 100644 --- a/app/src/main/java/ch/protonmail/android/crypto/UserCrypto.kt +++ b/app/src/main/java/ch/protonmail/android/crypto/UserCrypto.kt @@ -60,14 +60,13 @@ class UserCrypto( override fun decrypt(message: CipherText): TextDecryptionResult = decrypt(message, getVerificationKeys(), openPgp.time) - fun decryptMessage(message: String): TextDecryptionResult { + fun decryptMessage(message: CipherText): TextDecryptionResult { val errorMessage = "Error decrypting message, invalid passphrase" checkNotNull(mailboxPassword) { errorMessage } return withCurrentKeys("Error decrypting message") { key -> - val cipherText = CipherText(message) val unarmored = Armor.unarmor(key.privateKey.string) - val decrypted = openPgp.decryptMessageBinKey(cipherText.armored, unarmored, mailboxPassword) + val decrypted = openPgp.decryptMessageBinKey(message.armored, unarmored, mailboxPassword) TextDecryptionResult(decrypted, false, false) } } diff --git a/app/src/main/java/ch/protonmail/android/di/ApplicationModule.kt b/app/src/main/java/ch/protonmail/android/di/ApplicationModule.kt index 3418da85c..f6960f289 100644 --- a/app/src/main/java/ch/protonmail/android/di/ApplicationModule.kt +++ b/app/src/main/java/ch/protonmail/android/di/ApplicationModule.kt @@ -19,7 +19,6 @@ package ch.protonmail.android.di -import android.app.NotificationManager import android.content.Context import android.content.SharedPreferences import android.net.ConnectivityManager @@ -39,7 +38,6 @@ import ch.protonmail.android.api.models.messages.receive.IMessageSenderFactory import ch.protonmail.android.api.models.messages.receive.MessageSenderFactory import ch.protonmail.android.api.models.messages.receive.ServerLabel import ch.protonmail.android.api.models.room.contacts.ContactLabel -import ch.protonmail.android.api.segments.event.AlarmReceiver import ch.protonmail.android.attachments.Armorer import ch.protonmail.android.attachments.OpenPgpArmorer import ch.protonmail.android.core.Constants @@ -87,9 +85,6 @@ object ApplicationModule { fun protonMailApplication(context: Context): ProtonMailApplication = context.app - @Provides - fun alarmReceiver() = AlarmReceiver() - @Provides @AlternativeApiPins fun alternativeApiPins() = listOf( @@ -162,11 +157,6 @@ object ApplicationModule { userManager: UserManager ) = userManager.mailSettings - @Provides - @Singleton - fun notificationManager(context: Context): NotificationManager = - context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - @Provides @Singleton fun protonRetrofitBuilder( diff --git a/app/src/main/java/ch/protonmail/android/fcm/FcmIntentService.java b/app/src/main/java/ch/protonmail/android/fcm/FcmIntentService.java new file mode 100644 index 000000000..209b7e9d3 --- /dev/null +++ b/app/src/main/java/ch/protonmail/android/fcm/FcmIntentService.java @@ -0,0 +1,327 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * + * This file is part of ProtonMail. + * + * ProtonMail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonMail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonMail. If not, see https://www.gnu.org/licenses/. + */ +package ch.protonmail.android.fcm; + +import android.app.IntentService; +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.text.TextUtils; + +import androidx.annotation.Nullable; + +import com.google.gson.Gson; + +import java.util.Calendar; +import java.util.List; + +import javax.inject.Inject; + +import ch.protonmail.android.BuildConfig; +import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository; +import ch.protonmail.android.api.ProtonMailApiManager; +import ch.protonmail.android.api.local.SnoozeSettings; +import ch.protonmail.android.api.models.DatabaseProvider; +import ch.protonmail.android.api.models.User; +import ch.protonmail.android.api.models.messages.receive.MessageResponse; +import ch.protonmail.android.api.models.messages.receive.MessagesResponse; +import ch.protonmail.android.api.models.room.messages.Message; +import ch.protonmail.android.api.models.room.notifications.Notification; +import ch.protonmail.android.api.models.room.notifications.NotificationsDatabase; +import ch.protonmail.android.api.segments.event.AlarmReceiver; +import ch.protonmail.android.core.QueueNetworkUtil; +import ch.protonmail.android.core.UserManager; +import ch.protonmail.android.crypto.CipherText; +import ch.protonmail.android.crypto.Crypto; +import ch.protonmail.android.crypto.UserCrypto; +import ch.protonmail.android.fcm.models.NotificationData; +import ch.protonmail.android.fcm.models.NotificationEncryptedData; +import ch.protonmail.android.fcm.models.NotificationSender; +import ch.protonmail.android.servers.notification.INotificationServer; +import ch.protonmail.android.servers.notification.NotificationServer; +import ch.protonmail.android.utils.AppUtil; +import ch.protonmail.android.utils.Logger; +import ch.protonmail.android.utils.crypto.TextDecryptionResult; +import dagger.hilt.android.AndroidEntryPoint; +import io.sentry.event.EventBuilder; +import timber.log.Timber; + +@AndroidEntryPoint +public class FcmIntentService extends IntentService { + + private static final String TAG_FCM_INTENT_SERVICE = "FcmIntentService"; + + public static final String EXTRA_READ = "CMD_READ"; + private static final String EXTRA_ENCRYPTED_DATA = "encryptedMessage"; + private static final String EXTRA_UID = "UID"; + + @Inject + ProtonMailApiManager mApi; + @Inject + UserManager mUserManager; + @Inject + QueueNetworkUtil mNetworkUtils; + @Inject + MessageDetailsRepository messageDetailsRepository; + @Inject + DatabaseProvider databaseProvider; + + private NotificationsDatabase notificationsDatabase; + private INotificationServer notificationServer; + + public FcmIntentService() { + super("FCM"); + setIntentRedelivery(true); + } + + @Override + public void onCreate() { + super.onCreate(); + NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + notificationServer = new NotificationServer(this, notificationManager); + } + + private void startMeForeground() { + final int messageId = (int) System.currentTimeMillis(); + final android.app.Notification notification = notificationServer.createCheckingMailboxNotification(); + startForeground(messageId, notification); + } + + @Override + protected void onHandleIntent(Intent intent) { + startMeForeground(); + + final Bundle extras = intent.getExtras(); + if (extras != null && !extras.isEmpty()) { + if (!extras.containsKey("CMD")) { + // we are always registering for push in MailboxActivity + + boolean isAppInBackground = AppUtil.isAppInBackground(); + if (!isAppInBackground) { + AlarmReceiver alarmReceiver = new AlarmReceiver(); + alarmReceiver.setAlarm(this, true); + } + + mNetworkUtils.setCurrentlyHasConnectivity(); + NotificationData notificationData = null; + NotificationEncryptedData messageData = null; + String sessionId = ""; + + if (extras.containsKey(EXTRA_UID)) { + sessionId = extras.getString(EXTRA_UID, ""); + } + + String notificationUsername = mUserManager.getUsernameBySessionId(sessionId); + if (TextUtils.isEmpty(notificationUsername)) { + // we do not show notifications for unknown/inactive users + return; + } + + User user = mUserManager.getUser(notificationUsername); + notificationsDatabase = databaseProvider.provideNotificationsDao(notificationUsername); + if (!user.isBackgroundSync()) { + return; + } + + try { + if (extras.containsKey(EXTRA_ENCRYPTED_DATA)) { + String encryptedStr = extras.getString(EXTRA_ENCRYPTED_DATA); + UserCrypto crypto = Crypto.forUser(mUserManager, notificationUsername); + TextDecryptionResult textDecryptionResult = crypto.decryptMessage(new CipherText(encryptedStr)); + String decryptedStr = textDecryptionResult.getDecryptedData(); + notificationData = tryParseNotificationModel(decryptedStr); + messageData = notificationData.getData(); + } + } catch (Exception e) { + // can not deliver notification + if (!BuildConfig.DEBUG) { + EventBuilder eventBuilder = new EventBuilder().withTag("FCM_MU", TextUtils.isEmpty(notificationUsername) ? "EMPTY" : "NOT_EMPTY"); + Timber.e(e, eventBuilder.toString()); + } + } + + if (notificationData == null || messageData == null) { + return; + } + + final String messageId = messageData.getMessageId(); + final String notificationBody = messageData.getBody(); + NotificationSender notificationSender = messageData.getSender(); + String sender = notificationSender.getSenderName(); + if (TextUtils.isEmpty(sender)) { + sender = notificationSender.getSenderEmail(); + } + boolean primaryUser = mUserManager.getUsername().equals(notificationUsername); + if (extras.containsKey(EXTRA_READ) && extras.getBoolean(EXTRA_READ)) { + removeNotification(user, messageId, primaryUser); + return; + } + + boolean isQuickSnoozeEnabled = mUserManager.isSnoozeQuickEnabled(); + boolean isScheduledSnoozeEnabled = mUserManager.isSnoozeScheduledEnabled(); + if (!isQuickSnoozeEnabled && (!isScheduledSnoozeEnabled || shouldShowNotificationWhileScheduledSnooze(user))) { + sendNotification(user, messageId, notificationBody, sender, primaryUser); + } + } + } + stopForeground(true); + } + + /** + * Remove the Notification with the given id and eventually recreate a notification with other + * unread Notifications from database + * + * @param user current logged {@link User} + * @param messageId String id of {@link Message} for delete relative {@link Notification} + */ + private void removeNotification(final User user, + final String messageId, + final boolean primaryUser) { + NotificationManager notificationManager = + (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + + // Cancel all the Status Bar Notifications + notificationManager.cancelAll(); + + // Remove the Notification from Database + notificationsDatabase.deleteByMessageId(messageId); + List notifications = notificationsDatabase.findAllNotifications(); + + // Return if there are no more unreadNotifications + if (notifications.isEmpty()) return; + + Message message = fetchMessage(user, messageId); + if (notifications.size() > 1) { + notificationServer.notifyMultipleUnreadEmail(mUserManager, user, notifications); + } else { + Notification notification = notifications.get(0); + notificationServer.notifySingleNewEmail( + mUserManager, user, message, messageId, + notification.getNotificationBody(), + notification.getNotificationTitle(), primaryUser + ); + } + } + + /** + * Show a Notification for a new email received. + * + * @param user current logged {@link User} + * @param messageId String id for retrieve the {@link Message} details + * @param notificationBody String body of the Notification + * @param sender String name of the sender of the email + */ + private void sendNotification( + final User user, + final String messageId, + @Nullable final String notificationBody, + final String sender, + final boolean primaryUser + ) { + + // Insert current Notification in Database + Notification notification = new Notification(messageId, sender, notificationBody != null ? notificationBody : ""); + notificationsDatabase.insertNotification(notification); + + List notifications = notificationsDatabase.findAllNotifications(); + + Message message = fetchMessage(user, messageId); + if (notifications.size() > 1) { + notificationServer.notifyMultipleUnreadEmail(mUserManager, user, notifications); + } else { + notificationServer.notifySingleNewEmail( + mUserManager, user, message, messageId, notificationBody, sender, primaryUser + ); + } + } + + private Message fetchMessage(final User user, final String messageId) { + // Fetch message details if required by the current config + boolean fetchMessageDetails = user.isGcmDownloadMessageDetails(); + Message message; + if (fetchMessageDetails) { + message = fetchMessageDetails(messageId); + } else { + message = fetchMessageMetadata(messageId); + } + if (message == null) { + // try to find the message in the local storage, maybe it was received from the event + message = messageDetailsRepository.findMessageById(messageId); + } + + return message; + } + + private Message fetchMessageMetadata(final String messageId) { + Message message = null; + try { + MessagesResponse messageResponse = mApi.fetchSingleMessageMetadata(messageId); + if (messageResponse != null) { + List messages = messageResponse.getMessages(); + if (messages.size() > 0) { + message = messages.get(0); + } + if (message != null) { + Message savedMessage = messageDetailsRepository.findMessageById(message.getMessageId()); + if (savedMessage != null) { + message.setInline(savedMessage.isInline()); + } + message.setDownloaded(false); + messageDetailsRepository.saveMessageInDB(message); + } else { + // check if the message is already in local store + message = messageDetailsRepository.findMessageById(messageId); + } + } + } catch (Exception error) { + Logger.doLogException(TAG_FCM_INTENT_SERVICE, "error while fetching message detail", error); + } + return message; + } + + private Message fetchMessageDetails(final String messageId) { + Message message = null; + try { + MessageResponse messageResponse = mApi.messageDetail(messageId); + message = messageResponse.getMessage(); + Message savedMessage = messageDetailsRepository.findMessageById(messageId); + if (savedMessage != null) { + message.setInline(savedMessage.isInline()); + } + message.setDownloaded(true); + messageDetailsRepository.saveMessageInDB(message); + } catch (Exception error) { + Logger.doLogException(TAG_FCM_INTENT_SERVICE, "error while fetching message detail", error); + } + + return message; + } + + private boolean shouldShowNotificationWhileScheduledSnooze(User user) { + Calendar rightNow = Calendar.getInstance(); + SnoozeSettings snoozeSettings = mUserManager.getSnoozeSettings(); + return !snoozeSettings.shouldSuppressNotification(rightNow); + } + + private NotificationData tryParseNotificationModel(String decryptedStr) { + Gson gson = new Gson(); + return gson.fromJson(decryptedStr, NotificationData.class); + } +} diff --git a/app/src/main/java/ch/protonmail/android/fcm/PMFirebaseMessagingService.kt b/app/src/main/java/ch/protonmail/android/fcm/PMFirebaseMessagingService.kt index 45f88e57f..7a327f890 100644 --- a/app/src/main/java/ch/protonmail/android/fcm/PMFirebaseMessagingService.kt +++ b/app/src/main/java/ch/protonmail/android/fcm/PMFirebaseMessagingService.kt @@ -19,8 +19,10 @@ package ch.protonmail.android.fcm +import android.content.ComponentName import android.content.Intent import android.os.Bundle +import androidx.core.content.ContextCompat import androidx.localbroadcastmanager.content.LocalBroadcastManager import ch.protonmail.android.R import com.google.firebase.messaging.FirebaseMessagingService @@ -37,8 +39,6 @@ public class PMFirebaseMessagingService : FirebaseMessagingService() { @Inject lateinit var pmRegistrationWorkerEnqueuer: PMRegistrationWorker.Enqueuer - @Inject - lateinit var processPushNotificationData: ProcessPushNotificationDataWorker.Enqueuer override fun onNewToken(token: String) { super.onNewToken(token) @@ -60,7 +60,9 @@ public class PMFirebaseMessagingService : FirebaseMessagingService() { broadcastIntent.putExtras(bundle) broadcastIntent.action = baseContext.getString(R.string.action_notification) if (!LocalBroadcastManager.getInstance(baseContext).sendBroadcast(broadcastIntent)) { - processPushNotificationData(remoteMessage.data) + val serviceIntent = Intent(broadcastIntent) + val comp = ComponentName(baseContext.packageName, FcmIntentService::class.java.name) + ContextCompat.startForegroundService(baseContext, serviceIntent.setComponent(comp)) } } } diff --git a/app/src/main/java/ch/protonmail/android/fcm/ProcessPushNotificationDataWorker.kt b/app/src/main/java/ch/protonmail/android/fcm/ProcessPushNotificationDataWorker.kt deleted file mode 100644 index dd02affd2..000000000 --- a/app/src/main/java/ch/protonmail/android/fcm/ProcessPushNotificationDataWorker.kt +++ /dev/null @@ -1,243 +0,0 @@ -/* - * Copyright (c) 2020 Proton Technologies AG - * - * This file is part of ProtonMail. - * - * ProtonMail is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ProtonMail is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ProtonMail. If not, see https://www.gnu.org/licenses/. - */ - -package ch.protonmail.android.fcm - -import android.content.Context -import androidx.hilt.Assisted -import androidx.hilt.work.WorkerInject -import androidx.work.Constraints -import androidx.work.CoroutineWorker -import androidx.work.Data -import androidx.work.NetworkType -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.WorkManager -import androidx.work.WorkerParameters -import androidx.work.workDataOf -import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository -import ch.protonmail.android.api.ProtonMailApiManager -import ch.protonmail.android.api.models.DatabaseProvider -import ch.protonmail.android.api.models.User -import ch.protonmail.android.api.models.messages.receive.MessageResponse -import ch.protonmail.android.api.models.room.messages.Message -import ch.protonmail.android.api.models.room.notifications.Notification -import ch.protonmail.android.api.segments.event.AlarmReceiver -import ch.protonmail.android.core.QueueNetworkUtil -import ch.protonmail.android.core.UserManager -import ch.protonmail.android.crypto.UserCrypto -import ch.protonmail.android.domain.entity.Name -import ch.protonmail.android.fcm.models.PushNotification -import ch.protonmail.android.fcm.models.PushNotificationData -import ch.protonmail.android.servers.notification.NotificationServer -import ch.protonmail.android.utils.AppUtil -import me.proton.core.util.kotlin.EMPTY_STRING -import me.proton.core.util.kotlin.deserialize -import timber.log.Timber -import java.util.Calendar -import javax.inject.Inject - -const val KEY_PUSH_NOTIFICATION_UID = "UID" -const val KEY_PUSH_NOTIFICATION_ENCRYPTED_MESSAGE = "encryptedMessage" -const val KEY_PROCESS_PUSH_NOTIFICATION_DATA_ERROR = "ProcessPushNotificationDataError" - -/** - * A worker that is responsible for processing the data payload of the received FCM push notifications. - */ - -class ProcessPushNotificationDataWorker @WorkerInject constructor( - @Assisted context: Context, - @Assisted workerParameters: WorkerParameters, - private val notificationServer: NotificationServer, - private val alarmReceiver: AlarmReceiver, - private val queueNetworkUtil: QueueNetworkUtil, - private val userManager: UserManager, - private val databaseProvider: DatabaseProvider, - private val messageDetailsRepository: MessageDetailsRepository, - private val protonMailApiManager: ProtonMailApiManager -) : CoroutineWorker(context, workerParameters) { - - override suspend fun doWork(): Result { - val sessionId = inputData.getString(KEY_PUSH_NOTIFICATION_UID) - val encryptedMessage = inputData.getString(KEY_PUSH_NOTIFICATION_ENCRYPTED_MESSAGE) - - if (sessionId.isNullOrEmpty() || encryptedMessage.isNullOrEmpty()) { - return Result.failure( - workDataOf(KEY_PROCESS_PUSH_NOTIFICATION_DATA_ERROR to "Input data is missing") - ) - } - - if (!AppUtil.isAppInBackground()) { - alarmReceiver.setAlarm(applicationContext, true) - } - - queueNetworkUtil.setCurrentlyHasConnectivity() - - val notificationUsername = userManager.getUsernameBySessionId(sessionId) - if (notificationUsername.isNullOrEmpty()) { - // we do not show notifications for unknown/inactive users - return Result.failure(workDataOf(KEY_PROCESS_PUSH_NOTIFICATION_DATA_ERROR to "User is unknown or inactive")) - } - - val user = userManager.getUser(notificationUsername) - if (!user.isBackgroundSync) { - // we do not show notifications for users who have disabled background sync - return Result.failure(workDataOf(KEY_PROCESS_PUSH_NOTIFICATION_DATA_ERROR to "Background sync is disabled")) - } - - var pushNotification: PushNotification? = null - var pushNotificationData: PushNotificationData? = null - try { - val userCrypto = UserCrypto(userManager, userManager.openPgp, Name(notificationUsername)) - val textDecryptionResult = userCrypto.decryptMessage(encryptedMessage) - val decryptedData = textDecryptionResult.decryptedData - pushNotification = decryptedData.deserialize(PushNotification.serializer()) - pushNotificationData = pushNotification.data - } catch (e: Exception) { - Timber.e(e, "Error with decryption or deserialization of the notification data") - } - - if (pushNotification == null || pushNotificationData == null) { - return Result.failure( - workDataOf(KEY_PROCESS_PUSH_NOTIFICATION_DATA_ERROR to "Decryption or deserialization error") - ) - } - - val messageId = pushNotificationData.messageId - val notificationBody = pushNotificationData.body - val notificationSender = pushNotificationData.sender - val sender = notificationSender?.let { - it.senderName.ifEmpty { it.senderAddress } - } ?: EMPTY_STRING - - val primaryUser = userManager.username == notificationUsername - val isQuickSnoozeEnabled = userManager.isSnoozeQuickEnabled() - val isScheduledSnoozeEnabled = userManager.isSnoozeScheduledEnabled() - - if (!isQuickSnoozeEnabled && (!isScheduledSnoozeEnabled || !shouldSuppressNotification())) { - sendNotification(user, messageId, notificationBody, sender, primaryUser) - } - - return Result.success() - } - - private fun sendNotification( - user: User, - messageId: String, - notificationBody: String, - sender: String, - primaryUser: Boolean - ) { - - // Insert current Notification in Database - val notificationsDatabase = databaseProvider.provideNotificationsDao(user.username) - val notification = Notification(messageId, sender, notificationBody) - val notifications = notificationsDatabase.insertNewNotificationAndReturnAll(notification) - val message = fetchMessage(user, messageId) - - if (notifications.size > 1) { - notificationServer.notifyMultipleUnreadEmail(userManager, user, notifications) - } else { - notificationServer.notifySingleNewEmail( - userManager, user, message, messageId, notificationBody, sender, primaryUser - ) - } - } - - private fun fetchMessage(user: User, messageId: String): Message? { - // Fetch message details if required by the current config - val fetchMessageDetails = user.isGcmDownloadMessageDetails - return if (fetchMessageDetails) { - fetchMessageDetails(messageId) - } else { - fetchMessageMetadata(messageId) - } - ?: messageDetailsRepository.findMessageByIdBlocking(messageId) - } - - private fun fetchMessageMetadata(messageId: String): Message? { - var message: Message? = null - try { - val messageResponse = protonMailApiManager.fetchSingleMessageMetadata(messageId) - if (messageResponse != null) { - val messages = messageResponse.messages - if (messages.isNotEmpty()) { - message = messages[0] - } - if (message != null) { - val savedMessage = messageDetailsRepository.findMessageByIdBlocking(message.messageId!!) - if (savedMessage != null) { - message.isInline = savedMessage.isInline - } - message.isDownloaded = false - messageDetailsRepository.saveMessageInDB(message) - } else { - // check if the message is already in local store - message = messageDetailsRepository.findMessageByIdBlocking(messageId) - } - } - } catch (error: Exception) { - Timber.e(error, "error while fetching message metadata in ProcessPushNotificationDataWorker") - } - return message - } - - private fun fetchMessageDetails(messageId: String): Message? { - var message: Message? = null - try { - val messageResponse: MessageResponse = protonMailApiManager.messageDetail(messageId) - message = messageResponse.message - val savedMessage = messageDetailsRepository.findMessageByIdBlocking(messageId) - if (savedMessage != null) { - message.isInline = savedMessage.isInline - } - message.isDownloaded = true - messageDetailsRepository.saveMessageInDB(message) - } catch (error: Exception) { - Timber.e(error, "error while fetching message detail in ProcessPushNotificationDataWorker") - } - return message - } - - private fun shouldSuppressNotification(): Boolean { - val rightNow = Calendar.getInstance() - return userManager.snoozeSettings?.shouldSuppressNotification(rightNow) ?: false - } - - class Enqueuer @Inject constructor( - private val workManager: WorkManager - ) { - - operator fun invoke(pushNotificationData: Map) { - val constraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() - - val inputData = Data.Builder() - .putAll(pushNotificationData) - .build() - - val workRequest = OneTimeWorkRequestBuilder() - .setConstraints(constraints) - .setInputData(inputData) - .build() - - workManager.enqueue(workRequest) - } - } -} diff --git a/app/src/main/java/ch/protonmail/android/fcm/models/PushNotificationSender.kt b/app/src/main/java/ch/protonmail/android/fcm/models/NotificationData.kt similarity index 73% rename from app/src/main/java/ch/protonmail/android/fcm/models/PushNotificationSender.kt rename to app/src/main/java/ch/protonmail/android/fcm/models/NotificationData.kt index 07b11b60d..708aa3e59 100644 --- a/app/src/main/java/ch/protonmail/android/fcm/models/PushNotificationSender.kt +++ b/app/src/main/java/ch/protonmail/android/fcm/models/NotificationData.kt @@ -18,12 +18,10 @@ */ package ch.protonmail.android.fcm.models -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable +import com.google.gson.annotations.SerializedName -@Serializable -data class PushNotificationSender( - @SerialName("Address") val senderAddress: String, - @SerialName("Name") val senderName: String, - @SerialName("Group") val senderGroup: String -) +data class NotificationData( + @SerializedName("type") val type: String? = null, + @SerializedName("version") val version: Int = 0, + @SerializedName("data") val data: NotificationEncryptedData? = null +) \ No newline at end of file diff --git a/app/src/main/java/ch/protonmail/android/fcm/models/NotificationEncryptedData.kt b/app/src/main/java/ch/protonmail/android/fcm/models/NotificationEncryptedData.kt new file mode 100644 index 000000000..485db3cf2 --- /dev/null +++ b/app/src/main/java/ch/protonmail/android/fcm/models/NotificationEncryptedData.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * + * This file is part of ProtonMail. + * + * ProtonMail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonMail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonMail. If not, see https://www.gnu.org/licenses/. + */ +package ch.protonmail.android.fcm.models + +import com.google.gson.annotations.SerializedName + +data class NotificationEncryptedData( + @SerializedName("title") val title: String? = null, + @SerializedName("subtitle") val subtitle: String? = null, + @SerializedName("body") val body: String? = null, + @SerializedName("vibrate") val vibrate: Int = 0, + @SerializedName("sound") val sound: Int = 0, + @SerializedName("largeIcon") val largeIcon: String? = null, + @SerializedName("smallIcon") val smallIcon: String? = null, + @SerializedName("badge") val badge: Int = 0, + @SerializedName("messageId") val messageId: String? = null, + @SerializedName("customId") val customId: String? = null, + @SerializedName("sender") val sender: NotificationSender? = null +) \ No newline at end of file diff --git a/app/src/main/java/ch/protonmail/android/fcm/models/PushNotification.kt b/app/src/main/java/ch/protonmail/android/fcm/models/NotificationSender.kt similarity index 74% rename from app/src/main/java/ch/protonmail/android/fcm/models/PushNotification.kt rename to app/src/main/java/ch/protonmail/android/fcm/models/NotificationSender.kt index 9b27ce959..4ca0ecfac 100644 --- a/app/src/main/java/ch/protonmail/android/fcm/models/PushNotification.kt +++ b/app/src/main/java/ch/protonmail/android/fcm/models/NotificationSender.kt @@ -18,12 +18,9 @@ */ package ch.protonmail.android.fcm.models -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable +import com.google.gson.annotations.SerializedName -@Serializable -data class PushNotification( - @SerialName("type") val type: String, - @SerialName("version") val version: Int, - @SerialName("data") val data: PushNotificationData? -) +data class NotificationSender( + @SerializedName("Address") val senderEmail: String? = null, + @SerializedName("Name") val senderName: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/ch/protonmail/android/fcm/models/PushNotificationData.kt b/app/src/main/java/ch/protonmail/android/fcm/models/PushNotificationData.kt deleted file mode 100644 index 082b5556c..000000000 --- a/app/src/main/java/ch/protonmail/android/fcm/models/PushNotificationData.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2020 Proton Technologies AG - * - * This file is part of ProtonMail. - * - * ProtonMail is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ProtonMail is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ProtonMail. If not, see https://www.gnu.org/licenses/. - */ -package ch.protonmail.android.fcm.models - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class PushNotificationData( - @SerialName("title") val title: String, - @SerialName("subtitle") val subtitle: String, - @SerialName("body") val body: String, - @SerialName("vibrate") val vibrate: Int, - @SerialName("sound") val sound: Int, - @SerialName("largeIcon") val largeIcon: String, - @SerialName("smallIcon") val smallIcon: String, - @SerialName("badge") val badge: Int, - @SerialName("messageId") val messageId: String, - @SerialName("customId") val customId: String, - @SerialName("sender") val sender: PushNotificationSender? -) diff --git a/app/src/test/java/ch/protonmail/android/fcm/ProcessPushNotificationDataWorkerTest.kt b/app/src/test/java/ch/protonmail/android/fcm/ProcessPushNotificationDataWorkerTest.kt deleted file mode 100644 index dbbe22df6..000000000 --- a/app/src/test/java/ch/protonmail/android/fcm/ProcessPushNotificationDataWorkerTest.kt +++ /dev/null @@ -1,463 +0,0 @@ -/* - * Copyright (c) 2020 Proton Technologies AG - * - * This file is part of ProtonMail. - * - * ProtonMail is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ProtonMail is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ProtonMail. If not, see https://www.gnu.org/licenses/. - */ - -package ch.protonmail.android.fcm - -import android.content.Context -import androidx.work.ListenableWorker -import androidx.work.OneTimeWorkRequest -import androidx.work.WorkManager -import androidx.work.WorkerParameters -import androidx.work.workDataOf -import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository -import ch.protonmail.android.api.ProtonMailApiManager -import ch.protonmail.android.api.models.DatabaseProvider -import ch.protonmail.android.api.models.User -import ch.protonmail.android.api.models.room.messages.Message -import ch.protonmail.android.api.models.room.notifications.Notification -import ch.protonmail.android.api.segments.event.AlarmReceiver -import ch.protonmail.android.core.QueueNetworkUtil -import ch.protonmail.android.core.UserManager -import ch.protonmail.android.crypto.UserCrypto -import ch.protonmail.android.fcm.models.PushNotification -import ch.protonmail.android.fcm.models.PushNotificationData -import ch.protonmail.android.fcm.models.PushNotificationSender -import ch.protonmail.android.servers.notification.NotificationServer -import ch.protonmail.android.utils.AppUtil -import io.mockk.MockKAnnotations -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.impl.annotations.RelaxedMockK -import io.mockk.just -import io.mockk.justRun -import io.mockk.mockk -import io.mockk.mockkConstructor -import io.mockk.mockkStatic -import io.mockk.runs -import io.mockk.slot -import io.mockk.spyk -import io.mockk.unmockkConstructor -import io.mockk.unmockkStatic -import io.mockk.verify -import io.mockk.verifyOrder -import kotlinx.coroutines.runBlocking -import me.proton.core.util.kotlin.deserialize -import org.junit.After -import kotlin.test.BeforeTest -import kotlin.test.Test -import kotlin.test.assertEquals - -/** - * Tests the functionality of [ProcessPushNotificationDataWorker]. - */ - -class ProcessPushNotificationDataWorkerTest { - - @RelaxedMockK - private lateinit var context: Context - @RelaxedMockK - private lateinit var userManager: UserManager - @RelaxedMockK - private lateinit var workerParameters: WorkerParameters - @RelaxedMockK - private lateinit var workManager: WorkManager - - @MockK - private lateinit var alarmReceiver: AlarmReceiver - @MockK - private lateinit var databaseProvider: DatabaseProvider - @MockK - private lateinit var messageDetailsRepository: MessageDetailsRepository - @MockK - private lateinit var notificationServer: NotificationServer - @MockK - private lateinit var protonMailApiManager: ProtonMailApiManager - @MockK - private lateinit var queueNetworkUtil: QueueNetworkUtil - - private lateinit var processPushNotificationDataWorker: ProcessPushNotificationDataWorker - private lateinit var processPushNotificationDataWorkerEnqueuer: ProcessPushNotificationDataWorker.Enqueuer - - @BeforeTest - fun setUp() { - MockKAnnotations.init(this, relaxUnitFun = true) - - mockkConstructor(UserCrypto::class) - mockkStatic(AppUtil::class) - mockkStatic("me.proton.core.util.kotlin.SerializationUtilsKt") - - processPushNotificationDataWorker = spyk( - ProcessPushNotificationDataWorker( - context, - workerParameters, - notificationServer, - alarmReceiver, - queueNetworkUtil, - userManager, - databaseProvider, - messageDetailsRepository, - protonMailApiManager - ), - recordPrivateCalls = true - ) - processPushNotificationDataWorkerEnqueuer = ProcessPushNotificationDataWorker.Enqueuer( - workManager - ) - } - - @After - fun tearDown() { - unmockkConstructor(UserCrypto::class) - unmockkStatic(AppUtil::class) - unmockkStatic("me.proton.core.util.kotlin.SerializationUtilsKt") - } - - @Test - fun verifyWorkIsEnqueuedWhenEnqueuerIsInvoked() { - // given - val mockPushNotificationData = mockk>(relaxed = true) - - // when - processPushNotificationDataWorkerEnqueuer(mockPushNotificationData) - - // then - verify { workManager.enqueue(any()) } - } - - @Test - fun returnFailureIfInputDataIsMissing() { - runBlocking { - // given - every { workerParameters.inputData } returns mockk { - every { getString(KEY_PUSH_NOTIFICATION_UID) } returns "" - every { getString(KEY_PUSH_NOTIFICATION_ENCRYPTED_MESSAGE) } returns null - } - val expectedResult = ListenableWorker.Result.failure( - workDataOf(KEY_PROCESS_PUSH_NOTIFICATION_DATA_ERROR to "Input data is missing") - ) - - // when - val workResult = processPushNotificationDataWorker.doWork() - - // then - assertEquals(expectedResult, workResult) - } - } - - @Test - fun verifyAlarmIsSetIfAppIsNotInBackground() { - runBlocking { - // given - every { workerParameters.inputData } returns mockk { - every { getString(KEY_PUSH_NOTIFICATION_UID) } returns "uid" - every { getString(KEY_PUSH_NOTIFICATION_ENCRYPTED_MESSAGE) } returns "encryptedMessage" - } - every { AppUtil.isAppInBackground() } returns false - - // when - processPushNotificationDataWorker.doWork() - - // then - verify { alarmReceiver.setAlarm(any(), true) } - } - } - - @Test - fun verifySettingHasConnectivityToTrue() { - runBlocking { - // given - every { workerParameters.inputData } returns mockk { - every { getString(KEY_PUSH_NOTIFICATION_UID) } returns "uid" - every { getString(KEY_PUSH_NOTIFICATION_ENCRYPTED_MESSAGE) } returns "encryptedMessage" - } - every { AppUtil.isAppInBackground() } returns false - - // when - processPushNotificationDataWorker.doWork() - - // then - verify { queueNetworkUtil.setCurrentlyHasConnectivity() } - } - } - - @Test - fun returnFailureIfUserIsUnknownOrInactive() { - runBlocking { - // given - every { workerParameters.inputData } returns mockk { - every { getString(KEY_PUSH_NOTIFICATION_UID) } returns "uid" - every { getString(KEY_PUSH_NOTIFICATION_ENCRYPTED_MESSAGE) } returns "encryptedMessage" - } - every { AppUtil.isAppInBackground() } returns false - every { userManager.getUsernameBySessionId("uid") } returns null - - val expectedResult = ListenableWorker.Result.failure( - workDataOf(KEY_PROCESS_PUSH_NOTIFICATION_DATA_ERROR to "User is unknown or inactive") - ) - - // when - val workerResult = processPushNotificationDataWorker.doWork() - - // then - assertEquals(expectedResult, workerResult) - } - } - - @Test - fun returnFailureIfBackgroundSyncIsDisabled() { - runBlocking { - // given - every { workerParameters.inputData } returns mockk { - every { getString(KEY_PUSH_NOTIFICATION_UID) } returns "uid" - every { getString(KEY_PUSH_NOTIFICATION_ENCRYPTED_MESSAGE) } returns "encryptedMessage" - } - every { AppUtil.isAppInBackground() } returns false - every { userManager.getUsernameBySessionId("uid") } returns "username" - every { userManager.getUser("username") } returns mockk { - every { isBackgroundSync } returns false - } - - val expectedResult = ListenableWorker.Result.failure( - workDataOf(KEY_PROCESS_PUSH_NOTIFICATION_DATA_ERROR to "Background sync is disabled") - ) - - // when - val workerResult = processPushNotificationDataWorker.doWork() - - // then - assertEquals(expectedResult, workerResult) - } - } - - @Test - fun returnFailureIfDecryptionFails() { - runBlocking { - // given - every { workerParameters.inputData } returns mockk { - every { getString(KEY_PUSH_NOTIFICATION_UID) } returns "uid" - every { getString(KEY_PUSH_NOTIFICATION_ENCRYPTED_MESSAGE) } returns "encryptedMessage" - } - every { AppUtil.isAppInBackground() } returns false - every { userManager.getUsernameBySessionId("uid") } returns "username" - every { userManager.getUser("username") } returns mockk { - every { isBackgroundSync } returns true - } - every { userManager.openPgp } returns mockk(relaxed = true) - every { anyConstructed().decryptMessage(any()) } throws IllegalStateException() - - val expectedResult = ListenableWorker.Result.failure( - workDataOf(KEY_PROCESS_PUSH_NOTIFICATION_DATA_ERROR to "Decryption or deserialization error") - ) - - // when - val workerResult = processPushNotificationDataWorker.doWork() - - // then - assertEquals(expectedResult, workerResult) - } - } - - @Test - fun returnFailureIfDeserializationFails() { - runBlocking { - // given - every { workerParameters.inputData } returns mockk { - every { getString(KEY_PUSH_NOTIFICATION_UID) } returns "uid" - every { getString(KEY_PUSH_NOTIFICATION_ENCRYPTED_MESSAGE) } returns "encryptedMessage" - } - every { AppUtil.isAppInBackground() } returns false - every { userManager.getUsernameBySessionId("uid") } returns "username" - every { userManager.getUser("username") } returns mockk { - every { isBackgroundSync } returns true - } - every { userManager.openPgp } returns mockk(relaxed = true) - every { anyConstructed().decryptMessage(any()) } returns mockk { - every { decryptedData } returns "decryptedData" - } - every { "decryptedData".deserialize() } returns mockk { - every { data } returns null - } - - val expectedResult = ListenableWorker.Result.failure( - workDataOf(KEY_PROCESS_PUSH_NOTIFICATION_DATA_ERROR to "Decryption or deserialization error") - ) - - // when - val workerResult = processPushNotificationDataWorker.doWork() - - // then - assertEquals(expectedResult, workerResult) - } - } - - private fun mockForCallingSendNotificationSuccessfully(): Pair { - every { workerParameters.inputData } returns mockk { - every { getString(KEY_PUSH_NOTIFICATION_UID) } returns "uid" - every { getString(KEY_PUSH_NOTIFICATION_ENCRYPTED_MESSAGE) } returns "encryptedMessage" - } - every { AppUtil.isAppInBackground() } returns false - every { userManager.getUsernameBySessionId("uid") } returns "username" - every { userManager.getUser("username") } returns mockk { - every { isBackgroundSync } returns true - every { username } returns "username" - } - every { userManager.openPgp } returns mockk(relaxed = true) - every { anyConstructed().decryptMessage(any()) } returns mockk { - every { decryptedData } returns "decryptedData" - } - val mockNotificationSender = mockk { - every { senderName } returns "" - every { senderAddress } returns "senderAddress" - } - val mockNotificationEncryptedData = mockk { - every { messageId } returns "messageId" - every { body } returns "body" - every { sender } returns mockNotificationSender - } - every { "decryptedData".deserialize(any()) } returns mockk { - every { data } returns mockNotificationEncryptedData - } - every { userManager.username } returns "username" - every { userManager.isSnoozeQuickEnabled() } returns false - every { userManager.isSnoozeScheduledEnabled() } returns true - every { processPushNotificationDataWorker invokeNoArgs "shouldSuppressNotification" } returns false - - return Pair(mockNotificationSender, mockNotificationEncryptedData) - } - - @Test - fun verifyCorrectMethodInvocationAfterDecryptionAndDeserializationSucceedsWhenSnoozingNotificationsIsNotActive() { - runBlocking { - // given - val (mockNotificationSender, mockNotificationEncryptedData) = mockForCallingSendNotificationSuccessfully() - - justRun { processPushNotificationDataWorker invoke "sendNotification" withArguments listOf(any(), any(), any(), any(), any()) } - - // when - processPushNotificationDataWorker.doWork() - - // then - verifyOrder { - mockNotificationEncryptedData.messageId - mockNotificationEncryptedData.body - mockNotificationEncryptedData.sender - mockNotificationSender.senderName - mockNotificationSender.senderAddress - userManager.username - userManager.isSnoozeQuickEnabled() - userManager.isSnoozeScheduledEnabled() - processPushNotificationDataWorker invokeNoArgs "shouldSuppressNotification" - } - } - } - - @Test - fun verifyNotifySingleNewEmailIsCalledWithCorrectParametersWhenThereIsOneNotificationInTheDB() { - runBlocking { - // given - mockForCallingSendNotificationSuccessfully() - - val mockNotification = mockk() - every { databaseProvider.provideNotificationsDao("username") } returns mockk(relaxed = true) { - every { insertNewNotificationAndReturnAll(any()) } returns listOf(mockNotification) - } - val mockMessage = mockk() - every { processPushNotificationDataWorker invoke "fetchMessage" withArguments listOf(any(), any()) } returns mockMessage - every { notificationServer.notifySingleNewEmail(any(), any(), any(), any(), any(), any(), any()) } just runs - - // when - processPushNotificationDataWorker.doWork() - - // then - val userManagerSlot = slot() - val userSlot = slot() - val messageSlot = slot() - val messageIdSlot = slot() - val notificationBodySlot = slot() - val senderSlot = slot() - val primaryUserSlot = slot() - verify { - notificationServer.notifySingleNewEmail(capture(userManagerSlot), capture(userSlot), capture(messageSlot), capture(messageIdSlot), capture(notificationBodySlot), capture(senderSlot), capture(primaryUserSlot)) - } - assertEquals(userManager, userManagerSlot.captured) - assertEquals(userManager.getUser("username"), userSlot.captured) - assertEquals(mockMessage, messageSlot.captured) - assertEquals("messageId", messageIdSlot.captured) - assertEquals("body", notificationBodySlot.captured) - assertEquals("senderAddress", senderSlot.captured) - assertEquals(true, primaryUserSlot.captured) - } - } - - @Test - fun verifyNotifyMultipleUnreadEmailIsCalledWithCorrectParametersWhenThereAreMoreThanOneNotificationsInTheDB() { - runBlocking { - // given - mockForCallingSendNotificationSuccessfully() - - val mockNotification1 = mockk() - val mockNotification2 = mockk() - val unreadNotifications = listOf(mockNotification1, mockNotification2) - every { databaseProvider.provideNotificationsDao("username") } returns mockk(relaxed = true) { - every { insertNewNotificationAndReturnAll(any()) } returns unreadNotifications - } - val mockMessage = mockk() - every { processPushNotificationDataWorker invoke "fetchMessage" withArguments listOf(any(), any()) } returns mockMessage - every { notificationServer.notifyMultipleUnreadEmail(any(), any(), any()) } just runs - - // when - processPushNotificationDataWorker.doWork() - - // then - val userManagerSlot = slot() - val userSlot = slot() - val unreadNotificationsSlot = slot>() - verify { - notificationServer.notifyMultipleUnreadEmail(capture(userManagerSlot), capture(userSlot), capture(unreadNotificationsSlot)) - } - assertEquals(userManager, userManagerSlot.captured) - assertEquals(userManager.getUser("username"), userSlot.captured) - assertEquals(unreadNotifications, unreadNotificationsSlot.captured) - } - } - - @Test - fun returnSuccessWhenNotificationWasSent() { - runBlocking { - // given - mockForCallingSendNotificationSuccessfully() - - val mockNotification = mockk() - every { databaseProvider.provideNotificationsDao("username") } returns mockk(relaxed = true) { - every { insertNewNotificationAndReturnAll(any()) } returns listOf(mockNotification) - } - val mockMessage = mockk() - every { processPushNotificationDataWorker invoke "fetchMessage" withArguments listOf(any(), any()) } returns mockMessage - every { notificationServer.notifySingleNewEmail(any(), any(), any(), any(), any(), any(), any()) } just runs - - val expectedResult = ListenableWorker.Result.success() - - // when - val workerResult = processPushNotificationDataWorker.doWork() - - // then - assertEquals(expectedResult, workerResult) - } - } -} From f559c6913ab5377810584f52dfdfa8fe9027f537 Mon Sep 17 00:00:00 2001 From: stefanija Date: Thu, 21 Jan 2021 17:14:53 +0100 Subject: [PATCH 073/145] Fix issues created by reverting "Replace FcmIntentService with ProcessPushNotificationDataWorker" --- .../java/ch/protonmail/android/di/ApplicationModule.kt | 6 ++++++ .../java/ch/protonmail/android/fcm/FcmIntentService.java | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/di/ApplicationModule.kt b/app/src/main/java/ch/protonmail/android/di/ApplicationModule.kt index f6960f289..ad42ee909 100644 --- a/app/src/main/java/ch/protonmail/android/di/ApplicationModule.kt +++ b/app/src/main/java/ch/protonmail/android/di/ApplicationModule.kt @@ -19,6 +19,7 @@ package ch.protonmail.android.di +import android.app.NotificationManager import android.content.Context import android.content.SharedPreferences import android.net.ConnectivityManager @@ -157,6 +158,11 @@ object ApplicationModule { userManager: UserManager ) = userManager.mailSettings + @Provides + @Singleton + fun notificationManager(context: Context): NotificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + @Provides @Singleton fun protonRetrofitBuilder( diff --git a/app/src/main/java/ch/protonmail/android/fcm/FcmIntentService.java b/app/src/main/java/ch/protonmail/android/fcm/FcmIntentService.java index 209b7e9d3..3cce01782 100644 --- a/app/src/main/java/ch/protonmail/android/fcm/FcmIntentService.java +++ b/app/src/main/java/ch/protonmail/android/fcm/FcmIntentService.java @@ -263,7 +263,7 @@ private Message fetchMessage(final User user, final String messageId) { } if (message == null) { // try to find the message in the local storage, maybe it was received from the event - message = messageDetailsRepository.findMessageById(messageId); + message = messageDetailsRepository.findMessageByIdBlocking(messageId); } return message; @@ -279,7 +279,7 @@ private Message fetchMessageMetadata(final String messageId) { message = messages.get(0); } if (message != null) { - Message savedMessage = messageDetailsRepository.findMessageById(message.getMessageId()); + Message savedMessage = messageDetailsRepository.findMessageByIdBlocking(message.getMessageId()); if (savedMessage != null) { message.setInline(savedMessage.isInline()); } @@ -287,7 +287,7 @@ private Message fetchMessageMetadata(final String messageId) { messageDetailsRepository.saveMessageInDB(message); } else { // check if the message is already in local store - message = messageDetailsRepository.findMessageById(messageId); + message = messageDetailsRepository.findMessageByIdBlocking(messageId); } } } catch (Exception error) { @@ -301,7 +301,7 @@ private Message fetchMessageDetails(final String messageId) { try { MessageResponse messageResponse = mApi.messageDetail(messageId); message = messageResponse.getMessage(); - Message savedMessage = messageDetailsRepository.findMessageById(messageId); + Message savedMessage = messageDetailsRepository.findMessageByIdBlocking(messageId); if (savedMessage != null) { message.setInline(savedMessage.isInline()); } From b7d5875a0b9caea3ed9facb046fb35a36a6f93f6 Mon Sep 17 00:00:00 2001 From: Tomasz Giszczak Date: Tue, 19 Jan 2021 16:19:28 +0100 Subject: [PATCH 074/145] Changed error code for invalid deleted attachment in DeleteAttachmentWorker to fix attachemtns removal problem. MAILAND-1359 --- .../android/api/segments/BaseApi.kt | 31 ++++++++++-------- .../api/segments/event/EventHandler.kt | 6 ++-- .../android/worker/DeleteAttachmentWorker.kt | 6 ++-- .../worker/DeleteAttachmentWorkerTest.kt | 32 +++++++++++++++++++ 4 files changed, 56 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/api/segments/BaseApi.kt b/app/src/main/java/ch/protonmail/android/api/segments/BaseApi.kt index ff6470868..921cb55de 100644 --- a/app/src/main/java/ch/protonmail/android/api/segments/BaseApi.kt +++ b/app/src/main/java/ch/protonmail/android/api/segments/BaseApi.kt @@ -47,23 +47,26 @@ const val RESPONSE_CODE_UNAUTHORIZED = 401 const val RESPONSE_CODE_GATEWAY_TIMEOUT = 504 const val RESPONSE_CODE_TOO_MANY_REQUESTS = 429 const val RESPONSE_CODE_SERVICE_UNAVAILABLE = 503 -const val RESPONSE_CODE_OLD_PASSWORD_INCORRECT = 8002 -const val RESPONSE_CODE_NEW_PASSWORD_INCORRECT = 12022 -const val RESPONSE_CODE_NEW_PASSWORD_MESSED_UP = 12020 -const val RESPONSE_CODE_ATTACHMENT_DELETE_ID_INVALID = 11123 +const val RESPONSE_CODE_INVALID_ID = 2061 +const val RESPONSE_CODE_MESSAGE_READING_RESTRICTED = 2028 +const val RESPONSE_CODE_ERROR_GROUP_ALREADY_EXIST = 2500 const val RESPONSE_CODE_INVALID_APP_CODE = 5002 const val RESPONSE_CODE_FORCE_UPGRADE = 5003 -const val RESPONSE_CODE_RECIPIENT_NOT_FOUND = 33102 -const val RESPONSE_CODE_INVALID_EMAIL = 12065 -const val RESPONSE_CODE_INCORRECT_PASSWORD = 12066 -const val RESPONSE_CODE_ERROR_EMAIL_EXIST = 13007 -const val RESPONSE_CODE_ERROR_CONTACT_EXIST_THIS_EMAIL = 13002 -const val RESPONSE_CODE_ERROR_INVALID_EMAIL = 13006 -const val RESPONSE_CODE_ERROR_EMAIL_VALIDATION_FAILED = 13014 -const val RESPONSE_CODE_ERROR_EMAIL_DUPLICATE_FAILED = 13061 +const val RESPONSE_CODE_OLD_PASSWORD_INCORRECT = 8002 const val RESPONSE_CODE_ERROR_VERIFICATION_NEEDED = 9001 -const val RESPONSE_CODE_ERROR_GROUP_ALREADY_EXIST = 2500 -const val RESPONSE_CODE_EMAIL_FAILED_VALIDATION = 12006 +const val RESPONSE_CODE_ATTACHMENT_DELETE_ID_INVALID = 11_123 +const val RESPONSE_CODE_EMAIL_FAILED_VALIDATION = 12_006 +const val RESPONSE_CODE_NEW_PASSWORD_INCORRECT = 12_022 +const val RESPONSE_CODE_NEW_PASSWORD_MESSED_UP = 12_020 +const val RESPONSE_CODE_INVALID_EMAIL = 12_065 +const val RESPONSE_CODE_INCORRECT_PASSWORD = 12_066 +const val RESPONSE_CODE_ERROR_CONTACT_EXIST_THIS_EMAIL = 13_002 +const val RESPONSE_CODE_ERROR_INVALID_EMAIL = 13_006 +const val RESPONSE_CODE_ERROR_EMAIL_EXIST = 13_007 +const val RESPONSE_CODE_ERROR_EMAIL_VALIDATION_FAILED = 13_014 +const val RESPONSE_CODE_ERROR_EMAIL_DUPLICATE_FAILED = 13_061 +const val RESPONSE_CODE_MESSAGE_DOES_NOT_EXIST = 15_052 +const val RESPONSE_CODE_RECIPIENT_NOT_FOUND = 33_102 // endregion open class BaseApi { diff --git a/app/src/main/java/ch/protonmail/android/api/segments/event/EventHandler.kt b/app/src/main/java/ch/protonmail/android/api/segments/event/EventHandler.kt index 6a0635c90..0f5414295 100644 --- a/app/src/main/java/ch/protonmail/android/api/segments/event/EventHandler.kt +++ b/app/src/main/java/ch/protonmail/android/api/segments/event/EventHandler.kt @@ -50,6 +50,9 @@ import ch.protonmail.android.api.models.room.messages.MessagesDao import ch.protonmail.android.api.models.room.messages.MessagesDatabase import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDao import ch.protonmail.android.api.models.room.pendingActions.PendingActionsDatabase +import ch.protonmail.android.api.segments.RESPONSE_CODE_INVALID_ID +import ch.protonmail.android.api.segments.RESPONSE_CODE_MESSAGE_DOES_NOT_EXIST +import ch.protonmail.android.api.segments.RESPONSE_CODE_MESSAGE_READING_RESTRICTED import ch.protonmail.android.core.Constants import ch.protonmail.android.core.UserManager import ch.protonmail.android.events.MessageCountsEvent @@ -76,9 +79,6 @@ import kotlin.math.max // region constants private const val TAG_EVENT_HANDLER = "EventHandler" -private const val RESPONSE_CODE_INVALID_ID = 2061 -private const val RESPONSE_CODE_MESSAGE_READING_RESTRICTED = 2028 -private const val RESPONSE_CODE_MESSAGE_DOES_NOT_EXIST = 15_052 // endregion enum class EventType(val eventType: Int) { diff --git a/app/src/main/java/ch/protonmail/android/worker/DeleteAttachmentWorker.kt b/app/src/main/java/ch/protonmail/android/worker/DeleteAttachmentWorker.kt index 39baf0fc4..d85e7349e 100644 --- a/app/src/main/java/ch/protonmail/android/worker/DeleteAttachmentWorker.kt +++ b/app/src/main/java/ch/protonmail/android/worker/DeleteAttachmentWorker.kt @@ -32,7 +32,7 @@ import androidx.work.WorkerParameters import androidx.work.workDataOf import ch.protonmail.android.api.ProtonMailApiManager import ch.protonmail.android.api.models.room.messages.MessagesDatabase -import ch.protonmail.android.api.segments.RESPONSE_CODE_ATTACHMENT_DELETE_ID_INVALID +import ch.protonmail.android.api.segments.RESPONSE_CODE_INVALID_ID import ch.protonmail.android.attachments.KEY_INPUT_DATA_ATTACHMENT_ID_STRING import ch.protonmail.android.core.Constants import kotlinx.coroutines.withContext @@ -75,14 +75,16 @@ class DeleteAttachmentWorker @WorkerInject constructor( val response = api.deleteAttachment(attachmentId) if (response.code == Constants.RESPONSE_CODE_OK || - response.code == RESPONSE_CODE_ATTACHMENT_DELETE_ID_INVALID + response.code == RESPONSE_CODE_INVALID_ID ) { + Timber.v("Attachment ID: $attachmentId deleted on remote") val attachment = messagesDatabase.findAttachmentById(attachmentId) attachment?.let { messagesDatabase.deleteAttachment(it) return@withContext Result.success() } } + Timber.i("Delete Attachment on remote failure response: ${response.code} ${response.error}") Result.failure( workDataOf(KEY_WORKER_ERROR_DESCRIPTION to "ApiException response code ${response.code}") ) diff --git a/app/src/test/java/ch/protonmail/android/worker/DeleteAttachmentWorkerTest.kt b/app/src/test/java/ch/protonmail/android/worker/DeleteAttachmentWorkerTest.kt index 13f0eed2f..40533ea33 100644 --- a/app/src/test/java/ch/protonmail/android/worker/DeleteAttachmentWorkerTest.kt +++ b/app/src/test/java/ch/protonmail/android/worker/DeleteAttachmentWorkerTest.kt @@ -27,6 +27,7 @@ import ch.protonmail.android.api.ProtonMailApiManager import ch.protonmail.android.api.models.ResponseBody import ch.protonmail.android.api.models.room.messages.Attachment import ch.protonmail.android.api.models.room.messages.MessagesDao +import ch.protonmail.android.api.segments.RESPONSE_CODE_INVALID_ID import ch.protonmail.android.attachments.KEY_INPUT_DATA_ATTACHMENT_ID_STRING import ch.protonmail.android.core.Constants import io.mockk.MockKAnnotations @@ -35,6 +36,7 @@ import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.RelaxedMockK import io.mockk.mockk +import io.mockk.verify import kotlinx.coroutines.test.runBlockingTest import me.proton.core.test.kotlin.TestDispatcherProvider import kotlin.test.BeforeTest @@ -110,8 +112,10 @@ class DeleteAttachmentWorkerTest { // given val attachmentId = "id232" val randomErrorCode = 11212 + val errorMessage = "an error occurred" val deleteResponse = mockk { every { code } returns randomErrorCode + every { error } returns errorMessage } val expected = ListenableWorker.Result.failure( workDataOf(KEY_WORKER_ERROR_DESCRIPTION to "ApiException response code $randomErrorCode") @@ -130,4 +134,32 @@ class DeleteAttachmentWorkerTest { assertEquals(operationResult, expected) } } + + @Test + fun verifyThatServerErrorInvalidIdIsIgnoredAndMessageIsRemovedFromDbWithSuccess() { + runBlockingTest { + // given + val attachmentId = "id232" + val errorCode = RESPONSE_CODE_INVALID_ID + val errorMessage = "Invalid ID" + val deleteResponse = mockk { + every { code } returns errorCode + every { error } returns errorMessage + } + val expected = ListenableWorker.Result.success() + val attachment = mockk() + every { messagesDb.findAttachmentById(attachmentId) } returns attachment + every { messagesDb.deleteAttachment(attachment) } returns mockk() + every { parameters.inputData } returns + workDataOf(KEY_INPUT_DATA_ATTACHMENT_ID_STRING to attachmentId) + coEvery { api.deleteAttachment(any()) } returns deleteResponse + + // when + val operationResult = worker.doWork() + + // then + verify { messagesDb.deleteAttachment(attachment) } + assertEquals(operationResult, expected) + } + } } From 53747366b9b364f7512e6040f107a95fe00894cf Mon Sep 17 00:00:00 2001 From: proton-ci Date: Mon, 25 Jan 2021 06:37:08 +0000 Subject: [PATCH 075/145] [i18n@] ~ Upgrade translations from crowdin --- app/src/main/res/values-ca-rES/strings.xml | 4 +++- app/src/main/res/values-cs/strings.xml | 2 ++ app/src/main/res/values-da/strings.xml | 2 ++ app/src/main/res/values-de/strings.xml | 2 ++ app/src/main/res/values-el/strings.xml | 2 ++ app/src/main/res/values-es/strings.xml | 18 ++++++++++-------- app/src/main/res/values-fr/strings.xml | 2 ++ app/src/main/res/values-hr/strings.xml | 2 ++ app/src/main/res/values-hu-rHU/strings.xml | 2 ++ app/src/main/res/values-in/strings.xml | 2 ++ app/src/main/res/values-is-rIS/strings.xml | 2 ++ app/src/main/res/values-it/strings.xml | 2 ++ app/src/main/res/values-ja/strings.xml | 2 ++ app/src/main/res/values-kab/strings.xml | 2 ++ app/src/main/res/values-nl/strings.xml | 2 ++ app/src/main/res/values-pl/strings.xml | 2 ++ app/src/main/res/values-pt-rBR/strings.xml | 2 ++ app/src/main/res/values-pt/strings.xml | 2 ++ app/src/main/res/values-ro/strings.xml | 2 ++ app/src/main/res/values-ru/strings.xml | 2 ++ app/src/main/res/values-sv-rSE/strings.xml | 2 ++ app/src/main/res/values-tr/strings.xml | 6 ++++-- app/src/main/res/values-uk/strings.xml | 2 ++ app/src/main/res/values-zh-rCN/strings.xml | 2 ++ app/src/main/res/values-zh-rTW/strings.xml | 2 ++ 25 files changed, 61 insertions(+), 11 deletions(-) diff --git a/app/src/main/res/values-ca-rES/strings.xml b/app/src/main/res/values-ca-rES/strings.xml index efdf2b085..276d68d1e 100644 --- a/app/src/main/res/values-ca-rES/strings.xml +++ b/app/src/main/res/values-ca-rES/strings.xml @@ -988,7 +988,7 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Imatge no trobada Url de la imatge mal formada - Error while saving message. Try again. + Error en guardar el missatge. Torna a intentar-ho. Permés Denegat @@ -998,4 +998,6 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Anar a configuració La clau de l\'API de Firebase no és vàlida. Les notificacions Push no funcionaran. + Error en guardar un borrador en línia pel missatge: \"%s\" + No es pot obrir aquest missatge mentre s\'envia diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 28c697f17..5bd16afd2 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -1029,4 +1029,6 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Přejít do nastavení Klíč Firebase API není platný. Oznámení nebudou fungovat. + Failed saving online draft for message: \"%s\" + Can\'t open this message while it\'s being sent diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 8b4ec6894..2456fc3cf 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -1000,4 +1000,6 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Go to settings Ugyldig Firebase API-nøgle. Push-meddelelser vil ikke fungere. + Failed saving online draft for message: \"%s\" + Can\'t open this message while it\'s being sent diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 5945e346c..aadf2f6e4 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -984,4 +984,6 @@ zu: <br/><br/><u>%s</u><br/><br/> Möchten S Einstellungen öffnen Ungültiger Firebase API-Schlüssel. Push-Benachrichtigungen werden nicht funktionieren. + Speichern des Online-Entwurfs für Nachricht „%s” fehlgeschlagen + Nachricht kann während des Sendens nicht geöffnet werden diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 5833f1319..54255a87d 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -1000,4 +1000,6 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Μετάβαση στις ρυθμίσεις Μη έγκυρο κλειδί API Firebase. Οι ειδοποιήσεις push δεν θα λειτουργήσουν. + Απέτυχε η αποθήκευση του online προσχεδίου για το μήνυμα: \"%s\" + Δεν μπορεί να ανοίξει αυτό το μήνυμα κατά την αποστολή του diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index e7683baf3..7cf827dda 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -48,7 +48,7 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Credenciales inválidas Parámetros clave incorrectos Contraseña del buzón inválida - No está registrado en la Beta de ProtonMail para Android + No estás registrado en la Beta de ProtonMail para Android Sin conexión No hay conexión, mensaje en espera Actualización requerida @@ -979,13 +979,13 @@ compre más Problemas y soluciones habituales de conexión - Sin conexión a Internet - Asegúrese de que su conexión a Internet está funcionando. - \n\nproblema con el proveedor de servicios de Internet (ISP) - Intente conectarse a Proton desde una red diferente (o utilice ProtonVPN o Tor). - \n\nBloqueo de gobierno - Puede que su país esté bloqueando el acceso a Proton. Pruebe ProtonVPN (o cualquier otra VPN) o Tor para acceder a Proton. - \n\nInterferencia antivirus - Deshabilite o elimine temporalmente su antivirus. - \n\ninterferencia de Proxy/Firewall - Desactive cualquier proxy o cortafuegos, o póngase en contacto con su administrador de red. - \n\nProton está fuera de servicio - Compruebe Proton Status para ver el estado de nuestro sistema. - \n\nSi todavía no ha encontrado una solución - Contáctenos directamente a través de nuestro formulario de ayuda, correo electrónico (support@protonmail. om), o Twitter. + <i>Sin conexión a internet</i> - Por favor, asegúrate de que tu conexión a internet está funcionando. + <br/><br/><i>Problema con tu proveedor de servicios de Internet (ISP)</i> - Prueba a conectarte a Proton desde una red diferente (o utiliza <a href=\"https://protonvpn.com/\">ProtonVPN</a> o <a href=\"https://www.torproject.org/\">Tor</a>). + <br/><br/><i>Bloqueo del Gobierno</i> - Tu país puede estar bloqueando el acceso a Proton. Prueba <a href=\"https://protonvpn.com/\">ProtonVPN</a> (o cualquier otra VPN) o <a href=\"https://www.torproject.org/\">Tor</a> para acceder a Proton. + <br/><br/><i>Interferencia del antivirus</i> - Deshabilita o elimina temporalmente tu software de antivirus. + <br/><br/><i>Interferencia del proxy/firewall</i> - Desactiva cualquier proxy o firewall, o ponte en contacto con tu administrador de red. + <br/><br/><i>Proton está caído</i> - Comprueba <a href=\"http://protonstatus.com/\">Proton Status</a> para ver el estado de nuestro sistema. + <br/><br/><i>Todavía no puedes encontrar una solución</i> - Contáctanos directamente a través de nuestro <a href=\"https://protonmail.com/support-form\">formulario de soporte</a>, <a href=\"mailto:support@protonmail.com\">correo electrónico</a> (support@protonmail. om), o <a href=\"https://twitter.com/ProtonMail\">Twitter</a>. @@ -1001,4 +1001,6 @@ compre más Ir a los ajustes La clave de la API de Firebase no es válida. Las notificaciones Push no funcionarán. + Error al guardar el borrador en línea para el mensaje: \"%s\" + No se puede abrir este mensaje mientras está siendo enviado diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 61af41031..9c1348b3a 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1000,4 +1000,6 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Accéder aux paramètres Clé API pour Firebase invalide. Les notifications push ne fonctionneront pas. + Échec de l\'enregistrement du brouillon en ligne pour le message : \"%s\" + Impossible d\'ouvrir ce message lors de son envoi diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 4df68664c..834e8ccf4 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -1016,4 +1016,6 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Idi na postavke Nevažeći API ključ za Firebase. Push obavijesti neće raditi. + Spremanje online skice za poruku nije uspjelo: \"%s\" + Nije moguće otvoriti ovu poruku dok se šalje diff --git a/app/src/main/res/values-hu-rHU/strings.xml b/app/src/main/res/values-hu-rHU/strings.xml index ae5f1ab26..4e1bc2c2f 100644 --- a/app/src/main/res/values-hu-rHU/strings.xml +++ b/app/src/main/res/values-hu-rHU/strings.xml @@ -998,4 +998,6 @@ Svájci székhellyel Beállítások megnyitása Helytelen Firebase API kulcs. A Push értesítések nem fognak működni. + Failed saving online draft for message: \"%s\" + Can\'t open this message while it\'s being sent diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 203d8f77e..0e70a3474 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -985,4 +985,6 @@ ke:<br/><br/><u>%s</u><br/><br/>Apakah Anda Ke pengaturan Kunci API Firebase tidak valid. Notifikasi dorong tidak akan bekerja. + Gagal menyimpan draf daring pesan: \"%s\" + Tidak dapat membuka pesan ini saat sedang dikirim diff --git a/app/src/main/res/values-is-rIS/strings.xml b/app/src/main/res/values-is-rIS/strings.xml index 47f76b8dc..d3a5f48f7 100644 --- a/app/src/main/res/values-is-rIS/strings.xml +++ b/app/src/main/res/values-is-rIS/strings.xml @@ -999,4 +999,6 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Fara í stillingar Ógildur API-kerfisviðmótslykill fyrir Firebase. Ýtitilkynningar munu ekki virka. + Failed saving online draft for message: \"%s\" + Can\'t open this message while it\'s being sent diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 54c9035b1..f2b90ea66 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1000,4 +1000,6 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Vai alle impostazioni Chiave API Firebase non valida. Le notifiche push non funzioneranno. + Failed saving online draft for message: \"%s\" + Can\'t open this message while it\'s being sent diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 1242617da..ac204b0d1 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -970,4 +970,6 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. 設定を開く Firebase API キーが無効です。プッシュ通知は動作しません。 + Failed saving online draft for message: \"%s\" + Can\'t open this message while it\'s being sent diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index 4faf3f2c9..3b7f6f669 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -994,4 +994,6 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Ddu ɣer iɣewwaṛen Tasarut API i Firebase d mačči d tameɣtut. Ilɣuten push ur tteddun ara. + Yecceḍ usekles n urewway deg uẓeṭṭa i yizen: \"%s\" + Ur izmir ara ad ildi izen-a mi ara yettwazen diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 9dfc6a8eb..e68542330 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -995,4 +995,6 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Ga naar instellingen Ongeldige Firebase API-sleutel. Push-meldingen werken niet. + Failed saving online draft for message: \"%s\" + Can\'t open this message while it\'s being sent diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 4a581af12..6fb93c021 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -1027,4 +1027,6 @@ zresetować hasło logowania, jeśli je zapomnisz. Przejdź do ustawień Klucz interfejsu API Firebase jest nieprawidłowy. Powiadomienia nie będą działać. + Nie udało się zapisać szkicu: \"%s\" + Nie można otworzyć wiadomości podczas jej wysyłania diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index a4104ac65..3005d7154 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -1000,4 +1000,6 @@ redefina sua senha caso você necessite. Ir para configurações Chave API do Firebase inválida. As notificações push não funcionarão. + Falha ao salvar o rascunho online da mensagem: \"%s\" + Não é possível abrir esta mensagem enquanto ela está sendo enviada diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index cdf451c5e..f6e7890c8 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -999,4 +999,6 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Ir para as definições Chave API do Firebase inválida. As notificações push não funcionarão. + Falha ao gravar o rascunho online da mensagem: \"%s\" + Impossível abrir esta mensagem durante o envio diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index b08f269a9..139c5d8ee 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -1014,4 +1014,6 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Accesați setările Cheie API Firebase nevalidă. Notificările push nu vor funcționa. + Nu s-a reușit salvarea ciornei online pentru mesaj: \"%s\" + Acest mesaj nu poate fi deschis în timp ce este trimis diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 6911d4894..4863e6d77 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1032,4 +1032,6 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Перейти в настройки Неверный ключ API Firebase. Push-уведомления не будут работать. + Failed saving online draft for message: \"%s\" + Can\'t open this message while it\'s being sent diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 484be98c3..1e3c14777 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -992,4 +992,6 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Gå till inställningar Ogiltig Firebase API-nyckel. Push-meddelanden kommer inte fungera. + Det gick inte att spara utkast för meddelande: \"%s\" + Kan inte öppna detta meddelande när det skickas diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 9f7eef0e9..8c85fcc0a 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -233,7 +233,7 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Tekrar Gönder Doğrulama kodunu girin Doğrula - Açık + Yeni bir hesap oluştur Hesabınızı ayarlayın Hesap Oluştur @@ -978,7 +978,7 @@ Bu şifreyi kaybetmeyin, kurtaramayız. Şifrenizi kaybederseniz, e-postaların Resim bulunamadı Hatalı oluşturulmuş resim bağlantısı - Error while saving message. Try again. + İleti kaydedilirken hata oluştu. Tekrar deneyin. İzin verildi Reddedildi @@ -988,4 +988,6 @@ Bu şifreyi kaybetmeyin, kurtaramayız. Şifrenizi kaybederseniz, e-postaların Ayarlara git Geçersiz Firebase API anahtarı. Anlık bildirimler çalışmayacaktır. + İleti taslağı çevrimiçi olarak kaydedilemedi: \"%s\" + Bu ileti gönderilirken açılamaz diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index f70173015..7c9c54264 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -1030,4 +1030,6 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. Перейти до налаштувань Недійсний ключ API Firebas. Push-сповіщення не працюватимуть. + Failed saving online draft for message: \"%s\" + Can\'t open this message while it\'s being sent diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 0b9e3c7b9..08ffab4c6 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -967,4 +967,6 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. 前往设置 无效的 Firebase API 密钥。推送通知将无法使用。 + Failed saving online draft for message: \"%s\" + Can\'t open this message while it\'s being sent diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 4a98db105..e44548668 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -982,4 +982,6 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. 前往設定頁面 無效的 Firebase API 金鑰。推送通知將無法運作。 + 無法儲存郵件的線上草稿:「%s」 + 傳送時無法開啟此郵件 From f4416ee876e29fdd6a05885d31ebef30e0c85c34 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Fri, 13 Nov 2020 18:11:54 +0100 Subject: [PATCH 076/145] Create first ADR to agree on using ADRs This First ADR (`0000`) treats the decision of using ADRs in our ProtonMail project outlining the reasons, benefits and methodologies we want to follow --- ...-markdown-architectural-decision-record.md | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 docs/0000-use-markdown-architectural-decision-record.md diff --git a/docs/0000-use-markdown-architectural-decision-record.md b/docs/0000-use-markdown-architectural-decision-record.md new file mode 100644 index 000000000..8ad9e0409 --- /dev/null +++ b/docs/0000-use-markdown-architectural-decision-record.md @@ -0,0 +1,59 @@ +# Use Markdown Architectural Decision Records + +* Status: Accepted +* Deciders: Marino, Zorica, Stefanija, Davide, Tomasz, Dimitar, Nikola, Denys +* Date: 13/11/2020 + + +## Context and Problem Statement + +There are several very explicit goals that make the practice and discipline of architecture very important: + +* We want to think deeply about all our architectural decisions, exploring all alternatives and making a careful, considered, well-researched choice. +* We want to be as transparent as possible in our decision-making process. +* We don't want decisions to be made unilaterally in a vacuum. Specifically, we want to give our steering group the opportunity to review every major decision. +* Despite being a geographically and temporally distributed team, we want our contributors to have a strong shared understanding of the technical rationale behind decisions. +* We want to be able to revisit prior decisions to determine fairly if they still make sense, and if the motivating circumstances or conditions have changed. +* We want each developer in the company, new or old, to be have clear references to the decisions that were taken in the past and are currently in force in the project. + +**Which format and structure should these records follow?** + +## Considered Options + +* [MADR](https://adr.github.io/madr/) 2.1.0 - The Markdown Architectural Decision Records +* [Michael Nygard's template](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions) - The first incarnation of the term "ADR" +* [Sustainable Architectural Decisions](https://www.infoq.com/articles/sustainable-architectural-design-decisions) - The Y-Statements +* Other templates listed at +* Formless - No conventions for file format and structure + +## Decision Outcome + +Chosen option: "MADR 2.1.0", because + +* Implicit assumptions should be made explicit. +Design documentation is important to enable people understanding the decisions later on. +See also [A rational design process: How and why to fake it](https://doi.org/10.1109/TSE.1986.6312940). +* The MADR format is lean and fits our development style. +* The MADR structure is comprehensible and facilitates usage & maintenance. +* The MADR project is vivid. +* Version 2.1.0 is the latest one available when starting to document ADRs. + +The workflow will be: + +* A developer creates an ADR document outlining an approach for a particular question or problem. The ADR has an initial status of "proposed." +* The developers and steering group discuss the ADR. During this period, the ADR should be updated to reflect additional context, concerns raised, and proposed changes. +* Once consensus is reached, ADR can be transitioned to either an "accepted" or "rejected" state. +* Only after an ADR is accepted should implementing code be committed to the master branch of the relevant project/module. +* If a decision is revisited and a different conclusion is reached, a new ADR should be created documenting the context and rationale for the change. The new ADR should reference the old one, and once the new one is accepted, the old one should (in its "status" section) be updated to point to the new one. The old ADR should not be removed or otherwise modified except for the annotation pointing to the new ADR. + +## Consequences +1. Developers must write an ADR and submit it for review before selecting an approach to any architectural decision -- that is, any decision that affects the way ProtonMail application is put together at a high level. +2. We will have a concrete artifact around which to focus discussion, before finalizing decisions. +3. If we follow the process, decisions will be made deliberately, as a group. +4. The develop branch of our repositories will reflect the high-level consensus of the steering group. +5. We will have a useful persistent record of why the system is the way it is. + +## Links +* [Confluence page on ADR](https://confluence.protontech.ch/pages/viewpage.action?pageId=24117283) +* [Arachne Framework sample ADR](https://github.com/arachne-framework/architecture/blob/master/adr-001-use-adrs.md) +* [MADR ADR template](https://adr.github.io/madr/) From 8b1653bede0afebb6c5c019a1208a14fb9e0994f Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Wed, 13 Jan 2021 10:24:48 +0100 Subject: [PATCH 077/145] Imprpove formatting based on PR#296 feedback --- .../mailbox/ShowLabelsManagerDialogTask.kt | 46 ++--- .../repository/MessageDetailsRepository.kt | 182 +++++++++--------- .../api/models/room/messages/Message.kt | 23 ++- .../compose/ComposeMessageRepository.kt | 8 +- 4 files changed, 134 insertions(+), 125 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/activities/mailbox/ShowLabelsManagerDialogTask.kt b/app/src/main/java/ch/protonmail/android/activities/mailbox/ShowLabelsManagerDialogTask.kt index 2ed8a2197..e40882787 100644 --- a/app/src/main/java/ch/protonmail/android/activities/mailbox/ShowLabelsManagerDialogTask.kt +++ b/app/src/main/java/ch/protonmail/android/activities/mailbox/ShowLabelsManagerDialogTask.kt @@ -28,29 +28,31 @@ import java.util.HashMap import java.util.HashSet internal class ShowLabelsManagerDialogTask( - private val fragmentManager: FragmentManager, - private val messageDetailsRepository: MessageDetailsRepository, - private val messageIds: List + private val fragmentManager: FragmentManager, + private val messageDetailsRepository: MessageDetailsRepository, + private val messageIds: List ) : AsyncTask>() { - override fun doInBackground(vararg voids: Void): List { - return messageIds.filter { it.isNotEmpty() }.mapNotNull(messageDetailsRepository::findMessageByIdBlocking) - } + override fun doInBackground(vararg voids: Void): List = + messageIds.filter { it.isNotEmpty() }.mapNotNull(messageDetailsRepository::findMessageByIdBlocking) - override fun onPostExecute(messages: List) { - val attachedLabels = HashSet() - val numberOfSelectedMessages = HashMap() - messages.forEach { message -> - val messageLabelIds = message.labelIDsNotIncludingLocations - messageLabelIds.forEach { labelId -> - numberOfSelectedMessages[labelId] = numberOfSelectedMessages[labelId]?.let { it + 1 } ?: 1 - } - attachedLabels.addAll(messageLabelIds) - } - val manageLabelsDialogFragment = ManageLabelsDialogFragment.newInstance( - attachedLabels, numberOfSelectedMessages, ArrayList(messageIds)) - val transaction = fragmentManager.beginTransaction() - transaction.add(manageLabelsDialogFragment, manageLabelsDialogFragment.fragmentKey) - transaction.commitAllowingStateLoss() - } + override fun onPostExecute(messages: List) { + val attachedLabels = HashSet() + val numberOfSelectedMessages = HashMap() + messages.forEach { message -> + val messageLabelIds = message.labelIDsNotIncludingLocations + messageLabelIds.forEach { labelId -> + numberOfSelectedMessages[labelId] = numberOfSelectedMessages[labelId]?.let { it + 1 } ?: 1 + } + attachedLabels.addAll(messageLabelIds) + } + val manageLabelsDialogFragment = ManageLabelsDialogFragment.newInstance( + attachedLabels, + numberOfSelectedMessages, + ArrayList(messageIds) + ) + val transaction = fragmentManager.beginTransaction() + transaction.add(manageLabelsDialogFragment, manageLabelsDialogFragment.fragmentKey) + transaction.commitAllowingStateLoss() + } } diff --git a/app/src/main/java/ch/protonmail/android/activities/messageDetails/repository/MessageDetailsRepository.kt b/app/src/main/java/ch/protonmail/android/activities/messageDetails/repository/MessageDetailsRepository.kt index c038c49f1..8d24a22db 100644 --- a/app/src/main/java/ch/protonmail/android/activities/messageDetails/repository/MessageDetailsRepository.kt +++ b/app/src/main/java/ch/protonmail/android/activities/messageDetails/repository/MessageDetailsRepository.kt @@ -72,7 +72,7 @@ class MessageDetailsRepository @Inject constructor( @Named("messages_search") var searchDatabaseDao: MessagesDao, private var pendingActionsDatabase: PendingActionsDao, private val applicationContext: Context, - var databaseProvider: DatabaseProvider, + val databaseProvider: DatabaseProvider, private val dispatchers: DispatcherProvider ) { @@ -105,35 +105,40 @@ class MessageDetailsRepository @Inject constructor( suspend fun findMessageByMessageDbId(dbId: Long, dispatcher: CoroutineDispatcher): Message? = withContext(dispatcher) { - findMessageByMessageDbId(dbId) - } + findMessageByMessageDbId(dbId) + } fun findMessageByIdBlocking(messageId: String): Message? = messagesDao.findMessageById(messageId)?.apply { readMessageBodyFromFileIfNeeded(this) } fun findSearchMessageById(messageId: String): Message? = - searchDatabaseDao.findMessageById(messageId)?.apply { readMessageBodyFromFileIfNeeded(this) } + searchDatabaseDao.findMessageById(messageId)?.apply { readMessageBodyFromFileIfNeeded(this) } fun findMessageByIdSingle(messageId: String): Single = - messagesDao.findMessageByIdSingle(messageId).map(readMessageBodyFromFileIfNeeded) + messagesDao.findMessageByIdSingle(messageId).map(readMessageBodyFromFileIfNeeded) fun findMessageByIdObservable(messageId: String): Flowable = - messagesDao.findMessageByIdObservable(messageId).map(readMessageBodyFromFileIfNeeded) + messagesDao.findMessageByIdObservable(messageId).map(readMessageBodyFromFileIfNeeded) fun findMessageByMessageDbId(messageDbId: Long): Message? = - messagesDao.findMessageByMessageDbId(messageDbId)?.apply { readMessageBodyFromFileIfNeeded(this) } + messagesDao.findMessageByMessageDbId(messageDbId)?.apply { readMessageBodyFromFileIfNeeded(this) } fun findAllMessageByLastMessageAccessTime(laterThan: Long = 0): List = - messagesDao.findAllMessageByLastMessageAccessTime(laterThan).mapNotNull { readMessageBodyFromFileIfNeeded(it) } + messagesDao.findAllMessageByLastMessageAccessTime(laterThan).mapNotNull { readMessageBodyFromFileIfNeeded(it) } /** * Helper function mapping Message with body saved in file to body in memory. */ private val readMessageBodyFromFileIfNeeded: (Message?) -> Message? = { message -> message?.apply { - if (Constants.FeatureFlags.SAVE_MESSAGE_BODY_TO_FILE && - true == message.messageBody?.startsWith("file://")) { - val messageBodyFile = File(applicationContext.filesDir.toString() + Constants.DIR_MESSAGE_BODY_DOWNLOADS, message.messageId?.replace(" ", "_")?.replace("/", ":")) + if ( + Constants.FeatureFlags.SAVE_MESSAGE_BODY_TO_FILE && + true == message.messageBody?.startsWith("file://") + ) { + val messageBodyFile = File( + applicationContext.filesDir.toString() + Constants.DIR_MESSAGE_BODY_DOWNLOADS, + message.messageId?.replace(" ", "_")?.replace("/", ":") + ) message.messageBody = try { Timber.d("Reading body from file ${messageBodyFile.name}") FileInputStream(messageBodyFile).bufferedReader().use { it.readText() } @@ -150,14 +155,14 @@ class MessageDetailsRepository @Inject constructor( fun getMessagesByLabelIdAsync(label: String): LiveData> = messagesDao.getMessagesByLabelIdAsync(label) fun getMessagesByLocationAsync(location: Int): LiveData> = - messagesDao.getMessagesByLocationAsync(location) + messagesDao.getMessagesByLocationAsync(location) fun getAllMessages(): LiveData> = messagesDao.getAllMessages() fun getAllSearchMessages(): LiveData> = searchDatabaseDao.getAllMessages() fun searchMessages(subject: String, senderName: String, senderEmail: String): List = - messagesDao.searchMessages(subject, senderName, senderEmail).mapNotNull { readMessageBodyFromFileIfNeeded(it) } + messagesDao.searchMessages(subject, senderName, senderEmail).mapNotNull { readMessageBodyFromFileIfNeeded(it) } fun setFolderLocation(message: Message) = message.setFolderLocation(messagesDao) @@ -198,7 +203,7 @@ class MessageDetailsRepository @Inject constructor( fun saveAttachment(attachment: Attachment) = messagesDao.saveAttachment(attachment) fun findPendingSendByOfflineMessageIdAsync(messageId: String) = - pendingActionsDatabase.findPendingSendByOfflineMessageIdAsync(messageId) + pendingActionsDatabase.findPendingSendByOfflineMessageIdAsync(messageId) fun findPendingSendByMessageId(messageId: String) = pendingActionsDatabase.findPendingSendByMessageId(messageId) @@ -256,7 +261,7 @@ class MessageDetailsRepository @Inject constructor( messages.map(this::saveSearchMessageInDB) } - fun saveSearchMessagesInOneTransaction(messages:List) { + fun saveSearchMessagesInOneTransaction(messages: List) { if (messages.isEmpty()) { return } @@ -302,13 +307,13 @@ class MessageDetailsRepository @Inject constructor( return null } - fun deleteMessage(message:Message) = messagesDao.deleteMessage(message) + fun deleteMessage(message: Message) = messagesDao.deleteMessage(message) fun deleteMessagesByLocation(location: Constants.MessageLocationType) = - messagesDao.deleteMessagesByLocation(location.messageLocationTypeValue) + messagesDao.deleteMessagesByLocation(location.messageLocationTypeValue) fun deleteMessagesByLabel(labelId: String) = - messagesDao.deleteMessagesByLabel(labelId) + messagesDao.deleteMessagesByLabel(labelId) fun updateStarred(messageId: String, starred: Boolean) = messagesDao.updateStarred(messageId, starred) @@ -363,82 +368,85 @@ class MessageDetailsRepository @Inject constructor( dispatcher: CoroutineDispatcher, isTransient: Boolean ): IntentExtrasData = withContext(dispatcher) { - var toRecipientListString = "" - var includeCCList = false - val replyToEmailsFiltered = ArrayList() - when (messageAction) { - Constants.MessageActionType.REPLY -> { - val replyToEmails = message.replyToEmails - for (replyToEmail in replyToEmails) { - if (user.addresses!!.none { it.email equalsNoCase replyToEmail }) { - replyToEmailsFiltered.add(replyToEmail) - } - } - if (replyToEmailsFiltered.isEmpty()) { - replyToEmailsFiltered.addAll(listOf(message.toListString)) - } - - toRecipientListString = MessageUtils.getListOfStringsAsString(replyToEmailsFiltered) - } - Constants.MessageActionType.REPLY_ALL -> { - val emailSet = HashSet( - Arrays.asList(*message.toListString.split(Constants.EMAIL_DELIMITER.toRegex()) - .dropLastWhile { it.isEmpty() }.toTypedArray()) - ) - - val senderEmailAddress = if (message.replyToEmails.isNotEmpty()) - message.replyToEmails[0] else message.sender!!.emailAddress - toRecipientListString = if (emailSet.contains(senderEmailAddress)) { - message.toListString - } else { - senderEmailAddress + Constants.EMAIL_DELIMITER + message.toListString - } - includeCCList = true - } - else -> { - //NO OP + var toRecipientListString = "" + var includeCCList = false + val replyToEmailsFiltered = ArrayList() + when (messageAction) { + Constants.MessageActionType.REPLY -> { + val replyToEmails = message.replyToEmails + for (replyToEmail in replyToEmails) { + if (user.addresses!!.none { it.email equalsNoCase replyToEmail }) { + replyToEmailsFiltered.add(replyToEmail) } } - - val attachments = withContext(dispatcher) { - ArrayList(LocalAttachment.createLocalAttachmentList( - if (!isTransient) { - message.attachments(messagesDao) - } else { - message.attachments(searchDatabaseDao) - })) + if (replyToEmailsFiltered.isEmpty()) { + replyToEmailsFiltered.addAll(listOf(message.toListString)) } - IntentExtrasData.Builder() - .user(user) - .userAddresses() - .message(message) - .toRecipientListString(toRecipientListString) - .messageCcList() - .includeCCList(includeCCList) - .senderEmailAddress() - .messageSenderName() - .newMessageTitle(newMessageTitle) - .content(content) - .mBigContentHolder(mBigContentHolder) - .body() - .messageAction(messageAction) - .imagesDisplayed(mImagesDisplayed) // TODO - .remoteContentDisplayed(remoteContentDisplayed) - .isPGPMime() - .timeMs() - .messageIsEncrypted() - .messageId() - .addressID() - .addressEmailAlias() - .attachments(attachments, embeddedImagesAttachments) - .build() + toRecipientListString = MessageUtils.getListOfStringsAsString(replyToEmailsFiltered) + } + Constants.MessageActionType.REPLY_ALL -> { + val emailSet = HashSet( + Arrays.asList(*message.toListString.split(Constants.EMAIL_DELIMITER.toRegex()) + .dropLastWhile { it.isEmpty() }.toTypedArray()) + ) + + val senderEmailAddress = if (message.replyToEmails.isNotEmpty()) + message.replyToEmails[0] else message.sender!!.emailAddress + toRecipientListString = if (emailSet.contains(senderEmailAddress)) { + message.toListString + } else { + senderEmailAddress + Constants.EMAIL_DELIMITER + message.toListString + } + includeCCList = true + } + else -> { + //NO OP } + } + + val attachments = withContext(dispatcher) { + ArrayList( + LocalAttachment.createLocalAttachmentList( + if (!isTransient) { + message.attachments(messagesDao) + } else { + message.attachments(searchDatabaseDao) + } + ) + ) + } + + IntentExtrasData.Builder() + .user(user) + .userAddresses() + .message(message) + .toRecipientListString(toRecipientListString) + .messageCcList() + .includeCCList(includeCCList) + .senderEmailAddress() + .messageSenderName() + .newMessageTitle(newMessageTitle) + .content(content) + .mBigContentHolder(mBigContentHolder) + .body() + .messageAction(messageAction) + .imagesDisplayed(mImagesDisplayed) + .remoteContentDisplayed(remoteContentDisplayed) + .isPGPMime() + .timeMs() + .messageIsEncrypted() + .messageId() + .addressID() + .addressEmailAlias() + .attachments(attachments, embeddedImagesAttachments) + .build() + } suspend fun checkIfAttHeadersArePresent(message: Message, dispatcher: CoroutineDispatcher): Boolean = - withContext(dispatcher) { - message.checkIfAttHeadersArePresent(messagesDao) - } + withContext(dispatcher) { + message.checkIfAttHeadersArePresent(messagesDao) + } fun fetchMessageDetails(messageId: String): MessageResponse { return try { diff --git a/app/src/main/java/ch/protonmail/android/api/models/room/messages/Message.kt b/app/src/main/java/ch/protonmail/android/api/models/room/messages/Message.kt index 5ad17a092..090403d61 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/room/messages/Message.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/room/messages/Message.kt @@ -216,23 +216,22 @@ data class Message @JvmOverloads constructor( get() = messageEncryption in listOf(MessageEncryption.MIME_PGP) || Constants.MIME_TYPE_MULTIPART_MIXED == mimeType val replyToEmails: List - get() { - return replyTos - .asSequence() - .filter { it.address.isNotEmpty() } - .map { it.address } - .toList() - } + get() = replyTos + .asSequence() + .filter { it.address.isNotEmpty() } + .map { it.address } + .toList() + val toListString get() = MessageUtils.toContactString(toList) - val toListStringGroupsAware - get() = MessageUtils.toContactsAndGroupsString(toList) + val toListStringGroupsAware + get() = MessageUtils.toContactsAndGroupsString(toList) - val ccListString - get() = MessageUtils.toContactString(ccList) + val ccListString + get() = MessageUtils.toContactString(ccList) - val bccListString:String + val bccListString:String get() = MessageUtils.toContactString(bccList) fun locationFromLabel(): Constants.MessageLocationType = diff --git a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageRepository.kt b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageRepository.kt index 34eda5b95..feb2f6d22 100644 --- a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageRepository.kt +++ b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageRepository.kt @@ -61,10 +61,10 @@ class ComposeMessageRepository @Inject constructor( val jobManager: JobManager, val api: ProtonMailApiManager, val databaseProvider: DatabaseProvider, - @Named("messages") var messagesDatabase: MessagesDatabase, - @Named("messages_search") val searchDatabase: MessagesDatabase, - val messageDetailsRepository: MessageDetailsRepository, // FIXME: this should be removed){} - val dispatchers: DispatcherProvider + @Named("messages") private var messagesDatabase: MessagesDatabase, + @Named("messages_search") private val searchDatabase: MessagesDatabase, + private val messageDetailsRepository: MessageDetailsRepository, // FIXME: this should be removed){} + private val dispatchers: DispatcherProvider ) { val lazyManager = resettableManager() From 03a9beee885854c8dfb6a10636342e6314f89fbe Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Wed, 13 Jan 2021 12:20:33 +0100 Subject: [PATCH 078/145] Remove IMessageFactory interface and reformat files MAILAND-1304 --- .../messages/receive/IMessageFactory.kt | 28 --------- .../messages/receive/IMessageSenderFactory.kt | 26 -------- .../models/messages/receive/MessageFactory.kt | 60 +++++++++++-------- .../messages/receive/MessageSenderFactory.kt | 21 +++---- .../messages/send/MessageSendResponse.java | 7 +-- .../android/di/ApplicationModule.kt | 5 -- 6 files changed, 49 insertions(+), 98 deletions(-) delete mode 100644 app/src/main/java/ch/protonmail/android/api/models/messages/receive/IMessageFactory.kt delete mode 100644 app/src/main/java/ch/protonmail/android/api/models/messages/receive/IMessageSenderFactory.kt diff --git a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/IMessageFactory.kt b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/IMessageFactory.kt deleted file mode 100644 index fdd17ddf6..000000000 --- a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/IMessageFactory.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2020 Proton Technologies AG - * - * This file is part of ProtonMail. - * - * ProtonMail is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ProtonMail is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ProtonMail. If not, see https://www.gnu.org/licenses/. - */ -package ch.protonmail.android.api.models.messages.receive - -import ch.protonmail.android.api.models.DraftBody -import ch.protonmail.android.api.models.room.messages.Message - -interface IMessageFactory { - fun createMessage(serverMessage: ServerMessage): Message - fun createServerMessage(message: Message): ServerMessage - fun createDraftApiRequest(message: Message): DraftBody -} diff --git a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/IMessageSenderFactory.kt b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/IMessageSenderFactory.kt deleted file mode 100644 index d8a60948e..000000000 --- a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/IMessageSenderFactory.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) 2020 Proton Technologies AG - * - * This file is part of ProtonMail. - * - * ProtonMail is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ProtonMail is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ProtonMail. If not, see https://www.gnu.org/licenses/. - */ -package ch.protonmail.android.api.models.messages.receive - -import ch.protonmail.android.api.models.room.messages.MessageSender - -interface IMessageSenderFactory{ - fun createMessageSender(serverMessageSender:ServerMessageSender):MessageSender - fun createServerMessageSender(messageSender:MessageSender):ServerMessageSender -} diff --git a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageFactory.kt b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageFactory.kt index 981f20950..32e2ddbc8 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageFactory.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageFactory.kt @@ -31,13 +31,13 @@ import javax.inject.Inject class MessageFactory @Inject constructor( private val attachmentFactory: IAttachmentFactory, - private val messageSenderFactory: IMessageSenderFactory -) : IMessageFactory { + private val messageSenderFactory: MessageSenderFactory +) { - override fun createDraftApiRequest(message: Message): DraftBody = + fun createDraftApiRequest(message: Message): DraftBody = DraftBody(createServerMessage(message)) - override fun createServerMessage(message: Message): ServerMessage { + fun createServerMessage(message: Message): ServerMessage { return message.let { val serverMessage = ServerMessage() serverMessage.ID = it.messageId @@ -68,7 +68,7 @@ class MessageFactory @Inject constructor( } } - override fun createMessage(serverMessage: ServerMessage): Message { + fun createMessage(serverMessage: ServerMessage): Message { return serverMessage.let { val message = Message() message.messageId = it.ID @@ -82,26 +82,34 @@ class MessageFactory @Inject constructor( message.time = it.Time.checkIfSet("Time") message.totalSize = it.Size.checkIfSet("Size") message.location = it.LabelIDs!! - .asSequence() - .filter { it.length <= 2 } - .map { it.toInt() } - .fold(Constants.MessageLocationType.ALL_MAIL.messageLocationTypeValue) { location, newLocation -> - if (newLocation !in listOf(Constants.MessageLocationType.STARRED.messageLocationTypeValue, - Constants.MessageLocationType.ALL_MAIL.messageLocationTypeValue, - Constants.MessageLocationType.INVALID.messageLocationTypeValue) && - newLocation < location) { - newLocation - } else if (newLocation in listOf(Constants.MessageLocationType.DRAFT.messageLocationTypeValue, - Constants.MessageLocationType.SENT.messageLocationTypeValue)) { - newLocation - } else - location - } + .asSequence() + .filter { it.length <= 2 } + .map { it.toInt() } + .fold(Constants.MessageLocationType.ALL_MAIL.messageLocationTypeValue) { location, newLocation -> + if ( + newLocation !in listOf( + Constants.MessageLocationType.STARRED.messageLocationTypeValue, + Constants.MessageLocationType.ALL_MAIL.messageLocationTypeValue, + Constants.MessageLocationType.INVALID.messageLocationTypeValue + ) && + newLocation < location + ) { + newLocation + } else if ( + newLocation in listOf( + Constants.MessageLocationType.DRAFT.messageLocationTypeValue, + Constants.MessageLocationType.SENT.messageLocationTypeValue + ) + ) { + newLocation + } else + location + } message.isStarred = it.LabelIDs!! - .asSequence() - .filter { it.length <= 2 } - .map { Constants.MessageLocationType.fromInt(it.toInt()) } - .contains(Constants.MessageLocationType.STARRED) + .asSequence() + .filter { it.length <= 2 } + .map { Constants.MessageLocationType.fromInt(it.toInt()) } + .contains(Constants.MessageLocationType.STARRED) message.folderLocation = it.FolderLocation message.numAttachments = it.NumAttachments message.messageEncryption = MessageUtils.calculateEncryption(it.Flags) @@ -126,7 +134,9 @@ class MessageFactory @Inject constructor( val numOfAttachments = message.numAttachments val attachmentsListSize = message.Attachments.size if (attachmentsListSize != 0 && attachmentsListSize != numOfAttachments) - throw RuntimeException("Attachments size does not match expected: $numOfAttachments, actual: $attachmentsListSize ") + throw RuntimeException( + "Attachments size does not match expected: $numOfAttachments, actual: $attachmentsListSize " + ) message } } diff --git a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageSenderFactory.kt b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageSenderFactory.kt index 41e88b0f7..17ddf2338 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageSenderFactory.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageSenderFactory.kt @@ -20,17 +20,18 @@ package ch.protonmail.android.api.models.messages.receive import ch.protonmail.android.api.models.room.messages.MessageSender import ch.protonmail.android.utils.extensions.notNull +import javax.inject.Inject -class MessageSenderFactory : IMessageSenderFactory { +class MessageSenderFactory @Inject constructor() { - override fun createServerMessageSender(messageSender: MessageSender): ServerMessageSender { - val (name, emailAddress) = messageSender - return ServerMessageSender(name, emailAddress) - } + fun createServerMessageSender(messageSender: MessageSender): ServerMessageSender { + val (name, emailAddress) = messageSender + return ServerMessageSender(name, emailAddress) + } - override fun createMessageSender(serverMessageSender: ServerMessageSender): MessageSender { - val name = serverMessageSender.Name - val emailAddress = serverMessageSender.Address.notNull("emailAddress") - return MessageSender(name, emailAddress) - } + fun createMessageSender(serverMessageSender: ServerMessageSender): MessageSender { + val name = serverMessageSender.Name + val emailAddress = serverMessageSender.Address.notNull("emailAddress") + return MessageSender(name, emailAddress) + } } diff --git a/app/src/main/java/ch/protonmail/android/api/models/messages/send/MessageSendResponse.java b/app/src/main/java/ch/protonmail/android/api/models/messages/send/MessageSendResponse.java index e718b0b9f..fe61f4296 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/messages/send/MessageSendResponse.java +++ b/app/src/main/java/ch/protonmail/android/api/models/messages/send/MessageSendResponse.java @@ -20,20 +20,19 @@ import ch.protonmail.android.api.models.ResponseBody; import ch.protonmail.android.api.models.messages.receive.AttachmentFactory; -import ch.protonmail.android.api.models.messages.receive.IMessageFactory; -import ch.protonmail.android.api.models.messages.receive.IMessageSenderFactory; import ch.protonmail.android.api.models.messages.receive.MessageFactory; import ch.protonmail.android.api.models.messages.receive.MessageSenderFactory; import ch.protonmail.android.api.models.messages.receive.ServerMessage; import ch.protonmail.android.api.models.room.messages.Message; + public class MessageSendResponse extends ResponseBody { private ServerMessage Sent; private MessageParent Parent; public Message getSent() { final AttachmentFactory attachmentFactory = new AttachmentFactory(); - IMessageSenderFactory messageSenderFactory = new MessageSenderFactory(); - final IMessageFactory messageFactory = new MessageFactory(attachmentFactory,messageSenderFactory); + MessageSenderFactory messageSenderFactory = new MessageSenderFactory(); + final MessageFactory messageFactory = new MessageFactory(attachmentFactory, messageSenderFactory); return messageFactory.createMessage(Sent); } diff --git a/app/src/main/java/ch/protonmail/android/di/ApplicationModule.kt b/app/src/main/java/ch/protonmail/android/di/ApplicationModule.kt index ad42ee909..d0136f04b 100644 --- a/app/src/main/java/ch/protonmail/android/di/ApplicationModule.kt +++ b/app/src/main/java/ch/protonmail/android/di/ApplicationModule.kt @@ -35,8 +35,6 @@ import ch.protonmail.android.api.models.doh.Proxies import ch.protonmail.android.api.models.factories.IConverterFactory import ch.protonmail.android.api.models.messages.receive.AttachmentFactory import ch.protonmail.android.api.models.messages.receive.IAttachmentFactory -import ch.protonmail.android.api.models.messages.receive.IMessageSenderFactory -import ch.protonmail.android.api.models.messages.receive.MessageSenderFactory import ch.protonmail.android.api.models.messages.receive.ServerLabel import ch.protonmail.android.api.models.room.contacts.ContactLabel import ch.protonmail.android.attachments.Armorer @@ -212,9 +210,6 @@ object ApplicationModule { @Provides fun providesArmorer(): Armorer = OpenPgpArmorer() - @Provides - fun messageSenderFactory(): IMessageSenderFactory = MessageSenderFactory() - @Provides fun attachmentFactory(): IAttachmentFactory = AttachmentFactory() From fd0b8c4b25ec71b9ba18496ebfe31ddb2f26f23e Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Wed, 13 Jan 2021 19:42:47 +0100 Subject: [PATCH 079/145] Use StringResourceResolver to getString instead of appContext MAILAND-1304 --- .../compose/ComposeMessageViewModel.kt | 12 ++--- .../utils/resources/StringResourceResolver.kt | 31 +++++++++++ .../compose/ComposeMessageViewModelTest.kt | 51 ++++++++++++++++++- 3 files changed, 84 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/ch/protonmail/android/utils/resources/StringResourceResolver.kt diff --git a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt index c7ccd9947..3269c7a3a 100644 --- a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt +++ b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt @@ -61,6 +61,7 @@ import ch.protonmail.android.usecase.model.FetchPublicKeysResult import ch.protonmail.android.utils.Event import ch.protonmail.android.utils.MessageUtils import ch.protonmail.android.utils.UiUtil +import ch.protonmail.android.utils.resources.StringResourceResolver import ch.protonmail.android.viewmodel.ConnectivityBaseViewModel import com.squareup.otto.Subscribe import io.reactivex.Observable @@ -92,6 +93,7 @@ class ComposeMessageViewModel @Inject constructor( private val fetchPublicKeys: FetchPublicKeys, private val saveDraft: SaveDraft, private val dispatchers: DispatcherProvider, + private val stringResourceResolver: StringResourceResolver, verifyConnection: VerifyConnection, networkConfigurator: NetworkConfigurator ) : ConnectivityBaseViewModel(verifyConnection, networkConfigurator) { @@ -449,14 +451,13 @@ class ComposeMessageViewModel @Inject constructor( when (saveDraftResult) { is SaveDraftResult.Success -> onDraftSaved(saveDraftResult.draftId) SaveDraftResult.OnlineDraftCreationFailed -> { - val errorMessage = getStringResource( + val errorMessage = stringResourceResolver( R.string.failed_saving_draft_online ).format(message.subject) _savingDraftError.postValue(errorMessage) } SaveDraftResult.UploadDraftAttachmentsFailed -> { - val attachmentFailed = R.string.attachment_failed - val errorMessage = getStringResource(attachmentFailed) + message.subject + val errorMessage = stringResourceResolver(R.string.attachment_failed) + message.subject _savingDraftError.postValue(errorMessage) } SaveDraftResult.SendingInProgressError -> { @@ -473,11 +474,6 @@ class ComposeMessageViewModel @Inject constructor( } } - private fun getStringResource(stringId: Int): String { - val context = ProtonMailApplication.getApplication().applicationContext - return context.getString(stringId) - } - private suspend fun onDraftSaved(savedDraftId: String) { val draft = requireNotNull(messageDetailsRepository.findMessageById(savedDraftId)) diff --git a/app/src/main/java/ch/protonmail/android/utils/resources/StringResourceResolver.kt b/app/src/main/java/ch/protonmail/android/utils/resources/StringResourceResolver.kt new file mode 100644 index 000000000..dcb6c0307 --- /dev/null +++ b/app/src/main/java/ch/protonmail/android/utils/resources/StringResourceResolver.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2020 Proton Technologies AG + * + * This file is part of ProtonMail. + * + * ProtonMail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ProtonMail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with ProtonMail. If not, see https://www.gnu.org/licenses/. + */ + +package ch.protonmail.android.utils.resources + +import android.content.Context +import androidx.annotation.StringRes +import javax.inject.Inject + +class StringResourceResolver @Inject constructor( + private val context: Context +) { + + operator fun invoke(@StringRes resId: Int): String = context.getString(resId) +} diff --git a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt index 60516e9d4..ab2f36c60 100644 --- a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt +++ b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt @@ -20,6 +20,7 @@ package ch.protonmail.android.compose import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import ch.protonmail.android.R import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository import ch.protonmail.android.api.NetworkConfigurator import ch.protonmail.android.api.models.room.messages.Message @@ -34,6 +35,7 @@ import ch.protonmail.android.usecase.compose.SaveDraftResult import ch.protonmail.android.usecase.delete.DeleteMessage import ch.protonmail.android.usecase.fetch.FetchPublicKeys import io.mockk.MockKAnnotations +import ch.protonmail.android.utils.resources.StringResourceResolver import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every @@ -56,6 +58,9 @@ class ComposeMessageViewModelTest : CoroutinesTest { @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() + @RelaxedMockK + private lateinit var stringResourceResolver: StringResourceResolver + @RelaxedMockK lateinit var composeMessageRepository: ComposeMessageRepository @@ -124,7 +129,7 @@ class ComposeMessageViewModelTest : CoroutinesTest { val message = Message() val createdDraftId = "newDraftId" val createdDraft = Message(messageId = createdDraftId, localId = "local28348") - val testObserver = viewModel.savingDraftComplete.testObserver() + val savedDraftObserver = viewModel.savingDraftComplete.testObserver() givenViewModelPropertiesAreInitialised() coEvery { saveDraft(any()) } returns flowOf(SaveDraftResult.Success(createdDraftId)) coEvery { messageDetailsRepository.findMessageById(createdDraftId) } returns createdDraft @@ -133,7 +138,7 @@ class ComposeMessageViewModelTest : CoroutinesTest { viewModel.saveDraft(message, hasConnectivity = false) coEvery { messageDetailsRepository.findMessageById(createdDraftId) } - assertEquals(createdDraft, testObserver.observedValues[0]) + assertEquals(createdDraft, savedDraftObserver.observedValues[0]) } } @@ -180,6 +185,48 @@ class ComposeMessageViewModelTest : CoroutinesTest { } } + @Test + fun saveDraftResolvesLocalisedErrorMessageAndPostsOnLiveDataWhenSaveDraftUseCaseFailsCreatingTheDraft() { + runBlockingTest { + // Given + val messageSubject = "subject" + val message = Message(subject = messageSubject) + val saveDraftErrorObserver = viewModel.savingDraftError.testObserver() + val errorResId = R.string.failed_saving_draft_online + givenViewModelPropertiesAreInitialised() + coEvery { saveDraft(any()) } returns flowOf(SaveDraftResult.OnlineDraftCreationFailed) + every { stringResourceResolver.invoke(errorResId) } returns "Error creating draft for message %s" + + // When + viewModel.saveDraft(message, hasConnectivity = true) + + val expectedError = "Error creating draft for message $messageSubject" + coEvery { stringResourceResolver.invoke(errorResId) } + assertEquals(expectedError, saveDraftErrorObserver.observedValues[0]) + } + } + + @Test + fun saveDraftResolvesLocalisedErrorMessageAndPostsOnLiveDataWhenSaveDraftUseCaseFailsUploadingAttachments() { + runBlockingTest { + // Given + val messageSubject = "subject" + val message = Message(subject = messageSubject) + val saveDraftErrorObserver = viewModel.savingDraftError.testObserver() + val errorResId = R.string.attachment_failed + givenViewModelPropertiesAreInitialised() + coEvery { saveDraft(any()) } returns flowOf(SaveDraftResult.UploadDraftAttachmentsFailed) + every { stringResourceResolver.invoke(errorResId) } returns "Error uploading attachments for subject " + + // When + viewModel.saveDraft(message, hasConnectivity = true) + + val expectedError = "Error uploading attachments for subject $messageSubject" + coEvery { stringResourceResolver.invoke(errorResId) } + assertEquals(expectedError, saveDraftErrorObserver.observedValues[0]) + } + } + private fun givenViewModelPropertiesAreInitialised() { // Needed to set class fields to the right value and allow code under test to get executed viewModel.prepareMessageData(false, "addressId", "mail-alias", false) From bfe4529020426fc58f1447e2f59676df29115ecc Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Thu, 14 Jan 2021 15:31:50 +0100 Subject: [PATCH 080/145] Removing unused isDirty field from ComposeEditText MAILAND-1304 --- .../composeMessage/ComposeMessageActivity.java | 1 - .../ch/protonmail/android/views/ComposeEditText.java | 12 ------------ 2 files changed, 13 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java b/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java index 638b62619..6ff9c38cc 100644 --- a/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java +++ b/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java @@ -1108,7 +1108,6 @@ private void showDraftDialog() { getString(R.string.cancel), unit -> { composeMessageViewModel.deleteDraft(); - mComposeBodyEditText.setIsDirty(false); finishActivity(); return unit; }, diff --git a/app/src/main/java/ch/protonmail/android/views/ComposeEditText.java b/app/src/main/java/ch/protonmail/android/views/ComposeEditText.java index c950cd6b2..08895895d 100644 --- a/app/src/main/java/ch/protonmail/android/views/ComposeEditText.java +++ b/app/src/main/java/ch/protonmail/android/views/ComposeEditText.java @@ -28,9 +28,6 @@ public class ComposeEditText extends EditText { - private boolean mIsDirty = false; - private boolean isDirty; - public ComposeEditText(Context context, AttributeSet attrs) { super(context, attrs); } @@ -47,17 +44,8 @@ public boolean onTextContextMenuItem(int id) { case android.R.id.cut: case android.R.id.paste: case android.R.id.copy: - mIsDirty = true; break; } return consumed; } - - public boolean isIsDirty() { - return mIsDirty; - } - - public void setIsDirty(boolean isDirty) { - mIsDirty = isDirty; - } } From 9abbe7cc05cce7a4d0b75aeda031f1d1b076f06b Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Fri, 15 Jan 2021 18:15:03 +0100 Subject: [PATCH 081/145] Implement auto saving drafts when user stops typing for 1 second MAILAND-1304 Test existing auto save job is cancelled before scheduling new one This ensures save draft is only performed after one second of inactivity from the user --- .../ComposeMessageActivity.java | 3 +- .../compose/ComposeMessageViewModel.kt | 40 ++++++++---- .../compose/ComposeMessageViewModelTest.kt | 64 ++++++++++++++++++- 3 files changed, 94 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java b/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java index 6ff9c38cc..64524c62b 100644 --- a/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java +++ b/app/src/main/java/ch/protonmail/android/activities/composeMessage/ComposeMessageActivity.java @@ -750,13 +750,14 @@ public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { + public void onTextChanged(CharSequence text, int start, int before, int count) { if (skipInitial < 2) { skipInitial++; return; } skipInitial++; composeMessageViewModel.setIsDirty(true); + composeMessageViewModel.autoSaveDraft(text.toString()); } @Override diff --git a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt index 3269c7a3a..fc372be2b 100644 --- a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt +++ b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt @@ -66,8 +66,9 @@ import ch.protonmail.android.viewmodel.ConnectivityBaseViewModel import com.squareup.otto.Subscribe import io.reactivex.Observable import io.reactivex.Single -import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -78,6 +79,7 @@ import java.util.UUID import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject import kotlin.collections.set +import kotlin.time.seconds const val NEW_LINE = "
" const val LESS_THAN = "<" @@ -215,6 +217,8 @@ class ComposeMessageViewModel @Inject constructor( } val parentId: String? get() = _parentId + + internal var autoSaveJob: Job? = null // endregion private val loggedInUsernames = if (userManager.user.combinedContacts) { @@ -434,7 +438,7 @@ class ComposeMessageViewModel @Inject constructor( message.numAttachments = listOfAttachments.size saveMessage(message) newAttachmentIds = filterUploadedAttachments( - composeMessageRepository.createAttachmentList(_messageDataResult.attachmentList, IO), + composeMessageRepository.createAttachmentList(_messageDataResult.attachmentList, dispatchers.Io), uploadAttachments ) } @@ -491,7 +495,7 @@ class ComposeMessageViewModel @Inject constructor( // we need to compare them and find out which are new attachments if (uploadAttachments && localAttachmentsList.isNotEmpty()) { newAttachments = filterUploadedAttachments( - composeMessageRepository.createAttachmentList(localAttachmentsList, IO), uploadAttachments + composeMessageRepository.createAttachmentList(localAttachmentsList, dispatchers.Io), uploadAttachments ) } val currentAttachmentsList = messageDataResult.attachmentList @@ -570,8 +574,8 @@ class ComposeMessageViewModel @Inject constructor( private fun buildMessage() { viewModelScope.launch { var message: Message? = null - if (!TextUtils.isEmpty(draftId)) { - message = composeMessageRepository.findMessage(draftId, IO) + if (draftId.isNotEmpty()) { + message = composeMessageRepository.findMessage(draftId, dispatchers.Io) } if (message != null) { _draftId.set(message.messageId) @@ -599,7 +603,10 @@ class ComposeMessageViewModel @Inject constructor( } } _messageDataResult.attachmentList.addAll(listLocalAttachmentsAlreadySavedInDb) - val newAttachments = composeMessageRepository.createAttachmentList(_messageDataResult.attachmentList, IO) + val newAttachments = composeMessageRepository.createAttachmentList( + _messageDataResult.attachmentList, + dispatchers.Io + ) message.setAttachmentList(newAttachments) // endregion @@ -650,10 +657,10 @@ class ComposeMessageViewModel @Inject constructor( viewModelScope.launch { if (draftId.isNotEmpty()) { - val message = composeMessageRepository.findMessage(draftId, IO) + val message = composeMessageRepository.findMessage(draftId, dispatchers.Io) if (message != null) { - val messageAttachments = composeMessageRepository.getAttachments(message, _messageDataResult.isTransient, IO) + val messageAttachments = composeMessageRepository.getAttachments(message, _messageDataResult.isTransient, dispatchers.Io) if (oldList.size <= messageAttachments.size) { val attachments = LocalAttachment.createLocalAttachmentList(messageAttachments) _messageDataResult = MessageBuilderData.Builder() @@ -703,7 +710,7 @@ class ComposeMessageViewModel @Inject constructor( } else { // this will ensure the message get latest message id if it was already saved in a create/update draft job // and also that the message has all the latest edits in between draft saving (creation) and sending the message - val savedMessage = messageDetailsRepository.findMessageByMessageDbId(_dbId!!, IO) + val savedMessage = messageDetailsRepository.findMessageByMessageDbId(_dbId!!, dispatchers.Io) message.dbId = _dbId savedMessage?.let { if (!TextUtils.isEmpty(it.localId)) { @@ -738,7 +745,7 @@ class ComposeMessageViewModel @Inject constructor( fun createLocalAttachments(loadedMessage: Message) { viewModelScope.launch { - val messageAttachments = composeMessageRepository.getAttachments(loadedMessage, _messageDataResult.isTransient, IO) + val messageAttachments = composeMessageRepository.getAttachments(loadedMessage, _messageDataResult.isTransient, dispatchers.Io) val localAttachments = LocalAttachment.createLocalAttachmentList(messageAttachments).toMutableList() _messageDataResult = MessageBuilderData.Builder() .fromOld(_messageDataResult) @@ -1133,7 +1140,7 @@ class ComposeMessageViewModel @Inject constructor( } val fromHtmlMobileSignature = UiUtil.fromHtml(_messageDataResult.mobileSignature) - if (!TextUtils.isEmpty(fromHtmlMobileSignature)) { + if (fromHtmlMobileSignature.isNotEmpty()) { content = content.replace(fromHtmlMobileSignature.toString(), _messageDataResult.mobileSignature) } @@ -1227,4 +1234,15 @@ class ComposeMessageViewModel @Inject constructor( setBeforeSaveDraft(false, messageDataResult.content, UserAction.SAVE_DRAFT) } } + + fun autoSaveDraft(messageBody: String) { + Timber.v("Draft auto save scheduled!") + + autoSaveJob?.cancel() + autoSaveJob = viewModelScope.launch(dispatchers.Io) { + delay(1.seconds) + Timber.d("Draft auto save triggered") + setBeforeSaveDraft(true, messageBody) + } + } } diff --git a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt index ab2f36c60..2ed56b7e2 100644 --- a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt +++ b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt @@ -34,21 +34,26 @@ import ch.protonmail.android.usecase.compose.SaveDraft import ch.protonmail.android.usecase.compose.SaveDraftResult import ch.protonmail.android.usecase.delete.DeleteMessage import ch.protonmail.android.usecase.fetch.FetchPublicKeys -import io.mockk.MockKAnnotations +import ch.protonmail.android.utils.UiUtil import ch.protonmail.android.utils.resources.StringResourceResolver +import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.InjectMockKs import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.RelaxedMockK +import io.mockk.mockk +import io.mockk.mockkStatic import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runBlockingTest import me.proton.core.test.kotlin.CoroutinesTest import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue import org.junit.Rule import kotlin.test.BeforeTest import kotlin.test.Test +import kotlin.test.assertNotNull class ComposeMessageViewModelTest : CoroutinesTest { @@ -227,6 +232,63 @@ class ComposeMessageViewModelTest : CoroutinesTest { } } + @Test + fun autoSaveDraftSchedulesJobToPerformSaveDraftAfterSomeDelay() { + runBlockingTest(dispatchers.Io) { + // Given + val messageBody = "Message body being edited..." + val messageId = "draft8237472" + val message = Message(messageId, subject = "A subject") + val buildMessageObserver = viewModel.buildingMessageCompleted.testObserver() + givenViewModelPropertiesAreInitialised() + // message was already saved once (we're updating) + viewModel.draftId = messageId + mockkStatic(UiUtil::class) + every { UiUtil.toHtml(messageBody) } returns " $messageBody " + every { UiUtil.fromHtml(any()) } returns mockk(relaxed = true) + coEvery { composeMessageRepository.findMessage(messageId, dispatchers.Io) } returns message + coEvery { composeMessageRepository.createAttachmentList(any(), dispatchers.Io) } returns emptyList() + + // When + viewModel.autoSaveDraft(messageBody) + viewModel.autoSaveJob?.join() + + // Then + val expectedMessage = message.copy() + assertEquals(expectedMessage, buildMessageObserver.observedValues[0]?.peekContent()) + assertEquals("<html> Message body being edited... <html>", viewModel.messageDataResult.content) + } + } + + @Test + fun autoSaveDraftCancelsExistingJobBeforeSchedulingANewOneWhenCalledTwice() { + runBlockingTest(dispatchers.Io) { + // Given + val messageBody = "Message body being edited again..." + val messageId = "draft923823" + val message = Message(messageId, subject = "Another subject") + viewModel.buildingMessageCompleted.testObserver() + givenViewModelPropertiesAreInitialised() + // message was already saved once (we're updating) + viewModel.draftId = messageId + mockkStatic(UiUtil::class) + every { UiUtil.toHtml(messageBody) } returns " $messageBody " + every { UiUtil.fromHtml(any()) } returns mockk(relaxed = true) + coEvery { composeMessageRepository.findMessage(messageId, dispatchers.Io) } returns message + coEvery { composeMessageRepository.createAttachmentList(any(), dispatchers.Io) } returns emptyList() + + // When + viewModel.autoSaveDraft(messageBody) + assertNotNull(viewModel.autoSaveJob) + val firstScheduledJob = viewModel.autoSaveJob + viewModel.autoSaveDraft(messageBody) + + // Then + assertTrue(firstScheduledJob?.isCancelled ?: false) + assertTrue(viewModel.autoSaveJob?.isActive ?: false) + } + } + private fun givenViewModelPropertiesAreInitialised() { // Needed to set class fields to the right value and allow code under test to get executed viewModel.prepareMessageData(false, "addressId", "mail-alias", false) From 356fbe38a0fc2ba40b4222f0dc08f918d29442b1 Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Tue, 26 Jan 2021 10:51:21 +0100 Subject: [PATCH 082/145] Change erroneus coEvery to coVerify and add unmockkStatic Also change RuntimeException to more granular IllegalArgumentException in MessageFactory MAILAND-1304 --- .../api/models/messages/receive/MessageFactory.kt | 2 +- .../android/compose/ComposeMessageViewModelTest.kt | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageFactory.kt b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageFactory.kt index 32e2ddbc8..6ac55b009 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageFactory.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageFactory.kt @@ -134,7 +134,7 @@ class MessageFactory @Inject constructor( val numOfAttachments = message.numAttachments val attachmentsListSize = message.Attachments.size if (attachmentsListSize != 0 && attachmentsListSize != numOfAttachments) - throw RuntimeException( + throw IllegalArgumentException( "Attachments size does not match expected: $numOfAttachments, actual: $attachmentsListSize " ) message diff --git a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt index 2ed56b7e2..a5badd660 100644 --- a/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt +++ b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt @@ -45,6 +45,7 @@ import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.RelaxedMockK import io.mockk.mockk import io.mockk.mockkStatic +import io.mockk.unmockkStatic import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runBlockingTest import me.proton.core.test.kotlin.CoroutinesTest @@ -142,7 +143,7 @@ class ComposeMessageViewModelTest : CoroutinesTest { // When viewModel.saveDraft(message, hasConnectivity = false) - coEvery { messageDetailsRepository.findMessageById(createdDraftId) } + coVerify { messageDetailsRepository.findMessageById(createdDraftId) } assertEquals(createdDraft, savedDraftObserver.observedValues[0]) } } @@ -206,7 +207,7 @@ class ComposeMessageViewModelTest : CoroutinesTest { viewModel.saveDraft(message, hasConnectivity = true) val expectedError = "Error creating draft for message $messageSubject" - coEvery { stringResourceResolver.invoke(errorResId) } + coVerify { stringResourceResolver.invoke(errorResId) } assertEquals(expectedError, saveDraftErrorObserver.observedValues[0]) } } @@ -227,7 +228,7 @@ class ComposeMessageViewModelTest : CoroutinesTest { viewModel.saveDraft(message, hasConnectivity = true) val expectedError = "Error uploading attachments for subject $messageSubject" - coEvery { stringResourceResolver.invoke(errorResId) } + coVerify { stringResourceResolver.invoke(errorResId) } assertEquals(expectedError, saveDraftErrorObserver.observedValues[0]) } } @@ -257,6 +258,7 @@ class ComposeMessageViewModelTest : CoroutinesTest { val expectedMessage = message.copy() assertEquals(expectedMessage, buildMessageObserver.observedValues[0]?.peekContent()) assertEquals("<html> Message body being edited... <html>", viewModel.messageDataResult.content) + unmockkStatic(UiUtil::class) } } @@ -286,6 +288,7 @@ class ComposeMessageViewModelTest : CoroutinesTest { // Then assertTrue(firstScheduledJob?.isCancelled ?: false) assertTrue(viewModel.autoSaveJob?.isActive ?: false) + unmockkStatic(UiUtil::class) } } From 13ff2ff2a2817144d49e6fbaf8e30ff249281a9f Mon Sep 17 00:00:00 2001 From: Marino Meneghel Date: Fri, 22 Jan 2021 10:31:50 +0100 Subject: [PATCH 083/145] Revert "Convert message recipient class to kotlin" This reverts commit ae103b1b16f1e65abdd4bad84e7bcbc0d8383c71. --- .idea/codeStyles/Project.xml | 15 +- .../api/models/MessageRecipientMatcher.kt | 58 +++++ .../ComposeMessageActivity.java | 18 +- .../messageDetails/IntentExtrasData.kt | 6 +- .../messageDetails/MessagePrinter.kt | 4 +- .../adapters/MessageRecipientViewAdapter.java | 4 +- .../android/api/models/MessagePayload.kt | 3 +- .../android/api/models/MessageRecipient.java | 210 ++++++++++++++++++ .../android/api/models/MessageRecipient.kt | 119 ---------- .../api/models/factories/PackageFactory.java | 6 +- .../compose/ComposeMessageViewModel.kt | 6 +- .../recipients/GroupRecipientViewHolder.kt | 2 +- .../GroupRecipientsDialogFragment.kt | 4 +- .../recipients/GroupRecipientsViewModel.kt | 16 +- .../groups/list/ContactGroupsFragment.kt | 2 +- .../protonmail/android/utils/MessageUtils.kt | 6 +- .../android/views/MessageRecipientView.java | 30 +-- .../MessageDetailsRecipientsLayout.kt | 8 +- 18 files changed, 337 insertions(+), 180 deletions(-) create mode 100644 app/src/androidTest/java/ch/protonmail/android/api/models/MessageRecipientMatcher.kt create mode 100644 app/src/main/java/ch/protonmail/android/api/models/MessageRecipient.java delete mode 100644 app/src/main/java/ch/protonmail/android/api/models/MessageRecipient.kt diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index f5dc0e87e..fdfb8c30a 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -9,9 +9,18 @@ +