diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/jetpack/workmanager/EmailAndNameWorkerTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/jetpack/workmanager/EmailAndNameWorkerTest.kt new file mode 100644 index 0000000000..dd67bca95f --- /dev/null +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/jetpack/workmanager/EmailAndNameWorkerTest.kt @@ -0,0 +1,94 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: DenBond7 + */ + +package com.flowcrypt.email.jetpack.workmanager + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.work.ListenableWorker +import androidx.work.testing.TestListenableWorkerBuilder +import androidx.work.workDataOf +import com.flowcrypt.email.database.FlowCryptRoomDatabase +import com.flowcrypt.email.database.entity.RecipientEntity +import kotlinx.coroutines.runBlocking +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.`is` +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +/** + * @author Denis Bondarenko + * Date: 5/30/22 + * Time: 2:36 PM + * E-mail: DenBond7@gmail.com + */ +@RunWith(AndroidJUnit4::class) +class EmailAndNameWorkerTest { + private lateinit var context: Context + + @Before + fun setUp() { + context = ApplicationProvider.getApplicationContext() + } + + @Test + fun testUpdateExistingRecipient() { + val recipientEmail = "user@flowcrypt.test" + val recipientName = "Bob Alisa" + val recipientDao = FlowCryptRoomDatabase.getDatabase(context).recipientDao() + val worker = TestListenableWorkerBuilder( + context = context, + inputData = workDataOf( + EmailAndNameWorker.EXTRA_KEY_EMAILS to arrayOf(recipientEmail), + EmailAndNameWorker.EXTRA_KEY_NAMES to arrayOf(recipientName) + ) + ).build() + runBlocking { + recipientDao.insertSuspend(RecipientEntity(email = recipientEmail)) + val existingUserBeforeUpdating = recipientDao.getRecipientByEmail(recipientEmail) + assertNotNull(existingUserBeforeUpdating) + //check that a user have no name before updating + assertNull(existingUserBeforeUpdating?.name) + val result = worker.doWork() + assertThat(result, `is`(ListenableWorker.Result.success())) + //check that a user have some name after updating + val existingUserAfterUpdating = recipientDao.getRecipientByEmail(recipientEmail) + assertNotNull(existingUserAfterUpdating) + assertEquals(recipientName, existingUserAfterUpdating?.name) + } + } + + @Test + fun testCreateNewRecipient() { + val recipientEmail = "user@flowcrypt.test" + val recipientName = "Bob Alisa" + val recipientDao = FlowCryptRoomDatabase.getDatabase(context).recipientDao() + val worker = TestListenableWorkerBuilder( + context = context, + inputData = workDataOf( + EmailAndNameWorker.EXTRA_KEY_EMAILS to arrayOf(recipientEmail), + EmailAndNameWorker.EXTRA_KEY_NAMES to arrayOf(recipientName) + ) + ).build() + runBlocking { + //check that a user is not exist + val existingUserBeforeUpdating = recipientDao.getRecipientByEmail(recipientEmail) + assertNull(existingUserBeforeUpdating) + + val result = worker.doWork() + assertThat(result, `is`(ListenableWorker.Result.success())) + + //check that a new user was added + val existingUserAfterUpdating = recipientDao.getRecipientByEmail(recipientEmail) + assertNotNull(existingUserAfterUpdating) + assertEquals(recipientName, existingUserAfterUpdating?.name) + } + } +} diff --git a/FlowCrypt/src/main/AndroidManifest.xml b/FlowCrypt/src/main/AndroidManifest.xml index b91352a980..c91fcf1d18 100644 --- a/FlowCrypt/src/main/AndroidManifest.xml +++ b/FlowCrypt/src/main/AndroidManifest.xml @@ -143,11 +143,6 @@ android:name=".service.PassPhrasesInRAMService" android:exported="false" /> - - diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt index 90a304f511..219b40b9bc 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt @@ -32,9 +32,8 @@ import com.flowcrypt.email.database.entity.AttachmentEntity import com.flowcrypt.email.database.entity.LabelEntity import com.flowcrypt.email.database.entity.MessageEntity import com.flowcrypt.email.extensions.kotlin.toHex +import com.flowcrypt.email.jetpack.workmanager.EmailAndNameWorker import com.flowcrypt.email.jetpack.workmanager.sync.CheckIsLoadedMessagesEncryptedWorker -import com.flowcrypt.email.model.EmailAndNamePair -import com.flowcrypt.email.service.EmailAndNameUpdaterService import com.flowcrypt.email.service.MessagesNotificationManager import com.flowcrypt.email.util.FileAndDirectoryUtils import com.flowcrypt.email.util.GeneralUtil @@ -576,12 +575,12 @@ class MessagesViewModel(application: Application) : AccountViewModel(application val isSentFolder = imapFolder?.attributes?.contains("\\Sent") ?: true if (isSentFolder) { - val emailAndNamePairs = ArrayList() + val emailAndNamePairs = mutableListOf>() for (message in messages) { emailAndNamePairs.addAll(getEmailAndNamePairs(message)) } - EmailAndNameUpdaterService.enqueueWork(getApplication(), emailAndNamePairs) + EmailAndNameWorker.enqueue(getApplication(), emailAndNamePairs) } } catch (e: MessagingException) { e.printStackTrace() @@ -590,23 +589,23 @@ class MessagesViewModel(application: Application) : AccountViewModel(application } /** - * Generate a list of [EmailAndNamePair] objects from the input message. + * Generate a list of [Pair] objects from the input message. * This information will be retrieved from "to" and "cc" headers. * - * @param msg The input [javax.mail.Message]. - * @return [List] of EmailAndNamePair objects, which contains information + * @param msg The input [jakarta.mail.Message]. + * @return [List] of [Pair] objects, which contains information * about * emails and names. * @throws MessagingException when retrieve information about recipients. */ - private fun getEmailAndNamePairs(msg: Message): List { - val pairs = ArrayList() + private fun getEmailAndNamePairs(msg: Message): List> { + val pairs = mutableListOf>() val addressesTo = msg.getRecipients(Message.RecipientType.TO) if (addressesTo != null) { for (address in addressesTo) { val internetAddress = address as InternetAddress - pairs.add(EmailAndNamePair(internetAddress.address, internetAddress.personal)) + pairs.add(Pair(internetAddress.address, internetAddress.personal)) } } @@ -614,7 +613,7 @@ class MessagesViewModel(application: Application) : AccountViewModel(application if (addressesCC != null) { for (address in addressesCC) { val internetAddress = address as InternetAddress - pairs.add(EmailAndNamePair(internetAddress.address, internetAddress.personal)) + pairs.add(Pair(internetAddress.address, internetAddress.personal)) } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/EmailAndNameWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/EmailAndNameWorker.kt new file mode 100644 index 0000000000..ed3bdafdcd --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/EmailAndNameWorker.kt @@ -0,0 +1,87 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: denbond7 + */ + +package com.flowcrypt.email.jetpack.workmanager + +import android.content.Context +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import androidx.work.workDataOf +import com.flowcrypt.email.BuildConfig +import com.flowcrypt.email.database.FlowCryptRoomDatabase +import com.flowcrypt.email.database.dao.RecipientDao +import com.flowcrypt.email.database.entity.RecipientEntity +import com.flowcrypt.email.jetpack.workmanager.sync.BaseSyncWorker + +/** + * This service updates a name of some email entry or creates a new email entry if it not exist. + * + * Used a next logic: + * * if email in db: + * * if db_row.name is null and bool(name) == true: + * "save that person's name into the existing DB record" + * + * * else: + * "save that email, name pair into DB like so: new RecipientEntity(email, name);" + * + * @author DenBond7 + * Date: 22.05.2017 + * Time: 22:25 + * E-mail: DenBond7@gmail.com + */ +class EmailAndNameWorker(context: Context, params: WorkerParameters) : BaseWorker(context, params) { + private var recipientDao: RecipientDao = FlowCryptRoomDatabase.getDatabase(context).recipientDao() + + override suspend fun doWork(): Result { + val emails = inputData.getStringArray(EXTRA_KEY_EMAILS) ?: return Result.failure() + val names = inputData.getStringArray(EXTRA_KEY_NAMES) ?: return Result.failure() + + if (emails.size != names.size) return Result.failure() + + for (i in emails.indices) { + val email = emails[i].lowercase() + val name = names[i] + val recipientEntity = recipientDao.getRecipientByEmailSuspend(email) + if (recipientEntity != null) { + if (recipientEntity.name.isNullOrEmpty()) { + recipientDao.updateSuspend(recipientEntity.copy(name = name)) + } + } else { + recipientDao.insertSuspend(RecipientEntity(email = email, name = name)) + } + } + + return Result.success() + } + + companion object { + const val EXTRA_KEY_EMAILS = BuildConfig.APPLICATION_ID + ".EXTRA_KEY_EMAILS" + const val EXTRA_KEY_NAMES = BuildConfig.APPLICATION_ID + ".EXTRA_KEY_NAMES" + const val GROUP_UNIQUE_TAG = BuildConfig.APPLICATION_ID + ".UPDATE_EMAIL_AND_NAME" + + fun enqueue(context: Context, emailAndNamePairs: Collection>) { + val emails = emailAndNamePairs.map { it.first } + val names = emailAndNamePairs.map { it.second } + + WorkManager + .getInstance(context.applicationContext) + .enqueueUniqueWork( + GROUP_UNIQUE_TAG, + ExistingWorkPolicy.APPEND, + OneTimeWorkRequestBuilder() + .addTag(BaseSyncWorker.TAG_SYNC) + .setInputData( + workDataOf( + EXTRA_KEY_EMAILS to emails.toTypedArray(), + EXTRA_KEY_NAMES to names.toTypedArray() + ) + ) + .build() + ) + } + } +} diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/LoadRecipientsWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/LoadRecipientsWorker.kt index f0204422cd..83ea56eac8 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/LoadRecipientsWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/LoadRecipientsWorker.kt @@ -21,7 +21,6 @@ import com.flowcrypt.email.api.email.gmail.api.GmaiAPIMimeMessage import com.flowcrypt.email.database.FlowCryptRoomDatabase import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.database.entity.RecipientEntity -import com.flowcrypt.email.model.EmailAndNamePair import com.sun.mail.imap.IMAPFolder import jakarta.mail.FetchProfile import jakarta.mail.Folder @@ -152,7 +151,7 @@ class LoadRecipientsWorker(context: Context, params: WorkerParameters) : } private suspend fun updateContacts(msgs: Array) = withContext(Dispatchers.IO) { - val emailAndNamePairs = mutableListOf() + val emailAndNamePairs = mutableListOf>() for (msg in msgs) { emailAndNamePairs.addAll(parseRecipients(msg, Message.RecipientType.TO)) emailAndNamePairs.addAll(parseRecipients(msg, Message.RecipientType.CC)) @@ -176,22 +175,23 @@ class LoadRecipientsWorker(context: Context, params: WorkerParameters) : } for (emailAndNamePair in emailAndNamePairs) { - if (recipientsInDatabase.contains(emailAndNamePair.email)) { - val recipientEntity = recipientsByEmailMap[emailAndNamePair.email] + if (recipientsInDatabase.contains(emailAndNamePair.first)) { + val recipientEntity = recipientsByEmailMap[emailAndNamePair.first] if (recipientEntity?.email.isNullOrEmpty()) { - if (!recipientsToUpdate.contains(emailAndNamePair.email)) { - emailAndNamePair.email?.let { - recipientsToUpdate.add(it) - } - recipientEntity?.copy(name = emailAndNamePair.name)?.let { updateCandidates.add(it) } + if (!recipientsToUpdate.contains(emailAndNamePair.first)) { + recipientsToUpdate.add(emailAndNamePair.first) + recipientEntity?.copy(name = emailAndNamePair.second)?.let { updateCandidates.add(it) } } } } else { - if (!recipientsToCreate.contains(emailAndNamePair.email)) { - emailAndNamePair.email?.let { - recipientsToCreate.add(it) - newCandidates.add(RecipientEntity(email = it, name = emailAndNamePair.name)) - } + if (!recipientsToCreate.contains(emailAndNamePair.first)) { + recipientsToCreate.add(emailAndNamePair.first) + newCandidates.add( + RecipientEntity( + email = emailAndNamePair.first, + name = emailAndNamePair.second + ) + ) } } } @@ -201,30 +201,26 @@ class LoadRecipientsWorker(context: Context, params: WorkerParameters) : } /** - * Generate an array of [EmailAndNamePair] objects from the input message. + * Generate an array of [Pair] objects from the input message. * This information will be retrieved from "to", "cc" or "bcc" headers. * * @param msg The input [Message]. * @param recipientType The input [Message.RecipientType]. - * @return An array of EmailAndNamePair objects, which contains information about emails and names. + * @return An array of [Pair] objects, which contains information about emails and names. */ private suspend fun parseRecipients( msg: Message?, recipientType: Message.RecipientType? - ): List = withContext(Dispatchers.IO) { + ): List> = withContext(Dispatchers.IO) { if (msg != null && recipientType != null) { try { val header = msg.getHeader(recipientType.toString()) ?: return@withContext emptyList() if (header.isNotEmpty()) { if (!TextUtils.isEmpty(header[0])) { val addresses = InternetAddress.parse(header[0]) - val emailAndNamePairs = mutableListOf() + val emailAndNamePairs = mutableListOf>() for (address in addresses) { - emailAndNamePairs.add( - EmailAndNamePair( - address.address.lowercase(), address.personal - ) - ) + emailAndNamePairs.add(Pair(address.address.lowercase(), address.personal)) } return@withContext emailAndNamePairs diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jobscheduler/JobIdManager.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jobscheduler/JobIdManager.kt index 1e8c3d746d..db32095df4 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jobscheduler/JobIdManager.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jobscheduler/JobIdManager.kt @@ -22,9 +22,7 @@ class JobIdManager { */ const val JOB_MAX_ID = 10 - const val JOB_TYPE_SYNC = 1 const val JOB_TYPE_ACTION_QUEUE = 2 - const val JOB_TYPE_EMAIL_AND_NAME_UPDATE = 3 const val JOB_TYPE_PREPARE_OUT_GOING_MESSAGE = 4 const val JOB_TYPE_FEEDBACK = 5 } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/model/EmailAndNamePair.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/model/EmailAndNamePair.kt deleted file mode 100644 index f9f4067cdf..0000000000 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/model/EmailAndNamePair.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 - */ - -package com.flowcrypt.email.model - -import android.os.Parcel -import android.os.Parcelable - -/** - * This class describes a pair of email and name. This is a simple POJO object. - * - * @author DenBond7 - * Date: 22.05.2017 - * Time: 22:31 - * E-mail: DenBond7@gmail.com - */ -data class EmailAndNamePair constructor(val email: String? = null, val name: String? = null) : - Parcelable { - constructor(source: Parcel) : this( - source.readString(), - source.readString() - ) - - override fun describeContents(): Int { - return 0 - } - - override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) { - writeString(email) - writeString(name) - } - - companion object { - @JvmField - val CREATOR: Parcelable.Creator = - object : Parcelable.Creator { - override fun createFromParcel(source: Parcel): EmailAndNamePair = EmailAndNamePair(source) - override fun newArray(size: Int): Array = arrayOfNulls(size) - } - } -} diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/service/EmailAndNameUpdaterService.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/service/EmailAndNameUpdaterService.kt deleted file mode 100644 index 5c24b8aa04..0000000000 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/service/EmailAndNameUpdaterService.kt +++ /dev/null @@ -1,85 +0,0 @@ -/* - * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 - */ - -package com.flowcrypt.email.service - -import android.content.Context -import android.content.Intent -import androidx.core.app.JobIntentService -import com.flowcrypt.email.BuildConfig -import com.flowcrypt.email.database.FlowCryptRoomDatabase -import com.flowcrypt.email.database.dao.RecipientDao -import com.flowcrypt.email.database.entity.RecipientEntity -import com.flowcrypt.email.jobscheduler.JobIdManager -import com.flowcrypt.email.model.EmailAndNamePair -import java.util.ArrayList - -/** - * This service does update a name of some email entry or creates a new email entry if it not - * exists. - * - * - * Used a next logic: - * - * * if email in db: - * - * * if db_row.name is null and bool(name) == true: - * "save that person's name into the existing DB record" - * - * * else: - * "save that email, name pair into DB like so: new RecipientEntity(email, name);" - * - * - * @author DenBond7 - * Date: 22.05.2017 - * Time: 22:25 - * E-mail: DenBond7@gmail.com - */ -class EmailAndNameUpdaterService : JobIntentService() { - private var recipientDao: RecipientDao = FlowCryptRoomDatabase.getDatabase(this).recipientDao() - - override fun onHandleWork(intent: Intent) { - val pairs = - intent.getParcelableArrayListExtra(EXTRA_KEY_LIST_OF_PAIRS_EMAIL_NAME) - ?: return - - for (pair in pairs) { - val email = pair.email?.lowercase() ?: continue - val recipientEntity = recipientDao.getRecipientByEmail(email) - if (recipientEntity != null) { - if (recipientEntity.name.isNullOrEmpty()) { - recipientDao.update(recipientEntity.copy(name = pair.name)) - } - } else { - recipientDao.insert(RecipientEntity(email = email, name = pair.name)) - } - } - } - - companion object { - private const val EXTRA_KEY_LIST_OF_PAIRS_EMAIL_NAME = - BuildConfig.APPLICATION_ID + ".EXTRA_KEY_LIST_OF_PAIRS_EMAIL_NAME" - - /** - * Enqueue a new task for [EmailAndNameUpdaterService]. - * - * @param context Interface to global information about an application environment. - * @param emailAndNamePairs A list of EmailAndNamePair objects. - */ - fun enqueueWork(context: Context, emailAndNamePairs: ArrayList?) { - if (emailAndNamePairs != null && emailAndNamePairs.isNotEmpty()) { - val intent = Intent(context, EmailAndNameUpdaterService::class.java) - intent.putExtra(EXTRA_KEY_LIST_OF_PAIRS_EMAIL_NAME, emailAndNamePairs) - - enqueueWork( - context, - EmailAndNameUpdaterService::class.java, - JobIdManager.JOB_TYPE_EMAIL_AND_NAME_UPDATE, - intent - ) - } - } - } -}