diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3a3fb56ce..f031db1fe 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -206,7 +206,7 @@ unit tests release: --device model=Pixel2,version=28 --test-targets "class ch.protonmail.android.uitests.tests.suites.SmokeSuite" --use-orchestrator - --num-flaky-test-attempts=2 + --num-flaky-test-attempts=1 --timeout 30m firebase tests: @@ -258,7 +258,7 @@ firebase feature tests: - echo $SERVICE_ACCOUNT_MAIL > /tmp/service-account.json - gcloud auth activate-service-account --key-file /tmp/service-account.json - export CLOUDSDK_CORE_DISABLE_PROMPTS=1 - - gcloud --quiet firebase test android run + - gcloud beta --quiet firebase test android run --app app/build/outputs/apk/beta/debug/ProtonMail-Android-${VERSION_NAME}-beta-debug.apk --test app/build/outputs/apk/androidTest/beta/debug/ProtonMail-Android-${VERSION_NAME}-beta-debug-androidTest.apk --device model=$MODEL,version=$API_LEVEL @@ -266,6 +266,7 @@ firebase feature tests: --use-orchestrator --num-flaky-test-attempts=1 --timeout 45m + --client-details testType=$TEST_TYPE,testSuite=$TEST_CLASS,commitBranch=$CI_COMMIT_BRANCH,gitlabJobUrl=$CI_JOB_URL include: - project: 'translations/generator' diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index fdfb8c30a..d3c601529 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -145,4 +145,4 @@ - + \ No newline at end of file diff --git a/app/src/androidTest/java/ch/protonmail/android/api/models/room/ContactGroupsDatabaseTest.kt b/app/src/androidTest/java/ch/protonmail/android/api/models/room/ContactGroupsDatabaseTest.kt index 379fecc97..1b5becd86 100644 --- a/app/src/androidTest/java/ch/protonmail/android/api/models/room/ContactGroupsDatabaseTest.kt +++ b/app/src/androidTest/java/ch/protonmail/android/api/models/room/ContactGroupsDatabaseTest.kt @@ -25,10 +25,12 @@ import ch.protonmail.android.api.models.room.contacts.ContactEmail import ch.protonmail.android.api.models.room.contacts.ContactEmailContactLabelJoin import ch.protonmail.android.api.models.room.contacts.ContactLabel import ch.protonmail.android.api.models.room.contacts.ContactsDatabaseFactory +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runBlockingTest import org.junit.Assert import org.junit.Rule +import java.util.Arrays import kotlin.test.Test -import java.util.* /** * Created by Dino Kadrikj on 13.08.2018. @@ -51,7 +53,7 @@ class ContactGroupsDatabaseTest { database.saveContactGroupLabel(label2) database.saveContactGroupLabel(label3) - val needed = database.findContactGroupById("b") + val needed = database.findContactGroupByIdBlocking("b") Assert.assertEquals(label2, needed) } @@ -62,7 +64,7 @@ class ContactGroupsDatabaseTest { database.saveContactGroupLabel(label1) database.updateFullContactGroup(label2) - val needed = database.findContactGroupById("a") + val needed = database.findContactGroupByIdBlocking("a") Assert.assertEquals(needed?.name, "ab") } @@ -74,8 +76,8 @@ class ContactGroupsDatabaseTest { database.saveAllContactGroups(label1, label2) database.updateFullContactGroup(label3) - val neededUpdated = database.findContactGroupById("a") - val neededNotUpdated = database.findContactGroupById("b") + val neededUpdated = database.findContactGroupByIdBlocking("a") + val neededNotUpdated = database.findContactGroupByIdBlocking("b") Assert.assertEquals(neededUpdated?.name, "ab") Assert.assertEquals(neededNotUpdated?.name, "bb") } @@ -87,7 +89,7 @@ class ContactGroupsDatabaseTest { database.saveContactGroupLabel(label1) database.updatePartially(label2) - val needed = database.findContactGroupById("a") + val needed = database.findContactGroupByIdBlocking("a") Assert.assertEquals(needed?.color, "aaa") Assert.assertEquals(needed?.name, "ab") Assert.assertEquals(needed?.order, 1) @@ -113,7 +115,7 @@ class ContactGroupsDatabaseTest { database.saveContactGroupLabel(label2) database.saveContactGroupLabel(label3) - val actual = database.findContactGroupById("a") + val actual = database.findContactGroupByIdBlocking("a") Assert.assertEquals(label3, actual) } @@ -125,7 +127,7 @@ class ContactGroupsDatabaseTest { database.saveAllContactGroups(label1, label2, label3) val sizeAfterInsert = database.findContactGroupsLiveData().testValue?.size Assert.assertEquals(sizeAfterInsert, 3) - database.clearContactGroupsLabelsTable() + database.clearContactGroupsLabelsTableBlocking() val sizeAfterClearing = database.findContactGroupsLiveData().testValue?.size Assert.assertEquals(sizeAfterClearing, 0) } @@ -165,7 +167,7 @@ class ContactGroupsDatabaseTest { val email5 = ContactEmail("e5", "5@5.5", "e", labelIds = listOf("lb")) val email6 = ContactEmail("e6", "6@6.6", "f", labelIds = listOf("ld")) - database.saveAllContactsEmails(email1, email2, email3, email4, email5, email6) + database.saveAllContactsEmailsBlocking(email1, email2, email3, email4, email5, email6) database.saveAllContactGroups(label1, label2, label3, label4) val contactEmailContactLabel1 = ContactEmailContactLabelJoin("e1", "la") @@ -178,13 +180,13 @@ class ContactGroupsDatabaseTest { val contactEmailContactLabel8 = ContactEmailContactLabelJoin("e5", "lb") val contactEmailContactLabel9 = ContactEmailContactLabelJoin("e6", "ld") - database.saveContactEmailContactLabel(contactEmailContactLabel1, contactEmailContactLabel2, contactEmailContactLabel3, - contactEmailContactLabel4, contactEmailContactLabel5, contactEmailContactLabel6, contactEmailContactLabel7, - contactEmailContactLabel8, contactEmailContactLabel9) - val laCount = database.countContactEmailsByLabelId("la") - val lbCount = database.countContactEmailsByLabelId("lb") - val lcCount = database.countContactEmailsByLabelId("lc") - val ldCount = database.countContactEmailsByLabelId("ld") + database.saveContactEmailContactLabelBlocking(contactEmailContactLabel1, contactEmailContactLabel2, contactEmailContactLabel3, + contactEmailContactLabel4, contactEmailContactLabel5, contactEmailContactLabel6, contactEmailContactLabel7, + contactEmailContactLabel8, contactEmailContactLabel9) + val laCount = database.countContactEmailsByLabelIdBlocking("la") + val lbCount = database.countContactEmailsByLabelIdBlocking("lb") + val lcCount = database.countContactEmailsByLabelIdBlocking("lc") + val ldCount = database.countContactEmailsByLabelIdBlocking("ld") Assert.assertEquals(3, laCount) Assert.assertEquals(2, lbCount) Assert.assertEquals(3, lcCount) @@ -201,7 +203,7 @@ class ContactGroupsDatabaseTest { val email4 = ContactEmail("e4", "4@4.4", labelIds = listOf("lb"), name = "ce3") val email5 = ContactEmail("e5", "5@5.5", labelIds = listOf("lb", "la"), name = "ce4") - database.saveAllContactsEmails(email1, email2, email4, email5) + database.saveAllContactsEmailsBlocking(email1, email2, email4, email5) database.saveAllContactGroups(label1, label2) val contactEmailContactLabel1 = ContactEmailContactLabelJoin("e1", "la") @@ -211,8 +213,8 @@ class ContactGroupsDatabaseTest { val contactEmailContactLabel7 = ContactEmailContactLabelJoin("e5", "lb") val contactEmailContactLabel8 = ContactEmailContactLabelJoin("e5", "la") - database.saveContactEmailContactLabel(contactEmailContactLabel1, contactEmailContactLabel2, - contactEmailContactLabel3, contactEmailContactLabel4, contactEmailContactLabel7, contactEmailContactLabel8) + database.saveContactEmailContactLabelBlocking(contactEmailContactLabel1, contactEmailContactLabel2, + contactEmailContactLabel3, contactEmailContactLabel4, contactEmailContactLabel7, contactEmailContactLabel8) val laReturnedEmails = database.findAllContactsEmailsByContactGroupAsync("la").testValue val lbReturnedEmails = database.findAllContactsEmailsByContactGroupAsync("lb").testValue val lcReturnedEmails = database.findAllContactsEmailsByContactGroupAsync("lc").testValue @@ -233,7 +235,7 @@ class ContactGroupsDatabaseTest { val email4 = ContactEmail("e3", "3@3.3", labelIds = listOf("lb"), name = "ce3") val email5 = ContactEmail("e4", "4@3.4", labelIds = listOf("lb", "la"), name = "ce4") - database.saveAllContactsEmails(email1, email2, email4, email5) + database.saveAllContactsEmailsBlocking(email1, email2, email4, email5) database.saveAllContactGroups(label1, label2) val contactEmailContactLabel1 = ContactEmailContactLabelJoin("e1", "la") @@ -243,18 +245,20 @@ class ContactGroupsDatabaseTest { val contactEmailContactLabel7 = ContactEmailContactLabelJoin("e4", "lb") val contactEmailContactLabel8 = ContactEmailContactLabelJoin("e4", "la") - database.saveContactEmailContactLabel(contactEmailContactLabel1, contactEmailContactLabel2, - contactEmailContactLabel3, contactEmailContactLabel4, contactEmailContactLabel7, contactEmailContactLabel8) - var result = database.filterContactsEmailsByContactGroup("la", "%2%") - - Assert.assertNotNull(result) - Assert.assertEquals(1, result.size) - Assert.assertEquals(listOf(email2), result) - - result = database.filterContactsEmailsByContactGroup("lb", "%3%") - Assert.assertNotNull(result) - Assert.assertEquals(2, result.size) - Assert.assertEquals(listOf(email4, email5), result) + database.saveContactEmailContactLabelBlocking(contactEmailContactLabel1, contactEmailContactLabel2, + contactEmailContactLabel3, contactEmailContactLabel4, contactEmailContactLabel7, contactEmailContactLabel8) + runBlockingTest { + val result = database.filterContactsEmailsByContactGroup("la", "%2%").first() + + Assert.assertNotNull(result) + Assert.assertEquals(1, result.size) + Assert.assertEquals(listOf(email2), result) + + val result2 = database.filterContactsEmailsByContactGroup("lb", "%3%").first() + Assert.assertNotNull(result2) + Assert.assertEquals(2, result2.size) + Assert.assertEquals(listOf(email4, email5), result2) + } } @Test @@ -267,7 +271,7 @@ class ContactGroupsDatabaseTest { val email4 = ContactEmail("e4", "4@4.4", labelIds = listOf("lb"), name = "ce3") val email5 = ContactEmail("e5", "5@5.5", labelIds = listOf("lb", "la"), name = "ce4") - database.saveAllContactsEmails(email1, email2, email4, email5) + database.saveAllContactsEmailsBlocking(email1, email2, email4, email5) database.saveAllContactGroups(label1, label2) val contactEmailContactLabel1 = ContactEmailContactLabelJoin("e1", "la") @@ -277,7 +281,7 @@ class ContactGroupsDatabaseTest { val contactEmailContactLabel7 = ContactEmailContactLabelJoin("e5", "lb") val contactEmailContactLabel8 = ContactEmailContactLabelJoin("e5", "la") - database.saveContactEmailContactLabel(contactEmailContactLabel1, contactEmailContactLabel2, + database.saveContactEmailContactLabelBlocking(contactEmailContactLabel1, contactEmailContactLabel2, contactEmailContactLabel3, contactEmailContactLabel4, contactEmailContactLabel7, contactEmailContactLabel8) val laReturnedEmails = database.findAllContactGroupsByContactEmailAsync("e1").testValue diff --git a/app/src/androidTest/java/ch/protonmail/android/api/models/room/contacts/ContactsDatabaseTest.kt b/app/src/androidTest/java/ch/protonmail/android/api/models/room/contacts/ContactsDatabaseTest.kt index 88e24111c..687734707 100644 --- a/app/src/androidTest/java/ch/protonmail/android/api/models/room/contacts/ContactsDatabaseTest.kt +++ b/app/src/androidTest/java/ch/protonmail/android/api/models/room/contacts/ContactsDatabaseTest.kt @@ -24,6 +24,7 @@ import androidx.test.InstrumentationRegistry import ch.protonmail.android.api.models.ContactEncryptedData import ch.protonmail.android.api.models.room.testValue import ch.protonmail.android.core.Constants +import kotlinx.coroutines.runBlocking import org.hamcrest.Matchers.`is` import org.junit.Assert import org.junit.Ignore @@ -31,475 +32,584 @@ import org.junit.Rule import org.junit.Test import kotlin.test.BeforeTest -/** - * Created by Kamil Rajtar on 06.09.18. */ internal class ContactsDatabaseTest { - private val context = InstrumentationRegistry.getTargetContext() - private var databaseFactory = Room.inMemoryDatabaseBuilder(context, ContactsDatabaseFactory::class.java).build() - private var database = databaseFactory.getDatabase() - private val initiallyEmptyDatabase = databaseFactory.getDatabase() - @get:Rule - var instantTaskExecutorRule = InstantTaskExecutorRule() - - private val contactData=listOf( - ContactData(contactId = "aa",name = "aaa").apply {dbId=2}, - ContactData(contactId = "bb",name = "bbb").apply {dbId=4}, - ContactData(contactId = "cc",name = "ccc").apply {dbId=3}, - ContactData(contactId = "dd",name = "ddd").apply {dbId=1}, - ContactData(contactId = "ee",name = "eee").apply {dbId=7} - ) - - - private val contactEmails=listOf( - ContactEmail(contactEmailId = "a",email = "a@a.com",contactId = "aa",labelIds = listOf("aaa","aaaa","aaaaa"), name = "ce1"), - ContactEmail(contactEmailId = "b",email = "b@b.com",contactId = "bb",labelIds = listOf("bbb","bbbb","bbbbb"), name = "ce2"), - ContactEmail(contactEmailId = "c",email = "c@c.com",contactId = "bb",labelIds = listOf("ccc","cccc","ccccc"), name = "ce3"), - ContactEmail(contactEmailId = "d",email = "b@b.com",contactId = "dd",labelIds = listOf("ddd","dddd","ddddd"), name = "ce4"), - ContactEmail(contactEmailId = "e",email = "e@e.com",contactId = "ee",labelIds = listOf("eee","eeee","eeeee"), name = "ce5") - ) - - private val fullContactDetails=listOf( - FullContactDetails(contactId="a", - name="aa", - uid="aaa", - createTime=1, - modifyTime=1, - size=5, - defaults=3, - encryptedData=mutableListOf(ContactEncryptedData("aaaa","aaaaa", Constants.VCardType.SIGNED), - ContactEncryptedData("aaaaaa","aaaaaaa", Constants.VCardType.SIGNED_ENCRYPTED))), - FullContactDetails(contactId="b", - name="bb", - uid="bbb", - createTime=5, - modifyTime=7, - size=12, - defaults=2, - encryptedData=mutableListOf(ContactEncryptedData("bbbb","bbbbb", Constants.VCardType.SIGNED), - ContactEncryptedData("bbbbbb","bbbbbbb", Constants.VCardType.SIGNED_ENCRYPTED))), - FullContactDetails(contactId="c", - name="cc", - uid="ccc", - createTime=12, - modifyTime=1100, - size=2, - defaults=123, - encryptedData=mutableListOf(ContactEncryptedData("cccc","ccccc", Constants.VCardType.SIGNED), - ContactEncryptedData("cccccc","ccccccc", Constants.VCardType.SIGNED_ENCRYPTED))), - FullContactDetails(contactId="d", - name="dd", - uid="ddd", - createTime=3, - modifyTime=12, - size=112, - defaults=31, - encryptedData=mutableListOf(ContactEncryptedData("dddd","ddddd", Constants.VCardType.SIGNED), - ContactEncryptedData("dddddd","ddddddd", Constants.VCardType.SIGNED_ENCRYPTED))), - FullContactDetails(contactId="e", - name="ee", - uid="eee", - createTime=1, - modifyTime=131, - size=12, - defaults=321, - encryptedData=mutableListOf(ContactEncryptedData("eeee","eeeee", Constants.VCardType.SIGNED), - ContactEncryptedData("eeeeee","eeeeeee", Constants.VCardType.SIGNED_ENCRYPTED))), - FullContactDetails("f") - ) - - private fun ContactsDatabase.populate(){ - saveAllContactsData(contactData) - saveAllContactsEmails(contactEmails) - fullContactDetails.forEach(this::insertFullContactDetails) - } - - private fun assertDatabaseState(expectedContactData:Iterable =contactData, - expectedContactEmails:Iterable =contactEmails, - expectedFullContactDetails:Iterable =fullContactDetails) { - val expectedContactDataSet = expectedContactData.toSet() - val expectedContactEmailsSet = expectedContactEmails.toSet() - //hack as encrypted data has equals not defined - val expectedFullContactDetailsSet = expectedFullContactDetails.map { it.apply { encryptedData = mutableListOf() } }.toSet() - - val actualContactDataSet = database.findAllContactDataAsync().testValue!!.toSet() - val actualContactEmailsSet = database.findAllContactsEmailsAsync().testValue!!.toSet() - //hack as encrypted data has equals not defined - val actualFullContactDetailsSet = expectedFullContactDetails.map(FullContactDetails::contactId).map(database::findFullContactDetailsById).map { it?.apply { encryptedData = mutableListOf() } }.toSet() - - Assert.assertEquals(expectedContactDataSet, actualContactDataSet) - Assert.assertEquals(expectedContactEmailsSet, actualContactEmailsSet) - Assert.assertEquals(expectedFullContactDetailsSet, actualFullContactDetailsSet) - } - - @BeforeTest - fun setUp() { - database.populate() - } - - @Test - fun findContactDataById() { - val expected = contactData[3] - val actual = database.findContactDataById(expected.contactId!!) - Assert.assertEquals(expected, actual) - assertDatabaseState() - } - - @Test - fun findContactDataByDbId() { - val expected = contactData[3] - val actual = database.findContactDataByDbId(expected.dbId!!) - Assert.assertEquals(expected, actual) - assertDatabaseState() - } - - @Test - fun findAllContactDataAsync() { - val expected=contactData - val actual=database.findAllContactDataAsync().testValue - Assert.assertEquals(expected,actual) - assertDatabaseState() - } - - @Test - fun clearContactDataCache() { - database.clearContactDataCache() - assertDatabaseState(expectedContactData = emptyList()) - } - - @Test - fun saveContactData() { - val inserted=ContactData("z","zz") - val expected=contactData+inserted - database.saveContactData(inserted) - assertDatabaseState(expectedContactData = expected) - } - - @Test - fun saveAllContactsData() { - val inserted=listOf(ContactData("y","yy"),ContactData("z","zz")) - val expected=contactData+inserted - database.saveAllContactsData(inserted) - assertDatabaseState(expectedContactData=expected) - } - - @Test - fun saveAllContactsData1() { - val inserted=listOf(ContactData("y","yy"),ContactData("z","zz")) - val expected=contactData+inserted - database.saveAllContactsData(*inserted.toTypedArray()) - assertDatabaseState(expectedContactData=expected) - } - - @Test - fun deleteContactData() { - val deleted=contactData[3] - val expected=contactData-deleted - database.deleteContactData(deleted) - assertDatabaseState(expectedContactData=expected) - } - - @Test - fun deleteContactsData() { - val deleted=listOf(contactData[3],contactData[1]) - val expected=contactData-deleted - database.deleteContactsData(deleted) - assertDatabaseState(expectedContactData=expected) - } - - @Test - fun findContactEmailById() { - val expected=contactEmails[2] - val actual=database.findContactEmailById(expected.contactEmailId!!) - Assert.assertEquals(expected,actual) - assertDatabaseState() - } - - @Test - fun findContactEmailByEmail() { - val expected=contactEmails[2] - val actual=database.findContactEmailByEmail(expected.email) - Assert.assertEquals(expected,actual) - assertDatabaseState() - } - - @Test - fun findContactEmailsByContactId() { - val contactId=contactEmails[2].contactId!! - val expected=contactEmails.filter {it.contactId==contactId} - val actual=database.findContactEmailsByContactId(contactId) - Assert.assertEquals(expected,actual) - assertDatabaseState() - } - - @Test - fun findAllContactsEmailsAsync() { - val expected=contactEmails.toSet() - val actual=database.findAllContactsEmailsAsync().testValue?.toSet() - Assert.assertEquals(expected,actual) - assertDatabaseState() - } - - @Ignore("Implement with contacts groups") - @Test - fun findAllContactsEmailsByContactGroupAsync() { - TODO() - assertDatabaseState() - } - - @Test - fun clearByEmail() { - val deletedEmail=contactEmails[1].email - val expected=contactEmails.filterNot {it.email==deletedEmail} - database.clearByEmail(deletedEmail) - assertDatabaseState(expectedContactEmails = expected) - } - - @Test - fun clearContactEmailsCache() { - val expected=emptyList() - database.clearContactEmailsCache() - assertDatabaseState(expectedContactEmails=expected) - } - - @Test - fun deleteContactEmail() { - val deleted=listOf( contactEmails[3],contactEmails[1]) - val expected=contactEmails-deleted - database.deleteContactEmail(*deleted.toTypedArray()) - assertDatabaseState(expectedContactEmails=expected) - } - - @Test - fun deleteAllContactsEmails() { - val deleted=listOf(contactEmails[3],contactEmails[1]) - val expected=contactEmails-deleted - database.deleteAllContactsEmails(deleted) - assertDatabaseState(expectedContactEmails=expected) - } - - @Test - fun saveContactEmail() { - val inserted=ContactEmail("z","z@z.com",contactId = "zzz",labelIds = listOf("zzzz","zzzzz","zzzzzz"), name = "ce1") - val expected=contactEmails+inserted - database.saveContactEmail(inserted) - assertDatabaseState(expectedContactEmails = expected) - } - - @Test - fun saveAllContactsEmails() { - val inserted=listOf(ContactEmail("y", - "y@y.com", - contactId="yyy", - labelIds=listOf("yyyy","yyyyy","yyyyyy"), name = "ce1"),ContactEmail("z", - "z@z.com", - contactId="zzz", - labelIds=listOf("zzzz","zzzzz","zzzzzz"), name = "ce2")) - val expected=contactEmails+inserted - database.saveAllContactsEmails(inserted) - assertDatabaseState(expectedContactEmails=expected) - } - - @Test - fun saveAllContactsEmails1() { - val inserted=listOf(ContactEmail("y", - "y@y.com", - contactId="yyy", - labelIds=listOf("yyyy","yyyyy","yyyyyy"), name = "ce1"),ContactEmail("z", - "z@z.com", - contactId="zzz", - labelIds=listOf("zzzz","zzzzz","zzzzzz"), name = "ce2")) - val expected=contactEmails+inserted - database.saveAllContactsEmails(*inserted.toTypedArray()) - assertDatabaseState(expectedContactEmails=expected) - } - - @Ignore("Implement with contacts groups") - @Test - fun countContactEmails() { - TODO() - assertDatabaseState() - } - - @Ignore("Implement with contacts groups") - @Test - fun findContactGroupById() { - TODO() - assertDatabaseState() - } - - @Ignore("Implement with contacts groups") - @Test - fun findContactGroupByIdAsync() { - TODO() - assertDatabaseState() - } - - @Ignore("Implement with contacts groups") - @Test - fun findContactGroupsLiveData() { - TODO() - assertDatabaseState() - } - - @Ignore("Implement with contacts groups") - @Test - fun findContactGroupsObservable() { - TODO() - assertDatabaseState() - } - - @Ignore("Implement with contacts groups") - @Test - fun clearContactGroupsLabelsTable() { - TODO() - assertDatabaseState() - } - - @Ignore("Implement with contacts groups") - @Test - fun saveContactGroupLabel() { - TODO() - assertDatabaseState() - } - - @Ignore("Implement with contacts groups") - @Test - fun updateFullContactGroup() { - TODO() - assertDatabaseState() - } - - @Ignore("Implement with contacts groups") - @Test - fun saveAllContactGroups() { - TODO() - assertDatabaseState() - } - - @Ignore("Implement with contacts groups") - @Test - fun clearContactGroupsList() { - TODO() - assertDatabaseState() - } - - @Ignore("Implement with contacts groups") - @Test - fun saveContactGroupsList() { - TODO() - assertDatabaseState() - } - - @Ignore("Implement with contacts groups") - @Test - fun deleteByContactGroupLabelId() { - TODO() - assertDatabaseState() - } - - @Ignore("Implement with contacts groups") - @Test - fun deleteContactGroup() { - TODO() - assertDatabaseState() - } - - @Ignore("Implement with contacts groups") - @Test - fun getAllContactGroupsByIds() { - TODO() - assertDatabaseState() - } - - @Ignore("Implement with contacts groups") - @Test - fun updatePartially() { - TODO() - assertDatabaseState() - } - - @Test - fun insertFullContactDetails() { - val inserted=FullContactDetails(contactId="z", - name="zz", - uid="zzz", - createTime=1, - modifyTime=131, - size=12, - defaults=321, - encryptedData=mutableListOf(ContactEncryptedData("zzzz","zzzzz", Constants.VCardType.SIGNED), - ContactEncryptedData("zzzzzz","zzzzzzz", Constants.VCardType.SIGNED_ENCRYPTED))) - val expected=fullContactDetails+inserted - database.insertFullContactDetails(inserted) - assertDatabaseState(expectedFullContactDetails = expected) - } - - @Test - fun findFullContactDetailsById() { - val expected=fullContactDetails[1] - val actual=database.findFullContactDetailsById(expected.contactId) - Assert.assertThat(actual,`is`(FullContactsDetailsMatcher(expected))) - assertDatabaseState() - } - - @Test - fun clearFullContactDetailsCache() { - val expected=emptyList() - database.clearFullContactDetailsCache() - assertDatabaseState(expectedFullContactDetails =expected) - } - - @Test - fun deleteFullContactsDetails() { - val deleted=fullContactDetails[1] - val expected=fullContactDetails-deleted - database.deleteFullContactsDetails(deleted) - val found=database.findFullContactDetailsById(deleted.contactId) - Assert.assertNull(found) - assertDatabaseState(expectedFullContactDetails=expected) - } - - @Ignore("Implement with contacts groups") - @Test - fun countContactEmailsByLabelId() { - TODO() - assertDatabaseState() - } - - @Ignore("Implement with contacts groups") - @Test - fun saveContactEmailContactLabel() { - TODO() - assertDatabaseState() - } - - @Ignore("Implement with contacts groups") - @Test - fun saveContactEmailContactLabel1() { - TODO() - assertDatabaseState() - } - - @Ignore("Implement with contacts groups") - @Test - fun saveContactEmailContactLabel2() { - TODO() - assertDatabaseState() - } - - - @Test - fun testContactEmailsConverter() { - val email1=ContactEmail("e1","1@1.1","a",labelIds=listOf("la","lc")) - val email2=ContactEmail("e2","2@2.2","b",labelIds=listOf("la","lc")) - val email3=ContactEmail("e3","3@3.3","c",labelIds=listOf("la","lc")) - initiallyEmptyDatabase.saveAllContactsEmails(email1,email2,email3) - val emailFromDb=initiallyEmptyDatabase.findContactEmailById("e1") - Assert.assertNotNull(emailFromDb) - val listOfGroups=emailFromDb?.labelIds - Assert.assertNotNull(listOfGroups) - val expectedGroupId="la" - Assert.assertEquals(expectedGroupId,listOfGroups?.get(0)) - } + private val context = InstrumentationRegistry.getTargetContext() + private var databaseFactory = Room.inMemoryDatabaseBuilder(context, ContactsDatabaseFactory::class.java).build() + private var database = databaseFactory.getDatabase() + private val initiallyEmptyDatabase = databaseFactory.getDatabase() + + @get:Rule + var instantTaskExecutorRule = InstantTaskExecutorRule() + + private val contactData = listOf( + ContactData(contactId = "aa", name = "aaa").apply { dbId = 2 }, + ContactData(contactId = "bb", name = "bbb").apply { dbId = 4 }, + ContactData(contactId = "cc", name = "ccc").apply { dbId = 3 }, + ContactData(contactId = "dd", name = "ddd").apply { dbId = 1 }, + ContactData(contactId = "ee", name = "eee").apply { dbId = 7 } + ) + + + private val contactEmails = listOf( + ContactEmail( + contactEmailId = "a", + email = "a@a.com", + contactId = "aa", + labelIds = listOf("aaa", "aaaa", "aaaaa"), + name = "ce1" + ), + ContactEmail( + contactEmailId = "b", + email = "b@b.com", + contactId = "bb", + labelIds = listOf("bbb", "bbbb", "bbbbb"), + name = "ce2" + ), + ContactEmail( + contactEmailId = "c", + email = "c@c.com", + contactId = "bb", + labelIds = listOf("ccc", "cccc", "ccccc"), + name = "ce3" + ), + ContactEmail( + contactEmailId = "d", + email = "b@b.com", + contactId = "dd", + labelIds = listOf("ddd", "dddd", "ddddd"), + name = "ce4" + ), + ContactEmail( + contactEmailId = "e", + email = "e@e.com", + contactId = "ee", + labelIds = listOf("eee", "eeee", "eeeee"), + name = "ce5" + ) + ) + + private val fullContactDetails = listOf( + FullContactDetails( + contactId = "a", + name = "aa", + uid = "aaa", + createTime = 1, + modifyTime = 1, + size = 5, + defaults = 3, + encryptedData = mutableListOf( + ContactEncryptedData("aaaa", "aaaaa", Constants.VCardType.SIGNED), + ContactEncryptedData("aaaaaa", "aaaaaaa", Constants.VCardType.SIGNED_ENCRYPTED) + ) + ), + FullContactDetails( + contactId = "b", + name = "bb", + uid = "bbb", + createTime = 5, + modifyTime = 7, + size = 12, + defaults = 2, + encryptedData = mutableListOf( + ContactEncryptedData("bbbb", "bbbbb", Constants.VCardType.SIGNED), + ContactEncryptedData("bbbbbb", "bbbbbbb", Constants.VCardType.SIGNED_ENCRYPTED) + ) + ), + FullContactDetails( + contactId = "c", + name = "cc", + uid = "ccc", + createTime = 12, + modifyTime = 1100, + size = 2, + defaults = 123, + encryptedData = mutableListOf( + ContactEncryptedData("cccc", "ccccc", Constants.VCardType.SIGNED), + ContactEncryptedData("cccccc", "ccccccc", Constants.VCardType.SIGNED_ENCRYPTED) + ) + ), + FullContactDetails( + contactId = "d", + name = "dd", + uid = "ddd", + createTime = 3, + modifyTime = 12, + size = 112, + defaults = 31, + encryptedData = mutableListOf( + ContactEncryptedData("dddd", "ddddd", Constants.VCardType.SIGNED), + ContactEncryptedData("dddddd", "ddddddd", Constants.VCardType.SIGNED_ENCRYPTED) + ) + ), + FullContactDetails( + contactId = "e", + name = "ee", + uid = "eee", + createTime = 1, + modifyTime = 131, + size = 12, + defaults = 321, + encryptedData = mutableListOf( + ContactEncryptedData("eeee", "eeeee", Constants.VCardType.SIGNED), + ContactEncryptedData("eeeeee", "eeeeeee", Constants.VCardType.SIGNED_ENCRYPTED) + ) + ), + FullContactDetails("f") + ) + + private fun ContactsDatabase.populate() { + runBlocking { + saveAllContactsData(contactData) + saveAllContactsEmails(contactEmails) + } + fullContactDetails.forEach(this::insertFullContactDetails) + } + + private fun assertDatabaseState( + expectedContactData: Iterable = contactData, + expectedContactEmails: Iterable = contactEmails, + expectedFullContactDetails: Iterable = fullContactDetails + ) { + val expectedContactDataSet = expectedContactData.toSet() + val expectedContactEmailsSet = expectedContactEmails.toSet() + //hack as encrypted data has equals not defined + val expectedFullContactDetailsSet = expectedFullContactDetails + .map { it.apply { encryptedData = mutableListOf() } }.toSet() + + val actualContactDataSet = database.findAllContactDataAsync().testValue!!.toSet() + val actualContactEmailsSet = database.findAllContactsEmailsAsync().testValue!!.toSet() + //hack as encrypted data has equals not defined + val actualFullContactDetailsSet = expectedFullContactDetails + .map(FullContactDetails::contactId) + .map(database::findFullContactDetailsById) + .map { it?.apply { encryptedData = mutableListOf() } } + .toSet() + + Assert.assertEquals(expectedContactDataSet, actualContactDataSet) + Assert.assertEquals(expectedContactEmailsSet, actualContactEmailsSet) + Assert.assertEquals(expectedFullContactDetailsSet, actualFullContactDetailsSet) + } + + @BeforeTest + fun setUp() { + database.populate() + } + + @Test + fun findContactDataById() { + val expected = contactData[3] + val actual = database.findContactDataById(expected.contactId!!) + Assert.assertEquals(expected, actual) + assertDatabaseState() + } + + @Test + fun findContactDataByDbId() { + val expected = contactData[3] + val actual = database.findContactDataByDbId(expected.dbId!!) + Assert.assertEquals(expected, actual) + assertDatabaseState() + } + + @Test + fun findAllContactDataAsync() { + val expected = contactData + val actual = database.findAllContactDataAsync().testValue + Assert.assertEquals(expected, actual) + assertDatabaseState() + } + + @Test + fun clearContactDataCache() { + database.clearContactDataCache() + assertDatabaseState(expectedContactData = emptyList()) + } + + @Test + fun saveContactData() { + val inserted = ContactData("z", "zz") + val expected = contactData + inserted + database.saveContactData(inserted) + assertDatabaseState(expectedContactData = expected) + } + + @Test + fun saveAllContactsData() { + runBlocking { + val inserted = listOf( + ContactData("y", "yy"), ContactData("z", "zz") + ) + val expected = contactData + inserted + database.saveAllContactsData(inserted) + assertDatabaseState(expectedContactData = expected) + } + } + + @Test + fun saveAllContactsData1() { + val inserted = listOf( + ContactData("y", "yy"), ContactData("z", "zz") + ) + val expected = contactData + inserted + database.saveAllContactsData(*inserted.toTypedArray()) + assertDatabaseState(expectedContactData = expected) + } + + @Test + fun deleteContactData() { + val deleted = contactData[3] + val expected = contactData - deleted + database.deleteContactData(deleted) + assertDatabaseState(expectedContactData = expected) + } + + @Test + fun deleteContactsData() { + val deleted = listOf(contactData[3], contactData[1]) + val expected = contactData - deleted + database.deleteContactsData(deleted) + assertDatabaseState(expectedContactData = expected) + } + + @Test + fun findContactEmailById() { + val expected = contactEmails[2] + val actual = database.findContactEmailById(expected.contactEmailId!!) + Assert.assertEquals(expected, actual) + assertDatabaseState() + } + + @Test + fun findContactEmailByEmail() { + val expected = contactEmails[2] + val actual = database.findContactEmailByEmail(expected.email) + Assert.assertEquals(expected, actual) + assertDatabaseState() + } + + @Test + fun findContactEmailsByContactId() { + val contactId = contactEmails[2].contactId!! + val expected = contactEmails.filter { it.contactId == contactId } + val actual = database.findContactEmailsByContactId(contactId) + Assert.assertEquals(expected, actual) + assertDatabaseState() + } + + @Test + fun findAllContactsEmailsAsync() { + val expected = contactEmails.toSet() + val actual = database.findAllContactsEmailsAsync().testValue?.toSet() + Assert.assertEquals(expected, actual) + assertDatabaseState() + } + + @Ignore("Implement with contacts groups") + @Test + fun findAllContactsEmailsByContactGroupAsync() { + TODO() + assertDatabaseState() + } + + @Test + fun clearByEmail() { + val deletedEmail = contactEmails[1].email + val expected = contactEmails.filterNot { it.email == deletedEmail } + database.clearByEmail(deletedEmail) + assertDatabaseState(expectedContactEmails = expected) + } + + @Test + fun clearContactEmailsCache() { + val expected = emptyList() + database.clearContactEmailsCacheBlocking() + assertDatabaseState(expectedContactEmails = expected) + } + + @Test + fun deleteContactEmail() { + val deleted = listOf(contactEmails[3], contactEmails[1]) + val expected = contactEmails - deleted + database.deleteContactEmail(*deleted.toTypedArray()) + assertDatabaseState(expectedContactEmails = expected) + } + + @Test + fun deleteAllContactsEmails() { + val deleted = listOf(contactEmails[3], contactEmails[1]) + val expected = contactEmails - deleted + database.deleteAllContactsEmails(deleted) + assertDatabaseState(expectedContactEmails = expected) + } + + @Test + fun saveContactEmail() { + val inserted = ContactEmail( + "z", + "z@z.com", + contactId = "zzz", + labelIds = listOf("zzzz", "zzzzz", "zzzzzz"), + name = "ce1" + ) + val expected = contactEmails + inserted + database.saveContactEmail(inserted) + assertDatabaseState(expectedContactEmails = expected) + } + + @Test + fun saveAllContactsEmails() { + val inserted = listOf( + ContactEmail( + "y", + "y@y.com", + contactId = "yyy", + labelIds = listOf("yyyy", "yyyyy", "yyyyyy"), + name = "ce1" + ), + ContactEmail( + "z", + "z@z.com", + contactId = "zzz", + labelIds = listOf("zzzz", "zzzzz", "zzzzzz"), + name = "ce2" + ) + ) + val expected = contactEmails + inserted + database.saveAllContactsEmailsBlocking(inserted) + assertDatabaseState(expectedContactEmails = expected) + } + + @Test + fun saveAllContactsEmails1() { + val inserted = listOf( + ContactEmail( + "y", + "y@y.com", + contactId = "yyy", + labelIds = listOf("yyyy", "yyyyy", "yyyyyy"), + name = "ce1" + ), + ContactEmail( + "z", + "z@z.com", + contactId = "zzz", + labelIds = listOf("zzzz", "zzzzz", "zzzzzz"), + name = "ce2" + ) + ) + val expected = contactEmails + inserted + database.saveAllContactsEmailsBlocking(*inserted.toTypedArray()) + assertDatabaseState(expectedContactEmails = expected) + } + + @Ignore("Implement with contacts groups") + @Test + fun countContactEmails() { + TODO() + assertDatabaseState() + } + + @Ignore("Implement with contacts groups") + @Test + fun findContactGroupById() { + TODO() + assertDatabaseState() + } + + @Ignore("Implement with contacts groups") + @Test + fun findContactGroupByIdAsync() { + TODO() + assertDatabaseState() + } + + @Ignore("Implement with contacts groups") + @Test + fun findContactGroupsLiveData() { + TODO() + assertDatabaseState() + } + + @Ignore("Implement with contacts groups") + @Test + fun findContactGroupsObservable() { + TODO() + assertDatabaseState() + } + + @Ignore("Implement with contacts groups") + @Test + fun clearContactGroupsLabelsTable() { + TODO() + assertDatabaseState() + } + + @Ignore("Implement with contacts groups") + @Test + fun saveContactGroupLabel() { + TODO() + assertDatabaseState() + } + + @Ignore("Implement with contacts groups") + @Test + fun updateFullContactGroup() { + TODO() + assertDatabaseState() + } + + @Ignore("Implement with contacts groups") + @Test + fun saveAllContactGroups() { + TODO() + assertDatabaseState() + } + + @Ignore("Implement with contacts groups") + @Test + fun clearContactGroupsList() { + TODO() + assertDatabaseState() + } + + @Ignore("Implement with contacts groups") + @Test + fun saveContactGroupsList() { + TODO() + assertDatabaseState() + } + + @Ignore("Implement with contacts groups") + @Test + fun deleteByContactGroupLabelId() { + TODO() + assertDatabaseState() + } + + @Ignore("Implement with contacts groups") + @Test + fun deleteContactGroup() { + TODO() + assertDatabaseState() + } + + @Ignore("Implement with contacts groups") + @Test + fun getAllContactGroupsByIds() { + TODO() + assertDatabaseState() + } + + @Ignore("Implement with contacts groups") + @Test + fun updatePartially() { + TODO() + assertDatabaseState() + } + + @Test + fun insertFullContactDetails() { + val inserted = FullContactDetails( + contactId = "z", + name = "zz", + uid = "zzz", + createTime = 1, + modifyTime = 131, + size = 12, + defaults = 321, + encryptedData = mutableListOf( + ContactEncryptedData("zzzz", "zzzzz", Constants.VCardType.SIGNED), + ContactEncryptedData("zzzzzz", "zzzzzzz", Constants.VCardType.SIGNED_ENCRYPTED) + ) + ) + val expected = fullContactDetails + inserted + database.insertFullContactDetails(inserted) + assertDatabaseState(expectedFullContactDetails = expected) + } + + @Test + fun findFullContactDetailsById() { + val expected = fullContactDetails[1] + val actual = database.findFullContactDetailsById(expected.contactId) + Assert.assertThat( + actual, `is`(FullContactsDetailsMatcher(expected)) + ) + assertDatabaseState() + } + + @Test + fun clearFullContactDetailsCache() { + val expected = emptyList() + database.clearFullContactDetailsCache() + assertDatabaseState(expectedFullContactDetails = expected) + } + + @Test + fun deleteFullContactsDetails() { + val deleted = fullContactDetails[1] + val expected = fullContactDetails - deleted + database.deleteFullContactsDetails(deleted) + val found = database.findFullContactDetailsById(deleted.contactId) + Assert.assertNull(found) + assertDatabaseState(expectedFullContactDetails = expected) + } + + @Ignore("Implement with contacts groups") + @Test + fun countContactEmailsByLabelId() { + TODO() + assertDatabaseState() + } + + @Ignore("Implement with contacts groups") + @Test + fun saveContactEmailContactLabel() { + TODO() + assertDatabaseState() + } + + @Ignore("Implement with contacts groups") + @Test + fun saveContactEmailContactLabel1() { + TODO() + assertDatabaseState() + } + + @Ignore("Implement with contacts groups") + @Test + fun saveContactEmailContactLabel2() { + TODO() + assertDatabaseState() + } + + + @Test + fun testContactEmailsConverter() { + val email1 = ContactEmail( + "e1", + "1@1.1", + "a", + labelIds = listOf("la", "lc") + ) + val email2 = ContactEmail( + "e2", + "2@2.2", + "b", + labelIds = listOf("la", "lc") + ) + val email3 = ContactEmail( + "e3", + "3@3.3", + "c", + labelIds = listOf("la", "lc") + ) + initiallyEmptyDatabase.saveAllContactsEmailsBlocking(email1, email2, email3) + val emailFromDb = initiallyEmptyDatabase.findContactEmailById("e1") + Assert.assertNotNull(emailFromDb) + val listOfGroups = emailFromDb?.labelIds + Assert.assertNotNull(listOfGroups) + val expectedGroupId = "la" + Assert.assertEquals(expectedGroupId, listOfGroups?.get(0)) + } } diff --git a/app/src/androidTest/java/ch/protonmail/android/contacts/groups/details/ContactGroupDetailsRepositoryTest.kt b/app/src/androidTest/java/ch/protonmail/android/contacts/groups/details/ContactGroupDetailsRepositoryTest.kt index a77c2decb..2d8249627 100644 --- a/app/src/androidTest/java/ch/protonmail/android/contacts/groups/details/ContactGroupDetailsRepositoryTest.kt +++ b/app/src/androidTest/java/ch/protonmail/android/contacts/groups/details/ContactGroupDetailsRepositoryTest.kt @@ -64,7 +64,7 @@ class ContactGroupDetailsRepositoryTest { @BeforeTest fun setUp() { - contactGroupDetailsRepository = ContactGroupDetailsRepository(jobManager, protonMailApi, databaseProvider, workManager) + contactGroupDetailsRepository = ContactGroupDetailsRepository(protonMailApi, databaseProvider, workManager) } //region tests @@ -73,7 +73,7 @@ class ContactGroupDetailsRepositoryTest { val label1 = ContactLabel("a", "aa") every { database.findContactGroupByIdAsync("") } returns Single.just(label1) - val testObserver = contactGroupDetailsRepository.findContactGroupDetails("").test() + val testObserver = contactGroupDetailsRepository.findContactGroupDetailsBlocking("").test() testObserver.awaitTerminalEvent() testObserver.assertValue(label1) } @@ -84,7 +84,7 @@ class ContactGroupDetailsRepositoryTest { every { database.findContactGroupByIdAsync("a") } returns Single.just(label1) every { database.findContactGroupByIdAsync(any()) } returns Single.error(EmptyResultSetException("no such element")) - val testObserver = contactGroupDetailsRepository.findContactGroupDetails("b").test() + val testObserver = contactGroupDetailsRepository.findContactGroupDetailsBlocking("b").test() testObserver.awaitTerminalEvent() Assert.assertEquals(0, testObserver.valueCount()) testObserver.assertError(EmptyResultSetException::class.java) @@ -96,7 +96,7 @@ class ContactGroupDetailsRepositoryTest { val email2 = ContactEmail("b", "b@b.b", name = "ce2") every { database.findAllContactsEmailsByContactGroupAsyncObservable(any()) } returns Flowable.just(listOf(email1, email2)) - val testObserver = contactGroupDetailsRepository.getContactGroupEmails("").test() + val testObserver = contactGroupDetailsRepository.getContactGroupEmailsBlocking("").test() testObserver.awaitTerminalEvent() val returnedResult = testObserver.values()[0] val first = returnedResult?.get(0) @@ -111,7 +111,7 @@ class ContactGroupDetailsRepositoryTest { val emptyList: List = emptyList() every { database.findAllContactsEmailsByContactGroupAsyncObservable(any()) } returns Flowable.just(emptyList) - val testObserver = contactGroupDetailsRepository.getContactGroupEmails("").test() + val testObserver = contactGroupDetailsRepository.getContactGroupEmailsBlocking("").test() testObserver.awaitTerminalEvent() val returnedResult = testObserver.values() Assert.assertEquals(emptyList(), returnedResult[0]) //not sure about this @@ -140,7 +140,6 @@ class ContactGroupDetailsRepositoryTest { val testObserver = contactGroupDetailsRepository.createContactGroup(toCreateContactGroup).test() testObserver.awaitTerminalEvent() - verify(exactly = 1) { jobManager.addJobInBackground(any()) } verify(exactly = 0) { database.saveContactGroupLabel(any()) } } @@ -162,7 +161,7 @@ class ContactGroupDetailsRepositoryTest { val email2 = ContactEmail("b", "b@b.b", name = "ce2") every { database.findAllContactsEmailsByContactGroupAsyncObservable(any()) } returns Flowable.just(listOf(email1, email2)) - val testObserver = contactGroupDetailsRepository.getContactGroupEmails("").test() + val testObserver = contactGroupDetailsRepository.getContactGroupEmailsBlocking("").test() testObserver.awaitTerminalEvent() val returnedResult = testObserver.values()[0] val first = returnedResult?.get(0) 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..1d6c8c3e6 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; @@ -40,6 +39,7 @@ import androidx.appcompat.app.ActionBar; import androidx.core.content.FileProvider; +import androidx.lifecycle.ViewModelProvider; import androidx.work.Data; import androidx.work.OneTimeWorkRequest; import androidx.work.WorkManager; @@ -49,7 +49,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; @@ -63,14 +62,12 @@ 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.AttachmentsViewModel; +import ch.protonmail.android.attachments.AttachmentsViewState; 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 +80,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; @@ -103,8 +99,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; @@ -199,9 +193,6 @@ protected boolean checkForPermissionOnStartup() { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - messagesDatabase = MessagesDatabaseFactory.Companion.getInstance( - getApplicationContext()).getDatabase(); - ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); @@ -226,6 +217,11 @@ protected void onCreate(Bundle savedInstanceState) { mAdapter = new AttachmentListAdapter(this, attachmentList, totalEmbeddedImages, workManager); mListView.setAdapter(mAdapter); + + AttachmentsViewModel viewModel = new ViewModelProvider(this).get(AttachmentsViewModel.class); + viewModel.init(); + + viewModel.getViewState().observe(this, this::viewStateChanged); } @Override @@ -350,31 +346,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); @@ -440,6 +411,34 @@ public void onDownloadAttachmentEvent(DownloadedAttachmentEvent event) { } } + private void viewStateChanged(AttachmentsViewState viewState) { + if (viewState instanceof AttachmentsViewState.MissingConnectivity) { + onMessageReady(); + } + + if (viewState instanceof AttachmentsViewState.UpdateAttachments) { + onMessageReady(); + updateDisplayedAttachments( + ((AttachmentsViewState.UpdateAttachments) viewState).getAttachments() + ); + } + } + + private void onMessageReady() { + mDraftCreated = true; + mProgressLayout.setVisibility(View.GONE); + invalidateOptionsMenu(); + } + + private void updateDisplayedAttachments(List attachments) { + List localAttachments = new ArrayList<>( + LocalAttachment.Companion.createLocalAttachmentList(attachments) + ); + int totalEmbeddedImages = countEmbeddedImages(localAttachments); + mAdapter.updateData(new ArrayList(localAttachments), totalEmbeddedImages); + } + + private void handleAttachFileRequest(Uri uri, ClipData clipData) { String[] uris = null; @@ -571,60 +570,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/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/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/SearchActivity.java b/app/src/main/java/ch/protonmail/android/activities/SearchActivity.java index 50a3f8930..b918c0b7f 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,8 @@ 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.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.models.room.messages.Message; 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,17 +57,15 @@ 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 { - private PendingActionsDatabase pendingActionsDatabase; - private MessagesRecyclerViewAdapter mAdapter; private TextView noMessagesView; private ProgressBar mProgressBar; @@ -83,7 +73,6 @@ public class SearchActivity extends BaseActivity { private String mQueryText = ""; private int mCurrentPage; private SearchView searchView = null; - private MessagesDatabase searchDatabase; @Inject MessageDetailsRepository messageDetailsRepository; @@ -98,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); @@ -141,11 +128,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 +148,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 +244,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 +275,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); - } - } } 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 13ab1ef39..ced8362f8 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,10 +133,8 @@ 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.DraftCreatedEvent; import ch.protonmail.android.events.FetchDraftDetailEvent; import ch.protonmail.android.events.FetchMessageDetailEvent; import ch.protonmail.android.events.HumanVerifyOptionsEvent; @@ -187,7 +185,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; @@ -467,12 +464,20 @@ 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.getSavingDraftError().observe(this, errorMessage -> + Toast.makeText(this, errorMessage, Toast.LENGTH_SHORT).show()); 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); + onMessageLoaded( + event, + false, + TextUtils.isEmpty(mAction) && + composeMessageViewModel.getMessageDataResult().getAttachmentList().isEmpty() + ); }); composeMessageViewModel.getDbIdWatcher().observe(ComposeMessageActivity.this, new SendMessageObserver()); @@ -745,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 @@ -972,11 +978,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)) { @@ -995,18 +996,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(); @@ -1017,8 +1006,6 @@ protected void onStart() { if (askForPermission) { contactsPermissionHelper.checkPermission(); } - composeMessageViewModel.insertPendingDraft(); -// mToRecipientsView.invalidateRecipients(); } @Override @@ -1069,7 +1056,6 @@ private void onStoragePermissionGranted() { @Override protected void onStop() { super.onStop(); - composeMessageViewModel.removePendingDraft(); askForPermission = true; ProtonMailApplication.getApplication().getBus().unregister(this); ProtonMailApplication.getApplication().getBus().unregister(composeMessageViewModel); @@ -1122,11 +1108,7 @@ private void showDraftDialog() { getString(R.string.yes), getString(R.string.cancel), unit -> { - String draftId = composeMessageViewModel.getDraftId(); - if (!TextUtils.isEmpty(draftId)) { - composeMessageViewModel.deleteDraft(); - } - mComposeBodyEditText.setIsDirty(false); + composeMessageViewModel.deleteDraft(); finishActivity(); return unit; }, @@ -1207,13 +1189,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(); } } } @@ -1475,14 +1455,8 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { ArrayList listToSet = resultAttachmentList != null ? resultAttachmentList : new ArrayList<>(); composeMessageViewModel.setAttachmentList(listToSet); composeMessageViewModel.setIsDirty(true); - String draftId = data.getStringExtra(AddAttachmentsActivity.EXTRA_DRAFT_ID); String oldDraftId = composeMessageViewModel.getDraftId(); - if (!TextUtils.isEmpty(draftId) && !draftId.equals(oldDraftId)) { - composeMessageViewModel.setDraftId(draftId); - afterAttachmentsAdded(); - } else if (!TextUtils.isEmpty(oldDraftId)) { - afterAttachmentsAdded(); - } + afterAttachmentsAdded(); composeMessageViewModel.setIsDirty(true); } else if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_VALIDATE_PIN) { // region pin results @@ -1492,19 +1466,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); @@ -2163,19 +2133,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 @@ -2243,7 +2200,6 @@ public void onChanged(@Nullable Event messageEvent) { Message localMessage = messageEvent.getContentIfNotHandled(); if (localMessage != null) { - composeMessageViewModel.setOfflineDraftSaved(false); String aliasAddress = composeMessageViewModel.getMessageDataResult().getAddressEmailAlias(); MessageSender messageSender; @@ -2273,7 +2229,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/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..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} ${event.attachmentName}", - Toast.LENGTH_SHORT - ) - } - @Subscribe fun onMailboxLoginEvent(event: MailboxLoginEvent?) { if (event == null) { @@ -1771,7 +1762,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() @@ -1811,20 +1802,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/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..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 @@ -23,33 +23,36 @@ 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. - */ -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::findMessageById) - } + 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/MessageDetailsActivity.kt b/app/src/main/java/ch/protonmail/android/activities/messageDetails/MessageDetailsActivity.kt index 1a0a89568..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} ${event.attachmentName}", - 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/activities/messageDetails/repository/MessageDetailsRepository.kt b/app/src/main/java/ch/protonmail/android/activities/messageDetails/repository/MessageDetailsRepository.kt index a0993b5dd..2407f413a 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 @@ -51,7 +50,9 @@ import com.birbit.android.jobqueue.JobManager import io.reactivex.Flowable import io.reactivex.Single import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow 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 + val databaseProvider: DatabaseProvider, + private val dispatchers: DispatcherProvider ) { private var messagesDao: MessagesDao = databaseProvider.provideMessagesDao() @@ -86,53 +88,61 @@ 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) { - findMessageByMessageDbId(dbId) - } + suspend fun findMessageByMessageDbIdBlocking(dbId: Long, dispatcher: CoroutineDispatcher): Message? = + withContext(dispatcher) { + findMessageByMessageDbIdBlocking(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) } + 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) } + fun findMessageByMessageDbIdBlocking(messageDbId: Long): Message? = + messagesDao.findMessageByMessageDbId(messageDbId)?.apply { readMessageBodyFromFileIfNeeded(this) } + + fun findMessageByDbId(messageDbId: Long): Flow = + messagesDao.findMessageByDbId(messageDbId) 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() } @@ -149,14 +159,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) @@ -197,7 +207,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) @@ -218,12 +228,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) } @@ -255,7 +265,7 @@ class MessageDetailsRepository @Inject constructor( messages.map(this::saveSearchMessageInDB) } - fun saveSearchMessagesInOneTransaction(messages:List) { + fun saveSearchMessagesInOneTransaction(messages: List) { if (messages.isEmpty()) { return } @@ -301,13 +311,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) @@ -362,82 +372,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 { @@ -461,13 +474,6 @@ class MessageDetailsRepository @Inject constructor( jobManager.addJobInBackground(PostReadJob(listOf(messageId))) } - suspend fun insertPendingDraft(messageDbId: Long, dispatcher: CoroutineDispatcher) = - withContext(dispatcher) { - 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/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/activities/settings/BaseSettingsActivity.kt b/app/src/main/java/ch/protonmail/android/activities/settings/BaseSettingsActivity.kt index 220467989..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 @@ -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 @@ -153,7 +151,6 @@ abstract class BaseSettingsActivity : BaseConnectivityActivity() { override fun onResume() { super.onResume() user = mUserManager.user - settingsAdapter.notifyDataSetChanged() viewModel.checkConnectivity() } @@ -350,8 +347,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) @@ -406,6 +401,9 @@ abstract class BaseSettingsActivity : BaseConnectivityActivity() { } } } + else -> { + Timber.v("Unhandled setting: ${settingsId.toUpperCase(Locale.ENGLISH)} selection") + } } } @@ -439,6 +437,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 } } @@ -498,11 +501,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/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 = api.fetchContactEmails(pageSize) + override suspend fun fetchContactEmails(page: Int, pageSize: Int): ContactEmailsResponseV2 = + api.fetchContactEmails(page, pageSize) override fun fetchContactsEmailsByLabelId(page: Int, labelId: String): Observable = api.fetchContactsEmailsByLabelId(page, labelId) @@ -268,6 +270,8 @@ class ProtonMailApiManager @Inject constructor(var api: ProtonMailApi) : override fun fetchContactGroups(): Single = api.fetchContactGroups() + override suspend fun fetchContactGroupsList(): List = api.fetchContactGroupsList() + override fun fetchContactGroupsAsObservable(): Observable> = api.fetchContactGroupsAsObservable() override fun createLabel(label: LabelBody): LabelResponse = api.createLabel(label) @@ -319,9 +323,21 @@ 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 updateDraftBlocking( + messageId: String, + draftBody: DraftBody, + retrofitTag: RetrofitTag + ): MessageResponse? = api.updateDraftBlocking(messageId, draftBody, retrofitTag) - override fun updateDraft(messageId: String, draftBody: DraftBody, retrofitTag: RetrofitTag): MessageResponse? = api.updateDraft(messageId, draftBody, retrofitTag) + override suspend fun updateDraft( + messageId: String, + draftBody: DraftBody, + retrofitTag: 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/interceptors/ProtonMailAuthenticator.kt b/app/src/main/java/ch/protonmail/android/api/interceptors/ProtonMailAuthenticator.kt index 7fdb8ca49..2a257062a 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 @@ -19,6 +19,7 @@ package ch.protonmail.android.api.interceptors +import android.content.Context import ch.protonmail.android.BuildConfig import ch.protonmail.android.api.TokenManager import ch.protonmail.android.api.segments.HEADER_APP_VERSION @@ -28,9 +29,9 @@ import ch.protonmail.android.api.segments.HEADER_UID import ch.protonmail.android.api.segments.HEADER_USER_AGENT import ch.protonmail.android.api.segments.REFRESH_PATH import ch.protonmail.android.api.segments.RESPONSE_CODE_TOO_MANY_REQUESTS -import ch.protonmail.android.core.ProtonMailApplication import ch.protonmail.android.core.UserManager import ch.protonmail.android.utils.AppUtil +import ch.protonmail.android.utils.extensions.app import com.birbit.android.jobqueue.JobManager import com.birbit.android.jobqueue.TagConstraint import okhttp3.Authenticator @@ -45,7 +46,8 @@ import javax.inject.Singleton @Singleton class ProtonMailAuthenticator @Inject constructor( private val userManager: UserManager, - private val jobManager: JobManager + private val jobManager: JobManager, + private val appContext: Context ) : Authenticator { private val appVersionName by lazy { @@ -57,6 +59,8 @@ class ProtonMailAuthenticator @Inject constructor( } } + // api instance cannot be injected, due to a circular dependency + @Throws(IOException::class) override fun authenticate(route: Route?, response: Response): Request? = refreshAuthToken(response) @@ -85,7 +89,7 @@ class ProtonMailAuthenticator @Inject constructor( if (tokenManager != null && !originalRequest.url().encodedPath().contains(REFRESH_PATH)) { val refreshBody = tokenManager.createRefreshBody() val refreshResponse = - ProtonMailApplication.getApplication().api.refreshAuthBlocking(refreshBody, RetrofitTag(usernameAuth)) + appContext.app.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" @@ -104,7 +108,7 @@ class ProtonMailAuthenticator @Inject constructor( "(refresh token blank = ${tokenManager.isRefreshTokenBlank()}, " + "uid blank = ${tokenManager.isUidBlank()}), logging out" ) - ProtonMailApplication.getApplication().notifyLoggedOut(usernameAuth) + appContext.app.notifyLoggedOut(usernameAuth) jobManager.stop() jobManager.clear() jobManager.cancelJobsInBackground(null, TagConstraint.ALL) @@ -128,7 +132,7 @@ class ProtonMailAuthenticator @Inject constructor( .header(HEADER_UID, tokenManager.uid) .header(HEADER_APP_VERSION, appVersionName) .header(HEADER_USER_AGENT, AppUtil.buildUserAgent()) - .header(HEADER_LOCALE, ProtonMailApplication.getApplication().currentLocale) + .header(HEADER_LOCALE, appContext.app.currentLocale) .build() } } else { // if received 401 error while refreshing access token, send event to logout user @@ -137,7 +141,7 @@ class ProtonMailAuthenticator @Inject constructor( "(refresh token blank = ${tokenManager?.isRefreshTokenBlank()}, " + "uid blank = ${tokenManager?.isUidBlank()})" ) - ProtonMailApplication.getApplication().notifyLoggedOut(usernameAuth) + appContext.app.notifyLoggedOut(usernameAuth) userManager.logoutOffline(usernameAuth) return null } @@ -149,7 +153,7 @@ class ProtonMailAuthenticator @Inject constructor( .header(HEADER_UID, tokenManager.uid) .header(HEADER_APP_VERSION, appVersionName) .header(HEADER_USER_AGENT, AppUtil.buildUserAgent()) - .header(HEADER_LOCALE, ProtonMailApplication.getApplication().currentLocale) + .header(HEADER_LOCALE, appContext.app.currentLocale) .build() } } diff --git a/app/src/main/java/ch/protonmail/android/api/models/ContactEmailsResponseV2.java b/app/src/main/java/ch/protonmail/android/api/models/ContactEmailsResponseV2.java index fa6356834..9d7fab9c7 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/ContactEmailsResponseV2.java +++ b/app/src/main/java/ch/protonmail/android/api/models/ContactEmailsResponseV2.java @@ -25,9 +25,6 @@ import ch.protonmail.android.api.models.room.contacts.ContactEmail; import ch.protonmail.android.api.utils.Fields; -/** - * Created by dkadrikj on 8/22/16. - */ public class ContactEmailsResponseV2 extends ResponseBody { @SerializedName(Fields.Contact.TOTAL) 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..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 @@ -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 @@ -35,13 +36,23 @@ 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() { - if (field == null) { - field = HashMap() - } - return field - } + var attachmentKeyPackets: MutableMap = hashMapOf() + + 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/EventResponse.java b/app/src/main/java/ch/protonmail/android/api/models/EventResponse.java index 5022dacf3..b594cecc9 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/EventResponse.java +++ b/app/src/main/java/ch/protonmail/android/api/models/EventResponse.java @@ -18,6 +18,8 @@ */ package ch.protonmail.android.api.models; +import androidx.annotation.Nullable; + import com.google.gson.annotations.SerializedName; import java.util.List; @@ -60,6 +62,7 @@ public class EventResponse extends ResponseBody { @SerializedName(Fields.Events.ADDRESSES) private List addresses; + @Nullable public List getMessageUpdates() { return messages; } 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 ae791a375..c65bf43fe 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 @@ -36,5 +36,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/MessageRecipient.java b/app/src/main/java/ch/protonmail/android/api/models/MessageRecipient.java index 1fc549a65..058f9ebe7 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/MessageRecipient.java +++ b/app/src/main/java/ch/protonmail/android/api/models/MessageRecipient.java @@ -18,6 +18,8 @@ */ package ch.protonmail.android.api.models; +import android.widget.Toast; + import androidx.annotation.NonNull; import androidx.room.ColumnInfo; import androidx.room.Ignore; @@ -33,6 +35,7 @@ import java.io.Serializable; import java.lang.reflect.Type; import java.util.List; +import java.util.Objects; import static ch.protonmail.android.api.models.room.contacts.ContactDataKt.COLUMN_CONTACT_DATA_NAME; import static ch.protonmail.android.api.models.room.contacts.ContactEmailKt.COLUMN_CONTACT_EMAILS_EMAIL; @@ -60,6 +63,7 @@ public class MessageRecipient implements Serializable, Comparable groupRecipients; @Ignore boolean selected; + private static final long serialVersionUID = -110723370017912622L; public MessageRecipient(String Name, String Address, String Group) { this.Name = Name; @@ -198,4 +202,19 @@ public MessageRecipient deserialize(JsonElement json, Type typeOfT, JsonDeserial return messageRecipient; } } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MessageRecipient that = (MessageRecipient) o; + return Objects.equals(Name, that.Name) && + Objects.equals(Address, that.Address) && + Objects.equals(Group, that.Group); + } + + @Override + public int hashCode() { + return Objects.hash(Name, Address, Group); + } } 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..c366af500 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 @@ -282,11 +282,7 @@ public void save() { } if (MobileSignature == null) { - if (!isPaidUserSignatureEdit()) { - MobileSignature = ProtonMailApplication.getApplication().getString(R.string.default_mobile_signature); - } else { - MobileSignature = pref.getString(PREF_MOBILE_SIGNATURE, ProtonMailApplication.getApplication().getString(R.string.default_mobile_signature)); - } + MobileSignature = pref.getString(PREF_MOBILE_SIGNATURE, ProtonMailApplication.getApplication().getString(R.string.default_mobile_signature)); } ShowSignature = loadShowSignatureSetting(); @@ -676,7 +672,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/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/MessageFactory.kt b/app/src/main/java/ch/protonmail/android/api/models/messages/receive/MessageFactory.kt index acd1d77cb..b236cd696 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.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 @@ -26,13 +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 -) : IMessageFactory { +class MessageFactory @Inject constructor( + private val attachmentFactory: IAttachmentFactory, + private val messageSenderFactory: MessageSenderFactory +) { - override fun createServerMessage(message: Message): ServerMessage { + fun createDraftApiRequest(message: Message): DraftBody = + DraftBody(createServerMessage(message)) + + fun createServerMessage(message: Message): ServerMessage { return message.let { val serverMessage = ServerMessage() serverMessage.ID = it.messageId @@ -63,7 +68,7 @@ class MessageFactory( } } - override fun createMessage(serverMessage: ServerMessage): Message { + fun createMessage(serverMessage: ServerMessage): Message { return serverMessage.let { val message = Message() message.messageId = it.ID @@ -77,26 +82,34 @@ class MessageFactory( 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) @@ -121,8 +134,11 @@ class MessageFactory( 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 IllegalArgumentException( + "Attachments size does not match expected: $numOfAttachments, actual: $attachmentsListSize " + ) message } } + } 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 } 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..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,16 +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 { - override fun createServerMessageSender(messageSender:MessageSender):ServerMessageSender { - val (name,emailAddress)=messageSender - return ServerMessageSender(name,emailAddress) - } +class MessageSenderFactory @Inject constructor() { - 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 + fun createServerMessageSender(messageSender: MessageSender): ServerMessageSender { + val (name, emailAddress) = messageSender + return ServerMessageSender(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/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/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/api/models/room/contacts/ContactEmail.kt b/app/src/main/java/ch/protonmail/android/api/models/room/contacts/ContactEmail.kt index e77719509..fc74991c7 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/room/contacts/ContactEmail.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/room/contacts/ContactEmail.kt @@ -37,11 +37,13 @@ const val COLUMN_CONTACT_EMAILS_CONTACT_ID = "ContactID" const val COLUMN_CONTACT_EMAILS_LABEL_IDS = "LabelIDs" const val COLUMN_CONTACT_EMAILS_DEFAULTS = "Defaults" -@Entity(tableName = TABLE_CONTACT_EMAILS, - indices = [ - Index(COLUMN_CONTACT_EMAILS_ID, unique = true), - Index(COLUMN_CONTACT_EMAILS_EMAIL, unique = false) - ]) +@Entity( + tableName = TABLE_CONTACT_EMAILS, + indices = [ + Index(COLUMN_CONTACT_EMAILS_ID, unique = true), + Index(COLUMN_CONTACT_EMAILS_EMAIL, unique = false) + ] +) @TypeConverters(value = [ContactEmailConverter::class]) @kotlinx.serialization.Serializable data class ContactEmail @JvmOverloads constructor( diff --git a/app/src/main/java/ch/protonmail/android/api/models/room/contacts/ContactEmailContactLabelJoin.kt b/app/src/main/java/ch/protonmail/android/api/models/room/contacts/ContactEmailContactLabelJoin.kt index 2f3cca738..fcd26243b 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/room/contacts/ContactEmailContactLabelJoin.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/room/contacts/ContactEmailContactLabelJoin.kt @@ -18,8 +18,11 @@ */ package ch.protonmail.android.api.models.room.contacts -import androidx.room.* +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey import androidx.room.ForeignKey.CASCADE +import androidx.room.Index import ch.protonmail.android.api.models.room.messages.COLUMN_LABEL_ID // region constants @@ -28,23 +31,33 @@ const val COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID = "labelId" const val COLUMN_CONTACT_EMAILS_LABELS_JOIN_EMAIL_ID = "emailId" // endregion -/** - * Created by kadrikj on 8/30/18. - */ +@Entity( + tableName = TABLE_CONTACT_EMAILS_LABELS_JOIN, + primaryKeys = [ + COLUMN_CONTACT_EMAILS_LABELS_JOIN_EMAIL_ID, + COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID + ], + foreignKeys = [ + ForeignKey( + entity = ContactEmail::class, + childColumns = [COLUMN_CONTACT_EMAILS_LABELS_JOIN_EMAIL_ID], + parentColumns = [COLUMN_CONTACT_EMAILS_ID], onDelete = CASCADE + ), -@Entity(tableName = TABLE_CONTACT_EMAILS_LABELS_JOIN, - primaryKeys = [(COLUMN_CONTACT_EMAILS_LABELS_JOIN_EMAIL_ID), (COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID)], - foreignKeys = [ - (ForeignKey(entity = ContactEmail::class, childColumns = [COLUMN_CONTACT_EMAILS_LABELS_JOIN_EMAIL_ID], - parentColumns = [COLUMN_CONTACT_EMAILS_ID], onDelete = CASCADE)), - (ForeignKey(entity = ContactLabel::class, childColumns = [COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID], - parentColumns = [COLUMN_LABEL_ID], onDelete = CASCADE)) - ], - indices = [Index(COLUMN_CONTACT_EMAILS_LABELS_JOIN_EMAIL_ID), - Index(COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID)]) + ForeignKey( + entity = ContactLabel::class, + childColumns = [COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID], + parentColumns = [COLUMN_LABEL_ID], onDelete = CASCADE + ) + ], + indices = [ + Index(COLUMN_CONTACT_EMAILS_LABELS_JOIN_EMAIL_ID), + Index(COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID) + ] +) data class ContactEmailContactLabelJoin constructor( - @ColumnInfo(name = COLUMN_CONTACT_EMAILS_LABELS_JOIN_EMAIL_ID) - var emailId: String, - @ColumnInfo(name = COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID) - var labelId: String -) \ No newline at end of file + @ColumnInfo(name = COLUMN_CONTACT_EMAILS_LABELS_JOIN_EMAIL_ID) + val emailId: String, + @ColumnInfo(name = COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID) + val labelId: String +) diff --git a/app/src/main/java/ch/protonmail/android/api/models/room/contacts/ContactsDatabase.kt b/app/src/main/java/ch/protonmail/android/api/models/room/contacts/ContactsDatabase.kt index 926ec23ac..0d3169ee3 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/room/contacts/ContactsDatabase.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/room/contacts/ContactsDatabase.kt @@ -25,6 +25,7 @@ import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.Transaction import androidx.room.Update import ch.protonmail.android.api.models.MessageRecipient import ch.protonmail.android.api.models.room.messages.COLUMN_LABEL_ID @@ -32,232 +33,289 @@ import ch.protonmail.android.api.models.room.messages.COLUMN_LABEL_NAME import ch.protonmail.android.api.models.room.messages.COLUMN_LABEL_ORDER import io.reactivex.Flowable import io.reactivex.Single +import kotlinx.coroutines.flow.Flow // TODO remove when we change name of this class to ContactsDao and *Factory to *Database typealias ContactsDao = ContactsDatabase @Dao -abstract class -ContactsDatabase { - //region Contact data - @Query("SELECT * FROM $TABLE_CONTACT_DATA WHERE ${COLUMN_CONTACT_DATA_ID}=:contactId") - abstract fun findContactDataById(contactId:String):ContactData? +interface ContactsDatabase { + //region Contact data + @Query("SELECT * FROM $TABLE_CONTACT_DATA WHERE ${COLUMN_CONTACT_DATA_ID}=:contactId") + fun findContactDataById(contactId: String): ContactData? - @Query("SELECT * FROM $TABLE_CONTACT_DATA WHERE ${BaseColumns._ID}=:contactDbId") - abstract fun findContactDataByDbId(contactDbId:Long):ContactData? + @Query("SELECT * FROM $TABLE_CONTACT_DATA WHERE ${BaseColumns._ID}=:contactDbId") + fun findContactDataByDbId(contactDbId: Long): ContactData? - @Query("SELECT * FROM $TABLE_CONTACT_DATA ORDER BY $COLUMN_CONTACT_DATA_NAME COLLATE NOCASE ASC") - abstract fun findAllContactDataAsync():LiveData> + @Query("SELECT * FROM $TABLE_CONTACT_DATA ORDER BY $COLUMN_CONTACT_DATA_NAME COLLATE NOCASE ASC") + fun findAllContactDataAsync(): LiveData> - @Query("DELETE FROM $TABLE_CONTACT_DATA") - abstract fun clearContactDataCache() + @Query("SELECT * FROM $TABLE_CONTACT_DATA ORDER BY $COLUMN_CONTACT_DATA_NAME COLLATE NOCASE ASC") + fun findAllContactData(): Flow> - @Insert(onConflict=OnConflictStrategy.REPLACE) - abstract fun saveContactData(contactData:ContactData):Long + @Query("DELETE FROM $TABLE_CONTACT_DATA") + fun clearContactDataCache() - @Insert(onConflict=OnConflictStrategy.REPLACE) - abstract fun saveAllContactsData(vararg contactData:ContactData):List + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun saveContactData(contactData: ContactData): Long - @Insert(onConflict=OnConflictStrategy.REPLACE) - abstract fun saveAllContactsData(contactData:Collection):List + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun saveAllContactsData(vararg contactData: ContactData): List - @Delete - abstract fun deleteContactData(vararg contactData:ContactData) + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun saveAllContactsData(contactData: Collection): List - @Delete - abstract fun deleteContactsData(contactData:Collection) + @Delete + fun deleteContactData(vararg contactData: ContactData) - //endregion + @Delete + fun deleteContactsData(contactData: Collection) - //region Contact email - @Query("SELECT * FROM $TABLE_CONTACT_EMAILS WHERE ${COLUMN_CONTACT_EMAILS_ID}=:id") - abstract fun findContactEmailById(id:String):ContactEmail? + //endregion - @Query("SELECT * FROM $TABLE_CONTACT_EMAILS WHERE ${COLUMN_CONTACT_EMAILS_EMAIL}=:email") - abstract fun findContactEmailByEmail(email:String):ContactEmail? + //region Contact email + @Query("SELECT * FROM $TABLE_CONTACT_EMAILS WHERE $COLUMN_CONTACT_EMAILS_ID=:id") + fun findContactEmailById(id: String): ContactEmail? - @Query("SELECT * FROM $TABLE_CONTACT_EMAILS WHERE ${COLUMN_CONTACT_EMAILS_EMAIL}=:email") - abstract fun findContactEmailByEmailLiveData(email: String): LiveData + @Query("SELECT * FROM $TABLE_CONTACT_EMAILS WHERE $COLUMN_CONTACT_EMAILS_EMAIL=:email") + fun findContactEmailByEmail(email: String): ContactEmail? - @Query("SELECT * FROM $TABLE_CONTACT_EMAILS WHERE ${COLUMN_CONTACT_EMAILS_CONTACT_ID}=:contactId") - abstract fun findContactEmailsByContactId(contactId:String): List + @Query("SELECT * FROM $TABLE_CONTACT_EMAILS WHERE $COLUMN_CONTACT_EMAILS_EMAIL=:email") + fun findContactEmailByEmailLiveData(email: String): LiveData - @Query("SELECT * FROM $TABLE_CONTACT_EMAILS WHERE ${COLUMN_CONTACT_EMAILS_CONTACT_ID}=:contactId") - abstract fun findContactEmailsByContactIdObservable(contactId:String): Flowable> + @Query("SELECT * FROM $TABLE_CONTACT_EMAILS WHERE $COLUMN_CONTACT_EMAILS_CONTACT_ID=:contactId") + fun findContactEmailsByContactId(contactId: String): List - @Query("SELECT * FROM $TABLE_CONTACT_EMAILS ORDER BY $COLUMN_CONTACT_EMAILS_EMAIL") - abstract fun findAllContactsEmailsAsync():LiveData< List> + @Query("SELECT * FROM $TABLE_CONTACT_EMAILS WHERE $COLUMN_CONTACT_EMAILS_CONTACT_ID=:contactId") + fun findContactEmailsByContactIdObservable(contactId: String): Flowable> - @Query("SELECT * FROM $TABLE_CONTACT_EMAILS ORDER BY $COLUMN_CONTACT_EMAILS_EMAIL") - abstract fun findAllContactsEmailsAsyncObservable(): Flowable> + @Query("SELECT * FROM $TABLE_CONTACT_EMAILS ORDER BY $COLUMN_CONTACT_EMAILS_EMAIL") + fun findAllContactsEmailsAsync(): LiveData> - @Query("SELECT * FROM $TABLE_CONTACT_EMAILS WHERE ${TABLE_CONTACT_EMAILS}.${COLUMN_CONTACT_EMAILS_EMAIL} LIKE :filter ORDER BY $COLUMN_CONTACT_EMAILS_EMAIL") - abstract fun findAllContactsEmailsAsyncObservable(filter: String): Flowable> + @Query("SELECT * FROM $TABLE_CONTACT_EMAILS ORDER BY $COLUMN_CONTACT_EMAILS_EMAIL") + fun findAllContactsEmails(): Flow> - @Query("SELECT ${TABLE_CONTACT_EMAILS}.* FROM $TABLE_CONTACT_EMAILS INNER JOIN $TABLE_CONTACT_EMAILS_LABELS_JOIN ON ${TABLE_CONTACT_EMAILS}.${COLUMN_CONTACT_EMAILS_ID} = ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_EMAIL_ID} WHERE ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID} = :contactGroupId") - abstract fun findAllContactsEmailsByContactGroupAsync(contactGroupId: String):LiveData> + @Query("SELECT * FROM $TABLE_CONTACT_EMAILS ORDER BY $COLUMN_CONTACT_EMAILS_EMAIL") + fun findAllContactsEmailsAsyncObservable(): Flowable> - @Query("SELECT ${TABLE_CONTACT_EMAILS}.* FROM $TABLE_CONTACT_EMAILS INNER JOIN $TABLE_CONTACT_EMAILS_LABELS_JOIN ON ${TABLE_CONTACT_EMAILS}.${COLUMN_CONTACT_EMAILS_ID} = ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_EMAIL_ID} WHERE ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID} = :contactGroupId") - abstract fun findAllContactsEmailsByContactGroup(contactGroupId: String): List + @Query("SELECT * FROM $TABLE_CONTACT_EMAILS WHERE $TABLE_CONTACT_EMAILS.$COLUMN_CONTACT_EMAILS_EMAIL LIKE :filter ORDER BY $COLUMN_CONTACT_EMAILS_EMAIL") + fun findAllContactsEmailsAsyncObservable(filter: String): Flowable> - @Query("SELECT ${TABLE_CONTACT_EMAILS}.* FROM $TABLE_CONTACT_EMAILS INNER JOIN $TABLE_CONTACT_EMAILS_LABELS_JOIN ON ${TABLE_CONTACT_EMAILS}.${COLUMN_CONTACT_EMAILS_ID} = ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_EMAIL_ID} WHERE ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID} = :contactGroupId") - abstract fun findAllContactsEmailsByContactGroupAsyncObservable(contactGroupId: String): Flowable> + @Query("SELECT ${TABLE_CONTACT_EMAILS}.* FROM $TABLE_CONTACT_EMAILS INNER JOIN $TABLE_CONTACT_EMAILS_LABELS_JOIN ON ${TABLE_CONTACT_EMAILS}.${COLUMN_CONTACT_EMAILS_ID} = ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_EMAIL_ID} WHERE ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID} = :contactGroupId") + fun findAllContactsEmailsByContactGroupAsync(contactGroupId: String): LiveData> - @Query("SELECT ${TABLE_CONTACT_LABEL}.* FROM $TABLE_CONTACT_LABEL INNER JOIN $TABLE_CONTACT_EMAILS_LABELS_JOIN ON ${TABLE_CONTACT_LABEL}.${COLUMN_LABEL_ID} = ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID} WHERE ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_EMAIL_ID} = :emailId ORDER BY $COLUMN_LABEL_NAME") - abstract fun findAllContactGroupsByContactEmailAsync(emailId: String): LiveData> + @Query("SELECT ${TABLE_CONTACT_EMAILS}.* FROM $TABLE_CONTACT_EMAILS INNER JOIN $TABLE_CONTACT_EMAILS_LABELS_JOIN ON ${TABLE_CONTACT_EMAILS}.${COLUMN_CONTACT_EMAILS_ID} = ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_EMAIL_ID} WHERE ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID} = :contactGroupId") + fun findAllContactsEmailsByContactGroup(contactGroupId: String): List - @Query("SELECT ${TABLE_CONTACT_LABEL}.* FROM $TABLE_CONTACT_LABEL INNER JOIN $TABLE_CONTACT_EMAILS_LABELS_JOIN ON ${TABLE_CONTACT_LABEL}.${COLUMN_LABEL_ID} = ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID} WHERE ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_EMAIL_ID} = :emailId ORDER BY $COLUMN_LABEL_NAME") - abstract fun findAllContactGroupsByContactEmailAsyncObservable(emailId: String): Flowable> + @Query("SELECT ${TABLE_CONTACT_EMAILS}.* FROM $TABLE_CONTACT_EMAILS INNER JOIN $TABLE_CONTACT_EMAILS_LABELS_JOIN ON ${TABLE_CONTACT_EMAILS}.${COLUMN_CONTACT_EMAILS_ID} = ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_EMAIL_ID} WHERE ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID} = :contactGroupId") + suspend fun findAllContactsEmailsByContactGroupId(contactGroupId: String): List - /** - * Make sure you provide @param filter with included % or ? - */ - @Query("SELECT ${TABLE_CONTACT_EMAILS}.* FROM $TABLE_CONTACT_EMAILS INNER JOIN $TABLE_CONTACT_EMAILS_LABELS_JOIN ON ${TABLE_CONTACT_EMAILS}.${COLUMN_CONTACT_EMAILS_ID} = ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_EMAIL_ID} WHERE ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID} = :contactGroupId AND ${TABLE_CONTACT_EMAILS}.${COLUMN_CONTACT_EMAILS_EMAIL} LIKE :filter") - abstract fun filterContactsEmailsByContactGroupAsyncObservable(contactGroupId: String, filter: String): Flowable> + @Query("SELECT ${TABLE_CONTACT_EMAILS}.* FROM $TABLE_CONTACT_EMAILS INNER JOIN $TABLE_CONTACT_EMAILS_LABELS_JOIN ON ${TABLE_CONTACT_EMAILS}.${COLUMN_CONTACT_EMAILS_ID} = ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_EMAIL_ID} WHERE ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID} = :contactGroupId") + fun findAllContactsEmailsByContactGroupIdFlow(contactGroupId: String): Flow> - /** - * Make sure you provide @param filter with included % or ? - */ - @Query("SELECT ${TABLE_CONTACT_EMAILS}.* FROM $TABLE_CONTACT_EMAILS INNER JOIN $TABLE_CONTACT_EMAILS_LABELS_JOIN ON ${TABLE_CONTACT_EMAILS}.${COLUMN_CONTACT_EMAILS_ID} = ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_EMAIL_ID} WHERE ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID} = :contactGroupId AND ${TABLE_CONTACT_EMAILS}.${COLUMN_CONTACT_EMAILS_EMAIL} LIKE :filter") - abstract fun filterContactsEmailsByContactGroup(contactGroupId: String, filter: String): List + @Query("SELECT ${TABLE_CONTACT_EMAILS}.* FROM $TABLE_CONTACT_EMAILS INNER JOIN $TABLE_CONTACT_EMAILS_LABELS_JOIN ON ${TABLE_CONTACT_EMAILS}.${COLUMN_CONTACT_EMAILS_ID} = ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_EMAIL_ID} WHERE ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID} = :contactGroupId") + fun findAllContactsEmailsByContactGroupAsyncObservable(contactGroupId: String): Flowable> - @Query("SELECT ${TABLE_CONTACT_DATA}.${COLUMN_CONTACT_DATA_NAME},${TABLE_CONTACT_EMAILS}.${COLUMN_CONTACT_EMAILS_EMAIL} FROM $TABLE_CONTACT_DATA JOIN $TABLE_CONTACT_EMAILS ON ${TABLE_CONTACT_DATA}.${COLUMN_CONTACT_DATA_ID}=${TABLE_CONTACT_EMAILS}.${COLUMN_CONTACT_EMAILS_CONTACT_ID}") - abstract fun findAllMessageRecipientsLiveData():LiveData> + @Query("SELECT ${TABLE_CONTACT_LABEL}.* FROM $TABLE_CONTACT_LABEL INNER JOIN $TABLE_CONTACT_EMAILS_LABELS_JOIN ON ${TABLE_CONTACT_LABEL}.${COLUMN_LABEL_ID} = ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID} WHERE ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_EMAIL_ID} = :emailId ORDER BY $COLUMN_LABEL_NAME") + fun findAllContactGroupsByContactEmailAsync(emailId: String): LiveData> - @Query("SELECT ${TABLE_CONTACT_DATA}.${COLUMN_CONTACT_DATA_NAME},${TABLE_CONTACT_EMAILS}.${COLUMN_CONTACT_EMAILS_EMAIL} FROM $TABLE_CONTACT_DATA JOIN $TABLE_CONTACT_EMAILS ON ${TABLE_CONTACT_DATA}.${COLUMN_CONTACT_DATA_ID}=${TABLE_CONTACT_EMAILS}.${COLUMN_CONTACT_EMAILS_CONTACT_ID}") - abstract fun findAllMessageRecipients(): Flowable> + @Query("SELECT ${TABLE_CONTACT_LABEL}.* FROM $TABLE_CONTACT_LABEL INNER JOIN $TABLE_CONTACT_EMAILS_LABELS_JOIN ON ${TABLE_CONTACT_LABEL}.${COLUMN_LABEL_ID} = ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID} WHERE ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_EMAIL_ID} = :emailId ORDER BY $COLUMN_LABEL_NAME") + fun findAllContactGroupsByContactEmailAsyncObservable(emailId: String): Flowable> - @Query("DELETE FROM $TABLE_CONTACT_EMAILS WHERE ${COLUMN_CONTACT_EMAILS_EMAIL}=:email") - abstract fun clearByEmail(email:String) + /** + * Make sure you provide @param filter with included % or ? + */ + @Query("SELECT ${TABLE_CONTACT_EMAILS}.* FROM $TABLE_CONTACT_EMAILS INNER JOIN $TABLE_CONTACT_EMAILS_LABELS_JOIN ON ${TABLE_CONTACT_EMAILS}.${COLUMN_CONTACT_EMAILS_ID} = ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_EMAIL_ID} WHERE ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID} = :contactGroupId AND ${TABLE_CONTACT_EMAILS}.${COLUMN_CONTACT_EMAILS_EMAIL} LIKE :filter") + fun filterContactsEmailsByContactGroupAsyncObservable(contactGroupId: String, filter: String): Flowable> - @Query("DELETE FROM $TABLE_CONTACT_EMAILS") - abstract fun clearContactEmailsCache() + /** + * Make sure you provide @param filter with included % or ? + */ + @Query("SELECT ${TABLE_CONTACT_EMAILS}.* FROM $TABLE_CONTACT_EMAILS INNER JOIN $TABLE_CONTACT_EMAILS_LABELS_JOIN ON ${TABLE_CONTACT_EMAILS}.${COLUMN_CONTACT_EMAILS_ID} = ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_EMAIL_ID} WHERE ${TABLE_CONTACT_EMAILS_LABELS_JOIN}.${COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID} = :contactGroupId AND ${TABLE_CONTACT_EMAILS}.${COLUMN_CONTACT_EMAILS_EMAIL} LIKE :filter") + fun filterContactsEmailsByContactGroup(contactGroupId: String, filter: String): Flow> - @Delete - abstract fun deleteContactEmail(vararg contactEmail:ContactEmail) + @Query("SELECT ${TABLE_CONTACT_DATA}.${COLUMN_CONTACT_DATA_NAME},${TABLE_CONTACT_EMAILS}.${COLUMN_CONTACT_EMAILS_EMAIL} FROM $TABLE_CONTACT_DATA JOIN $TABLE_CONTACT_EMAILS ON ${TABLE_CONTACT_DATA}.${COLUMN_CONTACT_DATA_ID}=${TABLE_CONTACT_EMAILS}.${COLUMN_CONTACT_EMAILS_CONTACT_ID}") + fun findAllMessageRecipientsLiveData(): LiveData> - @Delete - abstract fun deleteAllContactsEmails(contactEmail:Collection) + @Query("SELECT ${TABLE_CONTACT_DATA}.${COLUMN_CONTACT_DATA_NAME},${TABLE_CONTACT_EMAILS}.${COLUMN_CONTACT_EMAILS_EMAIL} FROM $TABLE_CONTACT_DATA JOIN $TABLE_CONTACT_EMAILS ON ${TABLE_CONTACT_DATA}.${COLUMN_CONTACT_DATA_ID}=${TABLE_CONTACT_EMAILS}.${COLUMN_CONTACT_EMAILS_CONTACT_ID}") + fun findAllMessageRecipients(): Flowable> - @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun saveContactEmail(contactEmail: ContactEmail): Long + @Query("DELETE FROM $TABLE_CONTACT_EMAILS WHERE $COLUMN_CONTACT_EMAILS_EMAIL=:email") + fun clearByEmail(email: String) - @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun saveAllContactsEmails(vararg emailData: ContactEmail): List + @Query("DELETE FROM $TABLE_CONTACT_EMAILS") + fun clearContactEmailsCacheBlocking() - @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun saveAllContactsEmails(emailData: Collection): List + @Query("DELETE FROM $TABLE_CONTACT_EMAILS") + suspend fun clearContactEmailsCache() - @Query("SELECT count(*) FROM $TABLE_CONTACT_EMAILS WHERE $COLUMN_CONTACT_EMAILS_LABEL_IDS LIKE :contactGroupId") - abstract fun countContactEmails(contactGroupId: String): Int - //endregion + @Delete + fun deleteContactEmail(vararg contactEmail: ContactEmail) + + @Delete + fun deleteAllContactsEmails(contactEmail: Collection) - //region contacts labels aka contacts groups - @Query("SELECT * FROM $TABLE_CONTACT_LABEL WHERE $COLUMN_LABEL_ID = :labelId") - abstract fun findContactGroupById(labelId: String): ContactLabel? + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun saveContactEmail(contactEmail: ContactEmail): Long - @Query("SELECT * FROM $TABLE_CONTACT_LABEL WHERE $COLUMN_LABEL_ID = :labelId") - abstract fun findContactGroupByIdAsync(labelId: String): Single + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun saveAllContactsEmailsBlocking(vararg emailData: ContactEmail): List - @Query("SELECT * FROM $TABLE_CONTACT_LABEL WHERE $COLUMN_LABEL_NAME = :labelName") - abstract fun findContactGroupByNameAsync(labelName: String): Single + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun saveAllContactsEmailsBlocking(emailData: Collection): List - @Query("SELECT * FROM $TABLE_CONTACT_LABEL WHERE $COLUMN_LABEL_NAME = :groupName") - abstract fun findContactGroupByName(groupName: String): ContactLabel? + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun saveAllContactsEmails(emailData: Collection): List - @Query("SELECT * FROM $TABLE_CONTACT_LABEL ORDER BY $COLUMN_LABEL_NAME") - abstract fun findContactGroupsLiveData(): LiveData> + @Query("SELECT count(*) FROM $TABLE_CONTACT_EMAILS WHERE $COLUMN_CONTACT_EMAILS_LABEL_IDS LIKE :contactGroupId") + fun countContactEmails(contactGroupId: String): Int + //endregion - @Query("SELECT * FROM $TABLE_CONTACT_LABEL ORDER BY $COLUMN_LABEL_NAME") - abstract fun findContactGroupsObservable(): Flowable> + //region contacts labels aka contacts groups + @Query("SELECT * FROM $TABLE_CONTACT_LABEL WHERE $COLUMN_LABEL_ID = :labelId") + suspend fun findContactGroupById(labelId: String): ContactLabel? - @Query("SELECT * FROM $TABLE_CONTACT_LABEL WHERE $COLUMN_LABEL_NAME LIKE :filter ORDER BY $COLUMN_LABEL_NAME") - abstract fun findContactGroupsObservable(filter: String): Flowable> + @Query("SELECT * FROM $TABLE_CONTACT_LABEL WHERE $COLUMN_LABEL_ID = :labelId") + fun findContactGroupByIdBlocking(labelId: String): ContactLabel? - @Query("DELETE FROM $TABLE_CONTACT_LABEL") - abstract fun clearContactGroupsLabelsTable() + @Query("SELECT * FROM $TABLE_CONTACT_LABEL WHERE $COLUMN_LABEL_ID = :labelId") + fun findContactGroupByIdAsync(labelId: String): Single - @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun saveContactGroupLabel(contactLabel: ContactLabel): Long + @Query("SELECT * FROM $TABLE_CONTACT_LABEL WHERE $COLUMN_LABEL_NAME = :labelName") + fun findContactGroupByNameAsync(labelName: String): Single - @Update(onConflict = OnConflictStrategy.REPLACE) - abstract fun updateFullContactGroup(contactLabel: ContactLabel) + @Query("SELECT * FROM $TABLE_CONTACT_LABEL WHERE $COLUMN_LABEL_NAME = :groupName") + fun findContactGroupByName(groupName: String): ContactLabel? - @Query("UPDATE $TABLE_CONTACT_LABEL SET $COLUMN_LABEL_NAME = :name") - protected abstract fun updateName(name: String) + @Query("SELECT * FROM $TABLE_CONTACT_LABEL ORDER BY $COLUMN_LABEL_NAME") + fun findContactGroupsLiveData(): LiveData> - @Query("UPDATE $TABLE_CONTACT_LABEL SET $COLUMN_LABEL_ORDER = :order") - protected abstract fun updateOrder(order: Int) + @Query("SELECT * FROM $TABLE_CONTACT_LABEL ORDER BY $COLUMN_LABEL_NAME") + fun findContactGroupsObservable(): Flowable> - @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun saveAllContactGroups(vararg contactLabels: ContactLabel): List + @Query("SELECT * FROM $TABLE_CONTACT_LABEL ORDER BY $COLUMN_LABEL_NAME") + fun findContactGroups(): Flow> + @Query("SELECT * FROM $TABLE_CONTACT_LABEL WHERE $COLUMN_LABEL_NAME LIKE :filter ORDER BY $COLUMN_LABEL_NAME") + fun findContactGroupsObservable(filter: String): Flowable> - @Query("DELETE FROM $TABLE_CONTACT_LABEL") - abstract fun clearContactGroupsList() + @Query("SELECT * FROM $TABLE_CONTACT_LABEL WHERE $COLUMN_LABEL_NAME LIKE :filter ORDER BY $COLUMN_LABEL_NAME") + fun findContactGroupsFlow(filter: String): Flow> - @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun saveContactGroupsList(contactLabels: List): List + @Query("DELETE FROM $TABLE_CONTACT_LABEL") + fun clearContactGroupsLabelsTableBlocking() - @Query("DELETE FROM $TABLE_CONTACT_LABEL WHERE ${COLUMN_LABEL_ID}=:labelId") - abstract fun deleteByContactGroupLabelId(labelId: String) + @Query("DELETE FROM $TABLE_CONTACT_LABEL") + suspend fun clearContactGroupsLabelsTable() - @Delete - abstract fun deleteContactGroup(contactLabel: ContactLabel) + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun saveContactGroupLabel(contactLabel: ContactLabel): Long - @Query("SELECT * FROM $TABLE_CONTACT_LABEL WHERE $COLUMN_LABEL_ID IN (:labelIds) ORDER BY $COLUMN_LABEL_NAME") - abstract fun getAllContactGroupsByIds(labelIds: List): LiveData> + @Update(onConflict = OnConflictStrategy.REPLACE) + fun updateFullContactGroup(contactLabel: ContactLabel) - fun updatePartially(contactLabel: ContactLabel) { - updateName(contactLabel.name) - updateOrder(contactLabel.order) - } - //endregion + @Query("UPDATE $TABLE_CONTACT_LABEL SET $COLUMN_LABEL_NAME = :name") + fun updateName(name: String) - //region Full contact details - @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun insertFullContactDetails(fullContactDetails:FullContactDetails) + @Query("UPDATE $TABLE_CONTACT_LABEL SET $COLUMN_LABEL_ORDER = :order") + fun updateOrder(order: Int) - @Query("SELECT * FROM $TABLE_FULL_CONTACT_DETAILS WHERE ${COLUMN_CONTACT_ID}=:id") - abstract fun findFullContactDetailsById(id:String):FullContactDetails? + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun saveAllContactGroups(vararg contactLabels: ContactLabel): List - @Query("DELETE FROM $TABLE_FULL_CONTACT_DETAILS") - abstract fun clearFullContactDetailsCache() + @Query("DELETE FROM $TABLE_CONTACT_LABEL") + suspend fun clearContactGroupsList() - @Delete - abstract fun deleteFullContactsDetails(fullContactDetails:FullContactDetails) + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun saveContactGroupsListBlocking(contactLabels: List): List - //endregion + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun saveContactGroupsList(contactLabels: List): List - //region contact emails contact label join - @Query("SELECT count(*) FROM $TABLE_CONTACT_EMAILS_LABELS_JOIN WHERE ${COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID}=:contactGroupId") - abstract fun countContactEmailsByLabelId(contactGroupId: String): Int + @Query("DELETE FROM $TABLE_CONTACT_LABEL WHERE $COLUMN_LABEL_ID=:labelId") + fun deleteByContactGroupLabelId(labelId: String) - @Query("DELETE FROM $TABLE_CONTACT_EMAILS_LABELS_JOIN") - abstract fun clearContactEmailsLabelsJoin() + @Delete + suspend fun deleteContactGroup(contactLabel: ContactLabel) - @Query("SELECT * FROM $TABLE_CONTACT_EMAILS_LABELS_JOIN") - abstract fun fetchJoinsObservable(): Flowable> + @Query("SELECT * FROM $TABLE_CONTACT_LABEL WHERE $COLUMN_LABEL_ID IN (:labelIds) ORDER BY $COLUMN_LABEL_NAME") + fun getAllContactGroupsByIds(labelIds: List): LiveData> - @Query("SELECT * FROM $TABLE_CONTACT_EMAILS_LABELS_JOIN WHERE ${COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID}=:contactGroupId") - abstract fun fetchJoins(contactGroupId: String): List + fun updatePartially(contactLabel: ContactLabel) { + updateName(contactLabel.name) + updateOrder(contactLabel.order) + } + //endregion - @Query("SELECT * FROM $TABLE_CONTACT_EMAILS_LABELS_JOIN WHERE ${COLUMN_CONTACT_EMAILS_LABELS_JOIN_EMAIL_ID}=:contactEmailId") - abstract fun fetchJoinsByEmail(contactEmailId: String): List + //region Full contact details + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertFullContactDetails(fullContactDetails: FullContactDetails) - @Query("DELETE FROM $TABLE_CONTACT_EMAILS_LABELS_JOIN WHERE $COLUMN_CONTACT_EMAILS_LABELS_JOIN_EMAIL_ID IN (:contactEmailIds) AND ${COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID}=:contactGroupId") - abstract fun deleteJoinByGroupIdAndEmailId(contactEmailIds: List, contactGroupId: String) + @Query("SELECT * FROM $TABLE_FULL_CONTACT_DETAILS WHERE $COLUMN_CONTACT_ID=:id") + fun findFullContactDetailsById(id: String): FullContactDetails? - @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun saveContactEmailContactLabel(contactEmailContactLabelJoin: ContactEmailContactLabelJoin): Long + @Query("DELETE FROM $TABLE_FULL_CONTACT_DETAILS") + fun clearFullContactDetailsCache() - @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun saveContactEmailContactLabel(vararg contactEmailContactLabelJoin: ContactEmailContactLabelJoin): List + @Delete + fun deleteFullContactsDetails(fullContactDetails: FullContactDetails) - @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun saveContactEmailContactLabel(contactEmailContactLabelJoin: List): List + //endregion - @Delete - abstract fun deleteContactEmailContactLabel(contactEmailContactLabelJoin: Collection) - //endregion + //region contact emails contact label join + @Query("SELECT count(*) FROM $TABLE_CONTACT_EMAILS_LABELS_JOIN WHERE ${COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID}=:contactGroupId") + fun countContactEmailsByLabelIdBlocking(contactGroupId: String): Int + + @Query("SELECT count(*) FROM $TABLE_CONTACT_EMAILS_LABELS_JOIN WHERE ${COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID}=:contactGroupId") + suspend fun countContactEmailsByLabelId(contactGroupId: String): Int + + @Query("DELETE FROM $TABLE_CONTACT_EMAILS_LABELS_JOIN") + fun clearContactEmailsLabelsJoin() + + @Query("SELECT * FROM $TABLE_CONTACT_EMAILS_LABELS_JOIN") + fun fetchJoins(): Flow> + + @Query("SELECT * FROM $TABLE_CONTACT_EMAILS_LABELS_JOIN WHERE ${COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID}=:contactGroupId") + fun fetchJoins(contactGroupId: String): List + + @Query("SELECT * FROM $TABLE_CONTACT_EMAILS_LABELS_JOIN WHERE ${COLUMN_CONTACT_EMAILS_LABELS_JOIN_EMAIL_ID}=:contactEmailId") + fun fetchJoinsByEmail(contactEmailId: String): List + + @Query("DELETE FROM $TABLE_CONTACT_EMAILS_LABELS_JOIN WHERE $COLUMN_CONTACT_EMAILS_LABELS_JOIN_EMAIL_ID IN (:contactEmailIds) AND ${COLUMN_CONTACT_EMAILS_LABELS_JOIN_LABEL_ID}=:contactGroupId") + fun deleteJoinByGroupIdAndEmailId(contactEmailIds: List, contactGroupId: String) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun saveContactEmailContactLabelBlocking(contactEmailContactLabelJoin: ContactEmailContactLabelJoin): Long + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun saveContactEmailContactLabelBlocking( + vararg contactEmailContactLabelJoin: ContactEmailContactLabelJoin + ): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun saveContactEmailContactLabelBlocking( + contactEmailContactLabelJoin: List + ): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun saveContactEmailContactLabel( + contactEmailContactLabelJoin: List + ): List + + @Delete + fun deleteContactEmailContactLabel(contactEmailContactLabelJoin: Collection) + + @Transaction + suspend fun insertNewContactsAndLabels( + allContactEmails: List, + contactLabelList: List, + allJoins: List + ) { + clearContactEmailsCache() + clearContactGroupsList() + saveContactGroupsList(contactLabelList) + saveAllContactsEmails(allContactEmails) + saveContactEmailContactLabel(allJoins) + } + //endregion } diff --git a/app/src/main/java/ch/protonmail/android/api/models/room/counters/CountersDatabase.kt b/app/src/main/java/ch/protonmail/android/api/models/room/counters/CountersDatabase.kt index 7bf6f1cfd..06f992515 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/room/counters/CountersDatabase.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/room/counters/CountersDatabase.kt @@ -19,7 +19,11 @@ package ch.protonmail.android.api.models.room.counters import androidx.lifecycle.LiveData -import androidx.room.* +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction // TODO remove when we change name of this class to CountersDao and *Factory to *Database typealias CountersDao = CountersDatabase @@ -27,93 +31,95 @@ typealias CountersDao = CountersDatabase @Dao abstract class CountersDatabase { - //region Unread Labels Counters - @Query("SELECT * FROM $TABLE_UNREAD_LABEL_COUNTERS") - abstract fun findAllUnreadLabels():LiveData> + //region Unread Labels Counters + @Query("SELECT * FROM $TABLE_UNREAD_LABEL_COUNTERS") + abstract fun findAllUnreadLabels(): LiveData> - @Query("SELECT * FROM $TABLE_UNREAD_LABEL_COUNTERS WHERE ${COLUMN_COUNTER_ID}=:labelId") - abstract fun findUnreadLabelById(labelId:String):UnreadLabelCounter? + @Query("SELECT * FROM $TABLE_UNREAD_LABEL_COUNTERS WHERE ${COLUMN_COUNTER_ID}=:labelId") + abstract fun findUnreadLabelById(labelId: String): UnreadLabelCounter? - @Query("DELETE FROM $TABLE_UNREAD_LABEL_COUNTERS") - abstract fun clearUnreadLabelsTable() + @Query("DELETE FROM $TABLE_UNREAD_LABEL_COUNTERS") + abstract fun clearUnreadLabelsTable() - @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun insertUnreadLabel(unreadLabel:UnreadLabelCounter) + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract fun insertUnreadLabel(unreadLabel: UnreadLabelCounter) - @Insert(onConflict=OnConflictStrategy.REPLACE) - abstract fun insertAllUnreadLabels(unreadLabels:Collection) + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract fun insertAllUnreadLabels(unreadLabels: Collection) - //endregion + //endregion - //region Unread Locations Counters - @Query("SELECT * FROM $TABLE_UNREAD_LOCATION_COUNTERS WHERE ${COLUMN_COUNTER_ID}=:locationId") - abstract fun findUnreadLocationById(locationId:Int):UnreadLocationCounter? + //region Unread Locations Counters + @Query("SELECT * FROM $TABLE_UNREAD_LOCATION_COUNTERS WHERE ${COLUMN_COUNTER_ID}=:locationId") + abstract fun findUnreadLocationById(locationId: Int): UnreadLocationCounter? - @Query("SELECT * FROM $TABLE_UNREAD_LOCATION_COUNTERS") - abstract fun findAllUnreadLocations():LiveData> + @Query("SELECT * FROM $TABLE_UNREAD_LOCATION_COUNTERS") + abstract fun findAllUnreadLocations(): LiveData> - @Query("DELETE FROM $TABLE_UNREAD_LOCATION_COUNTERS") - abstract fun clearUnreadLocationsTable() + @Query("DELETE FROM $TABLE_UNREAD_LOCATION_COUNTERS") + abstract fun clearUnreadLocationsTable() - @Insert(onConflict=OnConflictStrategy.REPLACE) - abstract fun insertUnreadLocation(unreadLocation:UnreadLocationCounter) + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract fun insertUnreadLocation(unreadLocation: UnreadLocationCounter) - @Insert(onConflict=OnConflictStrategy.REPLACE) - abstract fun insertAllUnreadLocations(unreadLocations:Collection) - //endregion + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract fun insertAllUnreadLocations(unreadLocations: Collection) + //endregion - @Transaction - open fun updateUnreadCounters(locations:Collection,labels:Collection) { - clearUnreadLocationsTable() - clearUnreadLabelsTable() - insertAllUnreadLocations(locations) - insertAllUnreadLabels(labels) - } + @Transaction + open fun updateUnreadCounters( + locations: Collection, + labels: Collection + ) { + clearUnreadLocationsTable() + clearUnreadLabelsTable() + insertAllUnreadLocations(locations) + insertAllUnreadLabels(labels) + } - //region Total Label Counters - @Query("SELECT * FROM $TABLE_TOTAL_LABEL_COUNTERS") - abstract fun findAllTotalLabels():LiveData> + //region Total Label Counters + @Query("SELECT * FROM $TABLE_TOTAL_LABEL_COUNTERS") + abstract fun findAllTotalLabels(): LiveData> - @Query("SELECT * FROM $TABLE_TOTAL_LABEL_COUNTERS WHERE ${COLUMN_COUNTER_ID}=:labelId") - abstract fun findTotalLabelById(labelId:String):TotalLabelCounter? + @Query("SELECT * FROM $TABLE_TOTAL_LABEL_COUNTERS WHERE ${COLUMN_COUNTER_ID}=:labelId") + abstract fun findTotalLabelById(labelId: String): TotalLabelCounter? - @Query("DELETE FROM $TABLE_TOTAL_LABEL_COUNTERS") - abstract fun clearTotalLabelsTable() + @Query("DELETE FROM $TABLE_TOTAL_LABEL_COUNTERS") + abstract fun clearTotalLabelsTable() - @Insert(onConflict=OnConflictStrategy.REPLACE) - protected abstract fun insertTotalLabels(labels:Collection ) - //endregion + @Insert(onConflict = OnConflictStrategy.REPLACE) + protected abstract fun insertTotalLabels(labels: Collection) + //endregion - //region Total Location Counters - @Query("SELECT * FROM $TABLE_TOTAL_LOCATION_COUNTERS WHERE ${COLUMN_COUNTER_ID}=:locationId") - abstract fun findTotalLocationById(locationId:Int):TotalLocationCounter? + //region Total Location Counters + @Query("SELECT * FROM $TABLE_TOTAL_LOCATION_COUNTERS WHERE ${COLUMN_COUNTER_ID}=:locationId") + abstract fun findTotalLocationById(locationId: Int): TotalLocationCounter? - @Query("SELECT * FROM $TABLE_TOTAL_LOCATION_COUNTERS") - abstract fun findAllTotalLocations():LiveData> + @Query("SELECT * FROM $TABLE_TOTAL_LOCATION_COUNTERS") + abstract fun findAllTotalLocations(): LiveData> - @Query("DELETE FROM $TABLE_TOTAL_LOCATION_COUNTERS") - abstract fun clearTotalLocationsTable() + @Query("DELETE FROM $TABLE_TOTAL_LOCATION_COUNTERS") + abstract fun clearTotalLocationsTable() - @Insert(onConflict=OnConflictStrategy.REPLACE) - protected abstract fun insertTotalLocations(locations:Collection) + @Insert(onConflict = OnConflictStrategy.REPLACE) + protected abstract fun insertTotalLocations(locations: Collection) - @Transaction - open fun refreshTotalCounters(locations:Collection,labels:List) { - refreshTotalLocationCounters(locations) - refreshTotalLabelCounters(labels) - } + @Transaction + open fun refreshTotalCounters(locations: Collection, labels: List) { + refreshTotalLocationCounters(locations) + refreshTotalLabelCounters(labels) + } - @Transaction - protected open fun refreshTotalLabelCounters(labels:List) { - clearTotalLabelsTable() - insertTotalLabels(labels) - } - - @Transaction - protected open fun refreshTotalLocationCounters(locations:Collection) { - clearTotalLocationsTable() - insertTotalLocations(locations) - } - //endregion + @Transaction + protected open fun refreshTotalLabelCounters(labels: List) { + clearTotalLabelsTable() + insertTotalLabels(labels) + } + @Transaction + protected open fun refreshTotalLocationCounters(locations: Collection) { + clearTotalLocationsTable() + insertTotalLocations(locations) + } + //endregion } 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/models/room/messages/Message.kt b/app/src/main/java/ch/protonmail/android/api/models/room/messages/Message.kt index c4dcbdad5..569f3c44b 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 @@ -47,7 +46,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,22 +218,21 @@ data class Message @JvmOverloads constructor( val replyToEmails: List get() { return replyTos - ?.asSequence() - ?.filterNot { TextUtils.isEmpty(it.emailAddress) } - ?.map { it.emailAddress } - ?.toList() - ?: Arrays.asList(sender?.emailAddress!!) + .asSequence() + .filter { it.emailAddress.isNotEmpty() } + .map { it.emailAddress } + .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 = @@ -534,6 +531,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/api/models/room/messages/MessagesDatabase.kt b/app/src/main/java/ch/protonmail/android/api/models/room/messages/MessagesDatabase.kt index 23cbb9d00..b261e434a 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/room/messages/MessagesDatabase.kt +++ b/app/src/main/java/ch/protonmail/android/api/models/room/messages/MessagesDatabase.kt @@ -29,6 +29,8 @@ import androidx.room.Query import androidx.room.Transaction import io.reactivex.Flowable import io.reactivex.Single +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map // TODO remove when we change name of this class to MessagesDao and *Factory to *Database typealias MessagesDao = MessagesDatabase @@ -80,10 +82,18 @@ abstract class MessagesDatabase { fun findMessageByIdObservable(messageId: String) = findMessageInfoByIdObservable(messageId) - fun findMessageByMessageDbId(messageDbId: Long) = findMessageInfoByDbId(messageDbId)?.also { + fun findMessageByMessageDbId(messageDbId: Long) = findMessageInfoByDbIdBlocking(messageDbId)?.also { it.Attachments = it.attachments(this) } + fun findMessageByDbId(dbId: Long): Flow = + findMessageInfoByDbId(dbId).map { message -> + return@map message?.let { + it.Attachments = it.attachments(this) + it + } + } + @JvmOverloads fun findAllMessageByLastMessageAccessTime(laterThan: Long = 0) = findAllMessageInfoByLastMessageAccessTime(laterThan).also { it.forEach { message -> @@ -104,9 +114,12 @@ abstract class MessagesDatabase { protected abstract fun findMessageInfoByIdAsync(messageId: String): LiveData @Query("SELECT * FROM $TABLE_MESSAGES WHERE ${BaseColumns._ID}=:messageDbId") - protected abstract fun findMessageInfoByDbId(messageDbId: Long): Message? + protected abstract fun findMessageInfoByDbIdBlocking(messageDbId: Long): Message? + + @Query("SELECT * FROM $TABLE_MESSAGES WHERE ${BaseColumns._ID}=:messageDbId") + protected abstract fun findMessageInfoByDbId(messageDbId: Long): Flow - @Query("SELECT * FROM $TABLE_MESSAGES WHERE ${COLUMN_MESSAGE_ACCESS_TIME}>:laterThan ORDER BY $COLUMN_MESSAGE_ACCESS_TIME") + @Query("SELECT * FROM $TABLE_MESSAGES WHERE $COLUMN_MESSAGE_ACCESS_TIME>:laterThan ORDER BY $COLUMN_MESSAGE_ACCESS_TIME") protected abstract fun findAllMessageInfoByLastMessageAccessTime(laterThan: Long = 0): List @Transaction 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..613ff20a9 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 @@ -27,59 +27,47 @@ import androidx.room.Query // TODO remove when we change name of this class to PendingActionsDao and *Factory to *Database typealias PendingActionsDao = PendingActionsDatabase -/** - * Created by Kamil Rajtar on 14.07.18. - */ @Dao -abstract class PendingActionsDatabase { +interface PendingActionsDatabase { @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun insertPendingForSend(pendingSend: PendingSend) + fun insertPendingForSend(pendingSend: PendingSend) @Query("SELECT * FROM $TABLE_PENDING_SEND") - abstract fun findAllPendingSendsAsync(): LiveData> + fun findAllPendingSendsAsync(): LiveData> @Query("SELECT * FROM $TABLE_PENDING_SEND WHERE ${COLUMN_PENDING_SEND_MESSAGE_ID}=:messageId") - abstract fun findPendingSendByMessageId(messageId: String): PendingSend? + fun findPendingSendByMessageId(messageId: String): PendingSend? @Query("DELETE FROM $TABLE_PENDING_SEND WHERE ${COLUMN_PENDING_SEND_MESSAGE_ID}=:messageId") - abstract fun deletePendingSendByMessageId(messageId: String) + fun deletePendingSendByMessageId(messageId: String) @Query("DELETE FROM $TABLE_PENDING_SEND WHERE ${COLUMN_PENDING_SEND_LOCAL_DB_ID}=:messageDbId") - abstract fun deletePendingSendByDbId(messageDbId: Long) + fun deletePendingSendByDbId(messageDbId: Long) @Query("SELECT * FROM $TABLE_PENDING_SEND WHERE ${COLUMN_PENDING_SEND_OFFLINE_MESSAGE_ID}=:offlineMessageId") - abstract fun findPendingSendByOfflineMessageId(offlineMessageId: String): PendingSend? + fun findPendingSendByOfflineMessageId(offlineMessageId: String): PendingSend? @Query("SELECT * FROM $TABLE_PENDING_SEND WHERE ${COLUMN_PENDING_SEND_OFFLINE_MESSAGE_ID}=:offlineMessageId") - abstract fun findPendingSendByOfflineMessageIdAsync(offlineMessageId: String): LiveData + fun findPendingSendByOfflineMessageIdAsync(offlineMessageId: String): LiveData @Query("SELECT * FROM $TABLE_PENDING_SEND WHERE ${COLUMN_PENDING_SEND_LOCAL_DB_ID}=:dbId") - abstract fun findPendingSendByDbId(dbId: Long): PendingSend? + fun findPendingSendByDbId(dbId: Long): PendingSend? @Query("DELETE FROM $TABLE_PENDING_SEND") - abstract fun clearPendingSendCache() + fun clearPendingSendCache() @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun insertPendingForUpload(pendingUpload: PendingUpload) + fun insertPendingForUpload(pendingUpload: PendingUpload) @Query("SELECT * FROM $TABLE_PENDING_UPLOADS") - abstract fun findAllPendingUploadsAsync(): LiveData> + fun findAllPendingUploadsAsync(): LiveData> @Query("SELECT * FROM $TABLE_PENDING_UPLOADS WHERE ${COLUMN_PENDING_UPLOAD_MESSAGE_ID}=:messageId") - abstract fun findPendingUploadByMessageId(messageId: String): PendingUpload? + fun findPendingUploadByMessageId(messageId: String): PendingUpload? @Query("DELETE FROM $TABLE_PENDING_UPLOADS WHERE $COLUMN_PENDING_UPLOAD_MESSAGE_ID IN (:messageId)") - abstract fun deletePendingUploadByMessageId(vararg messageId: String) + fun deletePendingUploadByMessageId(vararg messageId: String) @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? + fun clearPendingUploadCache() } 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/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/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/contact/ContactApi.kt b/app/src/main/java/ch/protonmail/android/api/segments/contact/ContactApi.kt index ea73e3aa1..98bb01dbe 100644 --- a/app/src/main/java/ch/protonmail/android/api/segments/contact/ContactApi.kt +++ b/app/src/main/java/ch/protonmail/android/api/segments/contact/ContactApi.kt @@ -34,19 +34,16 @@ import ch.protonmail.android.api.utils.ParseUtils import io.reactivex.Completable import io.reactivex.Observable import io.reactivex.Single -import retrofit2.Call import java.io.IOException class ContactApi(private val service: ContactService) : BaseApi(), ContactApiSpec { @Throws(IOException::class) - override fun labelContacts(labelContactsBody: LabelContactsBody): Completable { - return service.labelContacts(labelContactsBody) - } + override fun labelContacts(labelContactsBody: LabelContactsBody): Completable = + service.labelContacts(labelContactsBody) @Throws(IOException::class) - override fun unlabelContactEmailsCompletable(labelContactsBody: LabelContactsBody): Completable { - return service.unlabelContactEmailsCompletable(labelContactsBody) - } + override fun unlabelContactEmailsCompletable(labelContactsBody: LabelContactsBody): Completable = + service.unlabelContactEmailsCompletable(labelContactsBody) override suspend fun unlabelContactEmails(labelContactsBody: LabelContactsBody) = service.unlabelContactEmails(labelContactsBody) @@ -54,27 +51,15 @@ class ContactApi(private val service: ContactService) : BaseApi(), ContactApiSpe override suspend fun fetchContacts(page: Int, pageSize: Int): ContactsDataResponse = service.contacts(page, pageSize) - @Throws(IOException::class) - override fun fetchContactEmails(pageSize: Int): List { - val list = ArrayList() - val pendingRequests = ArrayList>() - val firstPage = service.contactsEmails(0, pageSize).execute().body() - list.add(firstPage!!) - for (i in 1..(firstPage.total + (pageSize - 1)) / pageSize) { - pendingRequests.add(service.contactsEmails(i, pageSize)) - } - list.addAll(executeAll(pendingRequests)) - return list - } + override suspend fun fetchContactEmails(page: Int, pageSize: Int): ContactEmailsResponseV2 = + service.contactsEmails(page, pageSize) - override fun fetchContactsEmailsByLabelId(page: Int, labelId: String): Observable { - return service.contactsEmailsByLabelId(page, labelId) - } + override fun fetchContactsEmailsByLabelId(page: Int, labelId: String): Observable = + service.contactsEmailsByLabelId(page, labelId) @Throws(IOException::class) - override fun fetchContactDetailsBlocking(contactId: String): FullContactDetailsResponse? { - return ParseUtils.parse(service.contactByIdBlocking(contactId).execute()) - } + override fun fetchContactDetailsBlocking(contactId: String): FullContactDetailsResponse? = + ParseUtils.parse(service.contactByIdBlocking(contactId).execute()) override suspend fun fetchContactDetails(contactId: String): FullContactDetailsResponse = service.contactById(contactId) @@ -103,14 +88,12 @@ class ContactApi(private val service: ContactService) : BaseApi(), ContactApiSpe } @Throws(IOException::class) - override fun updateContact(contactId: String, body: CreateContactV2BodyItem): FullContactDetailsResponse? { - return ParseUtils.parse(service.updateContact(contactId, body).execute()) - } + override fun updateContact(contactId: String, body: CreateContactV2BodyItem): FullContactDetailsResponse? = + ParseUtils.parse(service.updateContact(contactId, body).execute()) @Throws(IOException::class) - override fun deleteContactSingle(contactIds: IDList): Single { - return service.deleteContactSingle(contactIds) - } + override fun deleteContactSingle(contactIds: IDList): Single = + service.deleteContactSingle(contactIds) override suspend fun deleteContact(contactIds: IDList): DeleteContactResponse = service.deleteContact(contactIds) diff --git a/app/src/main/java/ch/protonmail/android/api/segments/contact/ContactApiSpec.kt b/app/src/main/java/ch/protonmail/android/api/segments/contact/ContactApiSpec.kt index fea01d96b..7b01bc4e2 100644 --- a/app/src/main/java/ch/protonmail/android/api/segments/contact/ContactApiSpec.kt +++ b/app/src/main/java/ch/protonmail/android/api/segments/contact/ContactApiSpec.kt @@ -36,8 +36,7 @@ interface ContactApiSpec { suspend fun fetchContacts(page: Int, pageSize: Int): ContactsDataResponse - @Throws(IOException::class) - fun fetchContactEmails(pageSize: Int): List + suspend fun fetchContactEmails(page: Int, pageSize: Int): ContactEmailsResponseV2 @Throws(IOException::class) fun fetchContactsEmailsByLabelId(page: Int, labelId: String): Observable diff --git a/app/src/main/java/ch/protonmail/android/api/segments/contact/ContactEmailsManager.kt b/app/src/main/java/ch/protonmail/android/api/segments/contact/ContactEmailsManager.kt index f0831a5d8..5b451eed8 100644 --- a/app/src/main/java/ch/protonmail/android/api/segments/contact/ContactEmailsManager.kt +++ b/app/src/main/java/ch/protonmail/android/api/segments/contact/ContactEmailsManager.kt @@ -19,35 +19,47 @@ package ch.protonmail.android.api.segments.contact import ch.protonmail.android.api.ProtonMailApiManager -import ch.protonmail.android.api.models.DatabaseProvider +import ch.protonmail.android.api.models.ContactEmailsResponseV2 import ch.protonmail.android.api.models.room.contacts.ContactEmail import ch.protonmail.android.api.models.room.contacts.ContactEmailContactLabelJoin -import ch.protonmail.android.api.rx.ThreadSchedulers +import ch.protonmail.android.api.models.room.contacts.ContactsDao import ch.protonmail.android.core.Constants +import kotlinx.coroutines.runBlocking +import timber.log.Timber import javax.inject.Inject class ContactEmailsManager @Inject constructor( - private var apiManager: ProtonMailApiManager, - private val databaseProvider: DatabaseProvider + private var api: ProtonMailApiManager, + private val contactsDao: ContactsDao ) { - private var contactApi: ContactApiSpec = apiManager + suspend fun refresh(pageSize: Int = Constants.CONTACTS_PAGE_SIZE) { + val contactLabelList = api.fetchContactGroupsList() - fun refresh() { - // fetch and prepare data - val contactLabelList = apiManager.fetchContactGroups() - .map { it.contactGroups } - .subscribeOn(ThreadSchedulers.io()) - .observeOn(ThreadSchedulers.io()) - .blockingGet() - val list = contactApi.fetchContactEmails(Constants.CONTACTS_PAGE_SIZE) - val allContactEmails = ArrayList() - list.forEach { - it?.let { - allContactEmails.addAll(it.contactEmails) - } + var currentPage = 0 + var hasMorePages = true + val allResults = mutableListOf() + while (hasMorePages) { + val result = api.fetchContactEmails(currentPage, pageSize) + allResults += result + hasMorePages = currentPage < result.total / pageSize + currentPage++ + } + + if (allResults.isNotEmpty()) { + val allContactEmails = allResults.flatMap { it.contactEmails } + val allJoins = getJoins(allContactEmails) + Timber.v( + "Refresh emails: ${allContactEmails.size}, labels: ${contactLabelList.size}, allJoins: ${allJoins.size}" + ) + contactsDao.insertNewContactsAndLabels(allContactEmails, contactLabelList, allJoins) + } else { + Timber.v("contactEmails result list is empty") } - val allJoins = ArrayList() + } + + private fun getJoins(allContactEmails: List): List { + val allJoins = mutableListOf() for (contactEmail in allContactEmails) { val labelIds = contactEmail.labelIds if (labelIds != null) { @@ -56,14 +68,14 @@ class ContactEmailsManager @Inject constructor( } } } - val contactsDatabase = databaseProvider.provideContactsDatabase() - val contactsDao = databaseProvider.provideContactsDao() - contactsDatabase.runInTransaction { - contactsDao.clearContactEmailsCache() - contactsDao.clearContactGroupsList() - contactsDao.saveContactGroupsList(contactLabelList) - contactsDao.saveAllContactsEmails(allContactEmails) - contactsDao.saveContactEmailContactLabel(allJoins) - } + return allJoins + } + + @Deprecated( + message = "Please use suspended version wherever possible", + replaceWith = ReplaceWith("refresh()") + ) + fun refreshBlocking() = runBlocking { + refresh() } } diff --git a/app/src/main/java/ch/protonmail/android/api/segments/contact/ContactService.kt b/app/src/main/java/ch/protonmail/android/api/segments/contact/ContactService.kt index 9ba628995..e3b08c1fb 100644 --- a/app/src/main/java/ch/protonmail/android/api/segments/contact/ContactService.kt +++ b/app/src/main/java/ch/protonmail/android/api/segments/contact/ContactService.kt @@ -51,7 +51,11 @@ interface ContactService { @GET("contacts/emails") @Headers(CONTENT_TYPE, ACCEPT_HEADER_V1) - fun contactsEmails(@Query("Page") page: Int, @Query("PageSize") pageSize: Int): Call + fun contactsEmailsCall(@Query("Page") page: Int, @Query("PageSize") pageSize: Int): Call + + @GET("contacts/emails") + @Headers(CONTENT_TYPE, ACCEPT_HEADER_V1) + suspend fun contactsEmails(@Query("Page") page: Int, @Query("PageSize") pageSize: Int): ContactEmailsResponseV2 @GET("contacts/emails") @Headers(CONTENT_TYPE, ACCEPT_HEADER_V1) 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..511a8ab87 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 @@ -22,7 +22,6 @@ import android.content.Context 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.DatabaseProvider import ch.protonmail.android.api.models.EventResponse import ch.protonmail.android.api.models.MailSettings import ch.protonmail.android.api.models.MessageCount @@ -43,13 +42,16 @@ import ch.protonmail.android.api.models.room.contacts.ContactEmailContactLabelJo import ch.protonmail.android.api.models.room.contacts.ContactLabel import ch.protonmail.android.api.models.room.contacts.ContactsDao import ch.protonmail.android.api.models.room.contacts.ContactsDatabase +import ch.protonmail.android.api.models.room.counters.CountersDao import ch.protonmail.android.api.models.room.messages.Label 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.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 @@ -59,10 +61,7 @@ import ch.protonmail.android.events.user.MailSettingsEvent import ch.protonmail.android.events.user.UserSettingsEvent import ch.protonmail.android.usecase.fetch.LaunchInitialDataFetch import ch.protonmail.android.utils.AppUtil -import ch.protonmail.android.utils.Logger import ch.protonmail.android.utils.MessageUtils -import ch.protonmail.android.utils.extensions.ifNullElse -import ch.protonmail.android.utils.extensions.ifNullElseReturn import ch.protonmail.android.utils.extensions.removeFirst import ch.protonmail.android.utils.extensions.replaceFirst import ch.protonmail.android.worker.FetchContactsDataWorker @@ -71,40 +70,22 @@ import com.google.gson.JsonSyntaxException import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import timber.log.Timber +import javax.inject.Named import kotlin.collections.set 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) { - DELETE(0), - CREATE(1), - UPDATE(2), - UPDATE_FLAGS(3); - - companion object { - fun fromInt(eventType: Int): EventType { - return values().find { - eventType == it.eventType - } ?: DELETE - } - } -} - class EventHandler @AssistedInject constructor( private val context: Context, private val protonMailApiManager: ProtonMailApiManager, - private val databaseProvider: DatabaseProvider, private val userManager: UserManager, private val messageDetailsRepository: MessageDetailsRepository, private val fetchContactEmails: FetchContactsEmailsWorker.Enqueuer, private val fetchContactsData: FetchContactsDataWorker.Enqueuer, private val launchInitialDataFetch: LaunchInitialDataFetch, + private val pendingActionsDao: PendingActionsDao, + private val contactsDao: ContactsDao, + private val countersDao: CountersDao, + @Named("messages") private val messagesDao: MessagesDao, @Assisted val username: String ) { @@ -114,7 +95,8 @@ class EventHandler @AssistedInject constructor( } private val messageFactory: MessageFactory - private val contactsDao by lazy { databaseProvider.provideContactsDao(username) } + + private val stagedMessages = HashMap() init { val attachmentFactory = AttachmentFactory() @@ -126,8 +108,8 @@ class EventHandler @AssistedInject constructor( fun handleRefreshContacts() { contactsDao.clearContactDataCache() contactsDao.clearContactEmailsLabelsJoin() - contactsDao.clearContactEmailsCache() - contactsDao.clearContactGroupsLabelsTable() + contactsDao.clearContactEmailsCacheBlocking() + contactsDao.clearContactGroupsLabelsTableBlocking() fetchContactEmails.enqueue() fetchContactsData.enqueue() } @@ -138,41 +120,31 @@ class EventHandler @AssistedInject constructor( * anyway. */ fun handleRefresh() { - val messagesDao = databaseProvider.provideMessagesDao(username) messagesDao.clearMessagesCache() messagesDao.clearAttachmentsCache() messagesDao.clearLabelsCache() - val countersDao = databaseProvider.provideCountersDao(username) countersDao.clearUnreadLocationsTable() countersDao.clearUnreadLabelsTable() countersDao.clearTotalLocationsTable() countersDao.clearTotalLabelsTable() - // todo make this done sequentially, don't fire and forget. launchInitialDataFetch( shouldRefreshDetails = false, shouldRefreshContacts = false ) } - private lateinit var response: EventResponse - private val stagedMessages = HashMap() - /** * Does all the pre-processing which does not change the database state * @return Whether the staging process was successful or not */ - fun stage(response: EventResponse): Boolean { - this.response = response - stagedMessages.clear() - val messages = response.messageUpdates - if (messages != null) { + fun stage(messages: MutableList?): Boolean { + if (!messages.isNullOrEmpty()) { return stageMessagesUpdates(messages) } return true } private fun stageMessagesUpdates(events: List): Boolean { - val pendingActionsDao = databaseProvider.providePendingActionsDao(username) for (event in events) { val messageID = event.messageID val type = EventType.fromInt(event.type) @@ -185,49 +157,40 @@ class EventHandler @AssistedInject constructor( continue } - val messageStaged = protonMailApiManager.messageDetail(messageID, RetrofitTag(username)).ifNullElseReturn( - { - // If the response is null, an exception has been thrown while fetching message details - // Return false and with that terminate processing this event any further - // We'll try to process the same event again next time - return@ifNullElseReturn false - }, - { messageResponse -> - // If the response is not null, check the response code and act accordingly - when (messageResponse.code) { - Constants.RESPONSE_CODE_OK -> { - stagedMessages[messageID] = messageResponse.message - } - RESPONSE_CODE_INVALID_ID, - RESPONSE_CODE_MESSAGE_DOES_NOT_EXIST, - RESPONSE_CODE_MESSAGE_READING_RESTRICTED -> { - Timber.tag(TAG_EVENT_HANDLER).e("Error when fetching message: ${messageResponse.error}") - } - else -> { - Timber.tag(TAG_EVENT_HANDLER).e("Error when fetching message") - } + val messageResponse = protonMailApiManager.messageDetail(messageID, RetrofitTag(username)) + val isMessageStaged = if (messageResponse == null) { + // If the response is null, an exception has been thrown while fetching message details + // Return false and with that terminate processing this event any further + // We'll try to process the same event again next time + false + } else { + // If the response is not null, check the response code and act accordingly + when (messageResponse.code) { + Constants.RESPONSE_CODE_OK -> { + stagedMessages[messageID] = messageResponse.message + } + RESPONSE_CODE_INVALID_ID, + RESPONSE_CODE_MESSAGE_DOES_NOT_EXIST, + RESPONSE_CODE_MESSAGE_READING_RESTRICTED -> { + Timber.e("Error when fetching message: ${messageResponse.error}") + } + else -> { + Timber.e("Error when fetching message") } - return@ifNullElseReturn true } - ) + true + } - if (!messageStaged) { + Timber.d("isMessageStaged: $isMessageStaged, messages size: ${stagedMessages.size}") + if (!isMessageStaged) { return false } } return true } - fun write() { - databaseProvider.provideContactsDatabase(username).runInTransaction { - databaseProvider.provideMessagesDatabaseFactory(username).runInTransaction { - val messagesDao = databaseProvider.provideMessagesDao(username) - databaseProvider.providePendingActionsDatabase(username).runInTransaction { - val pendingActionsDao = databaseProvider.providePendingActionsDao(username) - unsafeWrite(contactsDao, messagesDao, pendingActionsDao) - } - } - } + fun write(response: EventResponse) { + unsafeWrite(contactsDao, messagesDao, pendingActionsDao, response) } private fun eventMessageSortSelector(message: EventResponse.MessageEventBody): Int = message.type @@ -238,10 +201,10 @@ class EventHandler @AssistedInject constructor( private fun unsafeWrite( contactsDao: ContactsDao, messagesDao: MessagesDao, - pendingActionsDao: PendingActionsDao + pendingActionsDao: PendingActionsDao, + response: EventResponse ) { - val response = this.response val savedUser = userManager.getUser(username) if (response.usedSpace > 0) { @@ -332,7 +295,7 @@ class EventHandler @AssistedInject constructor( } private fun writeMessagesUpdates( - messagesDatabase: MessagesDatabase, + messagesDatabase: MessagesDao, pendingActionsDatabase: PendingActionsDatabase, events: List ) { @@ -350,32 +313,31 @@ class EventHandler @AssistedInject constructor( private fun writeMessageUpdate( event: EventResponse.MessageEventBody, pendingActionsDatabase: PendingActionsDatabase, - messageID: String, - messagesDatabase: MessagesDatabase + messageId: String, + messagesDatabase: MessagesDao ) { val type = EventType.fromInt(event.type) - if (type != EventType.DELETE && checkPendingForSending(pendingActionsDatabase, messageID)) { + if (type != EventType.DELETE && checkPendingForSending(pendingActionsDatabase, messageId)) { return } + Timber.v("Update message type: $type Id: $messageId") when (type) { EventType.CREATE -> { try { - val savedMessage = messageDetailsRepository.findMessageById(messageID) - savedMessage.ifNullElse( - { - messageDetailsRepository.saveMessageInDB(messageFactory.createMessage(event.message)) - }, - { - updateMessageFlags(messagesDatabase, messageID, event) - } - ) - } catch (e: JsonSyntaxException) { - Logger.doLogException(TAG_EVENT_HANDLER, "unable to create Message object", e) + val savedMessage = messageDetailsRepository.findMessageByIdBlocking(messageId) + if (savedMessage == null) { + messageDetailsRepository.saveMessageInDB(messageFactory.createMessage(event.message)) + } else { + updateMessageFlags(messagesDatabase, messageId, event) + } + + } catch (syntaxException: JsonSyntaxException) { + Timber.w(syntaxException, "unable to create Message object") } } EventType.DELETE -> { - val message = messageDetailsRepository.findMessageById(messageID) + val message = messageDetailsRepository.findMessageByIdBlocking(messageId) if (message != null) { messagesDatabase.deleteMessage(message) } @@ -383,40 +345,42 @@ class EventHandler @AssistedInject constructor( EventType.UPDATE -> { // update Message body - val message = messageDetailsRepository.findMessageById(messageID) - stagedMessages[messageID]?.let { + val message = messageDetailsRepository.findMessageByIdBlocking(messageId) + stagedMessages[messageId]?.let { messageUpdate -> val dbTime = message?.time ?: 0 - val serverTime = it.time + val serverTime = messageUpdate.time if (message != null) { - message.Attachments = it.Attachments + message.Attachments = messageUpdate.Attachments } - if (serverTime > dbTime && message != null && it.messageBody != null) { - message.messageBody = it.messageBody + if (serverTime > dbTime && message != null && messageUpdate.messageBody != null) { + message.messageBody = messageUpdate.messageBody messageDetailsRepository.saveMessageInDB(message) } + Timber.v("Message Id: $messageId processed, staged size:${stagedMessages.size}") + stagedMessages.remove(messageId) } - updateMessageFlags(messagesDatabase, messageID, event) + updateMessageFlags(messagesDatabase, messageId, event) } EventType.UPDATE_FLAGS -> { - updateMessageFlags(messagesDatabase, messageID, event) + updateMessageFlags(messagesDatabase, messageId, event) } } return } private fun updateMessageFlags( - messagesDatabase: MessagesDatabase, - messageID: String, + messagesDatabase: MessagesDao, + messageId: String, item: EventResponse.MessageEventBody ) { - val message = messageDetailsRepository.findMessageById(messageID) + val message = messageDetailsRepository.findMessageByIdBlocking(messageId) val newMessage = item.message - + Timber.v("Update flags message id: $messageId, time: ${message?.time} staged size:${stagedMessages.size}") if (message != null) { if (newMessage.Subject != null) { @@ -493,10 +457,11 @@ class EventHandler @AssistedInject constructor( messageDetailsRepository.saveMessageInDB(message) } } else { - stagedMessages[messageID]?.let { + stagedMessages[messageId]?.let { messageDetailsRepository.saveMessageInDB(it) } } + stagedMessages.remove(messageId) } private fun checkPendingForSending(pendingActionsDao: PendingActionsDao, messageId: String): Boolean { @@ -528,10 +493,10 @@ class EventHandler @AssistedInject constructor( EventType.DELETE -> addresses.removeFirst(matcher) EventType.UPDATE_FLAGS -> { /* Do nothing */ } - else -> throw NotImplementedError("'$type' not implemented") + else -> Timber.w("'$type' not implemented") } - } catch (e: Exception) { - Logger.doLogException(e) + } catch (exception: Exception) { + Timber.e(exception, "writeAddressUpdates exception") } } @@ -541,6 +506,7 @@ class EventHandler @AssistedInject constructor( private fun writeContactsUpdates(contactsDatabase: ContactsDatabase, events: List) { for (event in events) { + Timber.v("New contacts event type: ${event.type} id: ${event.contactID}") when (EventType.fromInt(event.type)) { EventType.CREATE -> { val contact = event.contact @@ -587,18 +553,28 @@ class EventHandler @AssistedInject constructor( events: List ) { for (event in events) { + Timber.v("New contacts emails event type: ${event.type} id: ${event.contactID}") when (EventType.fromInt(event.type)) { EventType.CREATE -> { val contactEmail = event.contactEmail + val contactId = event.contactEmail.contactEmailId // get current contact email saved in local DB - val oldContactEmail = contactsDatabase.findContactEmailById(contactEmail.contactEmailId) + val oldContactEmail = contactsDatabase.findContactEmailById(contactId) if (oldContactEmail != null) { val contactEmailId = oldContactEmail.contactEmailId - val joins = contactsDatabase.fetchJoinsByEmail(contactEmailId) as ArrayList + val joins = contactsDatabase.fetchJoinsByEmail(contactEmailId).toMutableList() contactsDatabase.saveContactEmail(contactEmail) - contactsDatabase.saveContactEmailContactLabel(joins) + contactsDatabase.saveContactEmailContactLabelBlocking(joins) } else { contactsDatabase.saveContactEmail(contactEmail) + val newJoins = mutableListOf() + contactEmail.labelIds?.forEach { labelId -> + newJoins.add(ContactEmailContactLabelJoin(contactEmail.contactEmailId, labelId)) + } + Timber.v("Create new email contact: ${contactEmail.email} newJoins size: ${newJoins.size}") + if (newJoins.isNotEmpty()) { + contactsDatabase.saveContactEmailContactLabelBlocking(newJoins) + } } } @@ -606,17 +582,20 @@ class EventHandler @AssistedInject constructor( val contactId = event.contactEmail.contactEmailId // get current contact email saved in local DB val oldContactEmail = contactsDatabase.findContactEmailById(contactId) + Timber.v("Update contact id: $contactId oldContactEmail: ${oldContactEmail?.email}") if (oldContactEmail != null) { val updatedContactEmail = event.contactEmail val labelIds = updatedContactEmail.labelIds ?: ArrayList() val contactEmailId = updatedContactEmail.contactEmailId contactEmailId.let { - val joins = contactsDatabase.fetchJoinsByEmail(contactEmailId) as ArrayList contactsDatabase.saveContactEmail(updatedContactEmail) + val joins = contactsDatabase.fetchJoinsByEmail(contactEmailId).toMutableList() for (labelId in labelIds) { joins.add(ContactEmailContactLabelJoin(contactEmailId, labelId)) } - contactsDatabase.saveContactEmailContactLabel(joins) + if (joins.isNotEmpty()) { + contactsDatabase.saveContactEmailContactLabelBlocking(joins) + } } } else { contactsDatabase.saveContactEmail(event.contactEmail) @@ -627,6 +606,7 @@ class EventHandler @AssistedInject constructor( val contactId = event.contactID val contactEmail = contactsDatabase.findContactEmailById(contactId) if (contactEmail != null) { + Timber.v("Delete contact id: $contactId") contactsDatabase.deleteContactEmail(contactEmail) } } @@ -638,7 +618,7 @@ class EventHandler @AssistedInject constructor( } private fun writeLabelsUpdates( - messagesDatabase: MessagesDatabase, + messagesDatabase: MessagesDao, contactsDatabase: ContactsDatabase, events: List ) { @@ -669,7 +649,7 @@ class EventHandler @AssistedInject constructor( val label = messagesDatabase.findLabelById(labelId!!) writeMessageLabel(label, item, messagesDatabase) } else if (labelType == Constants.LABEL_TYPE_CONTACT_GROUPS) { - val contactLabel = contactsDatabase.findContactGroupById(labelId!!) + val contactLabel = contactsDatabase.findContactGroupByIdBlocking(labelId!!) writeContactGroup(contactLabel, item, contactsDatabase) } } @@ -691,7 +671,7 @@ class EventHandler @AssistedInject constructor( AppUtil.postEventOnUi(MessageCountsEvent(Status.SUCCESS, response)) } - private fun writeMessageLabel(currentLabel: Label?, updatedLabel: ServerLabel, messagesDatabase: MessagesDatabase) { + private fun writeMessageLabel(currentLabel: Label?, updatedLabel: ServerLabel, messagesDatabase: MessagesDao) { if (currentLabel != null) { val labelFactory = LabelFactory() val labelToSave = labelFactory.createDBObjectFromServerObject(updatedLabel) @@ -709,7 +689,18 @@ class EventHandler @AssistedInject constructor( val labelToSave = contactLabelFactory.createDBObjectFromServerObject(updatedGroup) val joins = contactsDatabase.fetchJoins(labelToSave.ID) contactsDatabase.saveContactGroupLabel(labelToSave) - contactsDatabase.saveContactEmailContactLabel(joins) + contactsDatabase.saveContactEmailContactLabelBlocking(joins) + } + } + + private enum class EventType(val eventType: Int) { + DELETE(0), + CREATE(1), + UPDATE(2), + UPDATE_FLAGS(3); + + companion object { + fun fromInt(eventType: Int) = values().find { eventType == it.eventType } ?: DELETE } } } diff --git a/app/src/main/java/ch/protonmail/android/api/segments/event/EventManager.kt b/app/src/main/java/ch/protonmail/android/api/segments/event/EventManager.kt index 2770ce815..ee869f851 100644 --- a/app/src/main/java/ch/protonmail/android/api/segments/event/EventManager.kt +++ b/app/src/main/java/ch/protonmail/android/api/segments/event/EventManager.kt @@ -147,9 +147,9 @@ class EventManager @Inject constructor( return } Timber.d("EventManager handler stage and write") - if (handler.stage(response)) { + if (handler.stage(response.messageUpdates)) { // Write the updates since the staging was completed without any error - handler.write() + handler.write(response) // Update next event id only after writing updates to local cache has finished successfully backupNextEventId(handler.username, response.eventID) } @@ -160,18 +160,20 @@ class EventManager @Inject constructor( } private fun recoverNextEventId(username: String): String? { - val prefs = sharedPrefs.getOrPut(username, { - protonMailApplication.getSecureSharedPreferences(username) - }) + val prefs = sharedPrefs.getOrPut( + username, + { protonMailApplication.getSecureSharedPreferences(username) } + ) Timber.d("EventManager recoverLastEventId") val lastEventId = prefs.getString(PREF_NEXT_EVENT_ID, null) return if (lastEventId.isNullOrEmpty()) null else lastEventId } private fun backupNextEventId(username: String, eventId: String) { - val prefs = sharedPrefs.getOrPut(username, { - protonMailApplication.getSecureSharedPreferences(username) - }) + val prefs = sharedPrefs.getOrPut( + username, + { protonMailApplication.getSecureSharedPreferences(username) } + ) Timber.d("EventManager backupLastEventId") prefs.edit().putString(PREF_NEXT_EVENT_ID, eventId).apply() } diff --git a/app/src/main/java/ch/protonmail/android/api/segments/label/LabelApi.kt b/app/src/main/java/ch/protonmail/android/api/segments/label/LabelApi.kt index b5812124c..06d98fdaf 100644 --- a/app/src/main/java/ch/protonmail/android/api/segments/label/LabelApi.kt +++ b/app/src/main/java/ch/protonmail/android/api/segments/label/LabelApi.kt @@ -54,6 +54,10 @@ class LabelApi (private val service : LabelService) : BaseApi(), LabelApiSpec { } } + override suspend fun fetchContactGroupsList(): List { + return service.fetchContactGroupsList().contactGroups + } + @Throws(IOException::class) override fun createLabel(label: LabelBody): LabelResponse { return ParseUtils.parse(service.createLabel(label).execute()) diff --git a/app/src/main/java/ch/protonmail/android/api/segments/label/LabelApiSpec.kt b/app/src/main/java/ch/protonmail/android/api/segments/label/LabelApiSpec.kt index 6e26e1016..c61d3123b 100644 --- a/app/src/main/java/ch/protonmail/android/api/segments/label/LabelApiSpec.kt +++ b/app/src/main/java/ch/protonmail/android/api/segments/label/LabelApiSpec.kt @@ -38,6 +38,8 @@ interface LabelApiSpec { @Throws(IOException::class) fun fetchContactGroups(): Single + suspend fun fetchContactGroupsList(): List + @Throws(IOException::class) fun fetchContactGroupsAsObservable(): Observable> diff --git a/app/src/main/java/ch/protonmail/android/api/segments/label/LabelService.kt b/app/src/main/java/ch/protonmail/android/api/segments/label/LabelService.kt index 26d5c3d7b..8aae3d7b4 100644 --- a/app/src/main/java/ch/protonmail/android/api/segments/label/LabelService.kt +++ b/app/src/main/java/ch/protonmail/android/api/segments/label/LabelService.kt @@ -58,6 +58,9 @@ interface LabelService { @GET("labels?" + Fields.Label.TYPE + "=" + Constants.LABEL_TYPE_CONTACT_GROUPS) fun fetchContactGroupsAsObservable(): Observable + @GET("labels?" + Fields.Label.TYPE + "=" + Constants.LABEL_TYPE_CONTACT_GROUPS) + suspend fun fetchContactGroupsList(): ContactGroupsResponse + @POST("labels") @Headers(CONTENT_TYPE, ACCEPT_HEADER_V1) fun createLabel(@Body label: LabelBody): Call 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..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 @@ -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/. */ @@ -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 @@ -118,22 +118,30 @@ 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) + + override suspend fun updateDraft( + messageId: String, + draftBody: DraftBody, + retrofitTag: RetrofitTag + ): MessageResponse = service.updateDraft(messageId, draftBody, 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, 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..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 @@ -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,10 +92,18 @@ 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? + 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 836d361ad..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 @@ -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) @@ -111,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) + suspend 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/api/services/MessagesService.kt b/app/src/main/java/ch/protonmail/android/api/services/MessagesService.kt index dd935547f..4ef444dbe 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 @@ -195,7 +195,7 @@ class MessagesService : JobIntentService() { private fun handleFetchContactGroups() { try { - contactEmailsManager.refresh() + contactEmailsManager.refreshBlocking() } catch (e: Exception) { Timber.i(e, "handleFetchContactGroups has failed") } @@ -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/api/services/PostMessageServiceFactory.kt b/app/src/main/java/ch/protonmail/android/api/services/PostMessageServiceFactory.kt index cb1a04508..fe4b3e137 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,20 +23,12 @@ 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 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 @@ -51,29 +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 - 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 { - 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)) - } - } - 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 @@ -86,14 +60,14 @@ class PostMessageServiceFactory @Inject constructor( } private suspend fun handleMessage(messageDbId: Long, content: String, username: String): Message? { - val message: Message? = messageDetailsRepository.findMessageByMessageDbId(messageDbId, bgDispatcher) + val message: Message? = messageDetailsRepository.findMessageByMessageDbIdBlocking(messageDbId, bgDispatcher) if (message != null) { val crypto = Crypto.forAddress(userManager, username, message.addressID!!) 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") } @@ -101,31 +75,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.saveMessageInDB(message, bgDispatcher) - - 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 - } - message.setLabelIDs(listOf(Constants.MessageLocationType.ALL_DRAFT.messageLocationTypeValue.toString(), Constants.MessageLocationType.ALL_MAIL.messageLocationTypeValue.toString(), Constants.MessageLocationType.DRAFT.messageLocationTypeValue.toString())) - messageDetailsRepository.saveMessageInDB(message, bgDispatcher) - 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())) @@ -138,16 +87,10 @@ 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) } - 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() @@ -167,10 +110,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/api/utils/Fields.kt b/app/src/main/java/ch/protonmail/android/api/utils/Fields.kt index 44c5b9620..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 @@ -135,6 +135,8 @@ object Fields { const val SELF = "self" const val TOTAL = "Total" const val SENDER = "Sender" + const val ID = "ID" + const val UNREAD = "Unread" object Send { const val EXPIRES_IN = "ExpiresIn" 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/AttachmentsViewModel.kt b/app/src/main/java/ch/protonmail/android/attachments/AttachmentsViewModel.kt new file mode 100644 index 000000000..920f6b11f --- /dev/null +++ b/app/src/main/java/ch/protonmail/android/attachments/AttachmentsViewModel.kt @@ -0,0 +1,83 @@ +/* + * 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.attachments + +import androidx.hilt.Assisted +import androidx.hilt.lifecycle.ViewModelInject +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import ch.protonmail.android.activities.AddAttachmentsActivity.EXTRA_DRAFT_ID +import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository +import ch.protonmail.android.api.models.room.messages.Message +import ch.protonmail.android.core.NetworkConnectivityManager +import ch.protonmail.android.utils.MessageUtils +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import me.proton.core.util.kotlin.DispatcherProvider + + +class AttachmentsViewModel @ViewModelInject constructor( + @Assisted private val savedStateHandle: SavedStateHandle, + private val dispatchers: DispatcherProvider, + private val messageDetailsRepository: MessageDetailsRepository, + private val networkConnectivityManager: NetworkConnectivityManager +) : ViewModel() { + + val viewState: MutableLiveData = MutableLiveData() + + fun init() { + viewModelScope.launch(dispatchers.Io) { + val messageId = savedStateHandle.get(EXTRA_DRAFT_ID) ?: return@launch + val message = messageDetailsRepository.findMessageById(messageId) + + message?.let { existingMessage -> + val messageDbId = requireNotNull(existingMessage.dbId) + val messageFlow = messageDetailsRepository.findMessageByDbId(messageDbId) + + if (!networkConnectivityManager.isInternetConnectionPossible()) { + viewState.postValue(AttachmentsViewState.MissingConnectivity) + } + + messageFlow.collect { updatedMessage -> + if (updatedMessage == null) { + return@collect + } + if (!this.isActive) { + return@collect + } + if (draftCreationHappened(existingMessage, updatedMessage)) { + viewState.postValue(AttachmentsViewState.UpdateAttachments(updatedMessage.Attachments)) + this.cancel() + } + } + } + } + } + + private fun draftCreationHappened(existingMessage: Message, updatedMessage: Message) = + !isRemoteMessage(existingMessage) && isRemoteMessage(updatedMessage) + + private fun isRemoteMessage(message: Message) = !MessageUtils.isLocalMessageId(message.messageId) + +} diff --git a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/IMessageFactory.kt b/app/src/main/java/ch/protonmail/android/attachments/AttachmentsViewState.kt similarity index 69% rename from app/src/main/java/ch/protonmail/android/api/models/messages/receive/IMessageFactory.kt rename to app/src/main/java/ch/protonmail/android/attachments/AttachmentsViewState.kt index aeee8b7ff..b69664844 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/IMessageFactory.kt +++ b/app/src/main/java/ch/protonmail/android/attachments/AttachmentsViewState.kt @@ -1,28 +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.api.models.messages.receive -import ch.protonmail.android.api.models.room.messages.Message +package ch.protonmail.android.attachments -/** - * Created by Kamil Rajtar on 18.07.18. */ -interface IMessageFactory { - fun createMessage(serverMessage:ServerMessage):Message - fun createServerMessage(message:Message):ServerMessage +import ch.protonmail.android.api.models.room.messages.Attachment + +sealed class AttachmentsViewState { + object MissingConnectivity : AttachmentsViewState() + data class UpdateAttachments(val attachments: List) : AttachmentsViewState() } 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/attachments/UploadAttachments.kt b/app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt index 58a199121..289211a01 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 ) { @@ -41,48 +44,88 @@ class UploadAttachments @Inject constructor( * Use #UploadAttachments.invoke instead */ @Deprecated("Needed to replace existing logic in legacy java jobs", ReplaceWith("invoke()", "")) - fun blocking(attachmentIds: List, message: Message, crypto: AddressCrypto) = + fun blocking(attachmentIds: List, message: Message, crypto: AddressCrypto, isMessageSending: Boolean) = runBlocking { - invoke(attachmentIds, message, crypto) + invoke(attachmentIds, message, crypto, isMessageSending) } - suspend operator fun invoke(attachmentIds: List, message: Message, crypto: AddressCrypto): Result = + suspend operator fun invoke(attachmentIds: List, message: Message, crypto: AddressCrypto, isMessageSending: Boolean): 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") - for (attachmentId in 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.e("Skipping attachment: either not found, invalid or" + - " was already uploaded = ${attachment?.isUploaded}") - continue + Timber.d( + "Skipping attachment ${attachment?.attachmentId}: " + + "not found, invalid or was already uploaded = ${attachment?.isUploaded}" + ) + 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 -> { + 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 $messageId FAILED.") + pendingActionsDao.deletePendingUploadByMessageId(messageId) + return@withContext Result.Failure(result.error) + } } attachment.deleteLocalFile() } val isAttachPublicKey = userManager.getMailSettings(userManager.username)?.getAttachPublicKey() ?: false - if (isAttachPublicKey) { + if (isAttachPublicKey && isMessageSending) { + 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 } + private suspend fun updateMessageWithUploadedAttachment( + message: Message, + uploadedAttachmentId: String + ) { + val uploadedAttachment = messageDetailsRepository.findAttachmentById(uploadedAttachmentId) + uploadedAttachment?.let { + val attachments = message.Attachments.toMutableList() + attachments + .find { it.fileName == uploadedAttachment.fileName } + ?.let { + attachments.remove(it) + attachments.add(uploadedAttachment) + } + message.setAttachmentList(attachments) + messageDetailsRepository.saveMessageLocally(message) + } + } + 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/compose/ComposeMessageRepository.kt b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageRepository.kt index 1499783cd..c8071c1be 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") 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() @@ -97,7 +99,7 @@ class ComposeMessageRepository @Inject constructor( .flatMap { list -> Observable.fromIterable(list) .map { - it.contactEmailsCount = tempContactsDao.countContactEmailsByLabelId(it.ID) + it.contactEmailsCount = tempContactsDao.countContactEmailsByLabelIdBlocking(it.ID) it } .toList() @@ -148,14 +150,14 @@ class ComposeMessageRepository @Inject constructor( withContext(dispatcher) { var message: Message? = null if (!TextUtils.isEmpty(draftId)) { - message = messageDetailsRepository.findMessageById(draftId) + message = messageDetailsRepository.findMessageByIdBlocking(draftId) } message } - suspend fun deleteMessageById(messageId: String, dispatcher: CoroutineDispatcher) = - withContext(dispatcher) { + suspend fun deleteMessageById(messageId: String) = + withContext(dispatchers.Io) { messagesDatabase.deleteMessageById(messageId) } @@ -214,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 56f0ab685..243a94ac8 100644 --- a/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt +++ b/app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt @@ -28,6 +28,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.liveData import androidx.lifecycle.switchMap import androidx.lifecycle.viewModelScope +import androidx.work.WorkManager import ch.protonmail.android.R import ch.protonmail.android.activities.composeMessage.MessageBuilderData import ch.protonmail.android.activities.composeMessage.UserAction @@ -48,11 +49,12 @@ 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 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 @@ -60,22 +62,26 @@ 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 ch.protonmail.android.worker.drafts.SAVE_DRAFT_UNIQUE_WORK_ID_PREFIX 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.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collect 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 -import java.util.concurrent.atomic.AtomicBoolean 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 = "<" @@ -89,6 +95,10 @@ class ComposeMessageViewModel @Inject constructor( private val postMessageServiceFactory: PostMessageServiceFactory, private val deleteMessage: DeleteMessage, private val fetchPublicKeys: FetchPublicKeys, + private val saveDraft: SaveDraft, + private val dispatchers: DispatcherProvider, + private val stringResourceResolver: StringResourceResolver, + private val workManager: WorkManager, verifyConnection: VerifyConnection, networkConfigurator: NetworkConfigurator ) : ConnectivityBaseViewModel(verifyConnection, networkConfigurator) { @@ -101,20 +111,19 @@ 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 _savingDraftError: MutableLiveData = MutableLiveData() private val _deleteResult: MutableLiveData> = MutableLiveData() 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() private val fetchKeyDetailsTrigger = MutableLiveData>() private val _androidContacts = java.util.ArrayList() - private val _protonMailContacts = java.util.ArrayList() + private val _protonMailContacts = mutableSetOf() private var _protonMailGroups: List = java.util.ArrayList() private var _androidContactsLoaded: Boolean = false private var _protonMailContactsLoaded: Boolean = false @@ -159,8 +168,10 @@ class ComposeMessageViewModel @Inject constructor( get() = _closeComposer val setupCompleteValue: Boolean get() = _setupCompleteValue - val savingDraftComplete: LiveData> + val savingDraftComplete: LiveData get() = _savingDraftComplete + val savingDraftError: LiveData + get() = _savingDraftError val senderAddresses: List get() = _senderAddresses val deleteResult: LiveData> @@ -209,10 +220,9 @@ class ComposeMessageViewModel @Inject constructor( } val parentId: String? get() = _parentId - // endregion - val messageDraftResult: LiveData - get() = _messageDraftResult + internal var autoSaveJob: Job? = null + // endregion private val loggedInUsernames = if (userManager.user.combinedContacts) { AccountManager.getInstance(ProtonMailApplication.getApplication().applicationContext).getLoggedInUsers() @@ -339,7 +349,7 @@ class ComposeMessageViewModel @Inject constructor( } } - private fun saveAttachmentsToDatabase( + private fun filterUploadedAttachments( localAttachments: List, uploadAttachments: Boolean ): List { @@ -360,12 +370,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) { @@ -383,34 +387,22 @@ class ComposeMessageViewModel @Inject constructor( } } - fun removePendingDraft() { - viewModelScope.launch { - _dbId?.let { - removePendingDraft(it) - } - } - } - - fun insertPendingDraft() { - viewModelScope.launch { - _dbId?.let { - insertPendingDraft(it, IO) - } - } - } - - fun saveDraft(message: Message, parentId: String?, hasConnectivity: Boolean) { + @SuppressLint("GlobalCoroutineUsage") + fun saveDraft(message: Message, hasConnectivity: Boolean) { val uploadAttachments = _messageDataResult.uploadAttachments - GlobalScope.launch { + // 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, 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 } @@ -418,11 +410,8 @@ class ComposeMessageViewModel @Inject constructor( message.messageId = draftId val newAttachments = calculateNewAttachments(uploadAttachments) - postMessageServiceFactory.startUpdateDraftService( - _dbId!!, - message.decryptedBody ?: "", - newAttachments, uploadAttachments, _oldSenderAddressId - ) + invokeSaveDraftUseCase(message, newAttachments, parentId, _actionId, _oldSenderAddressId) + if (newAttachments.isNotEmpty() && uploadAttachments) { _oldSenderAddressId = message.addressID ?: _messageDataResult.addressId // overwrite "old sender ID" when updating draft @@ -431,35 +420,25 @@ class ComposeMessageViewModel @Inject constructor( //endregion } else { //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 = saveAttachmentsToDatabase( - composeMessageRepository.createAttachmentList(_messageDataResult.attachmentList, IO), + saveMessage(message) + newAttachmentIds = filterUploadedAttachments( + composeMessageRepository.createAttachmentList(_messageDataResult.attachmentList, dispatchers.Io), uploadAttachments ) } - postMessageServiceFactory.startCreateDraftService( - _dbId!!, - _draftId.get(), - parentId, - _actionId, message.decryptedBody ?: "", - uploadAttachments, - newAttachments, - _oldSenderAddressId, - _messageDataResult.isTransient - ) + invokeSaveDraftUseCase(message, newAttachmentIds, parentId, _actionId, _oldSenderAddressId) + _oldSenderAddressId = "" setIsDirty(false) //endregion @@ -469,14 +448,58 @@ class ComposeMessageViewModel @Inject constructor( } } + private suspend fun invokeSaveDraftUseCase( + message: Message, + newAttachments: List, + parentId: String?, + messageActionType: Constants.MessageActionType, + oldSenderAddress: String + ) { + saveDraft( + SaveDraft.SaveDraftParameters( + message, + newAttachments, + parentId, + messageActionType, + oldSenderAddress + ) + ).collect { saveDraftResult -> + when (saveDraftResult) { + is SaveDraftResult.Success -> onDraftSaved(saveDraftResult.draftId) + SaveDraftResult.OnlineDraftCreationFailed -> { + val errorMessage = stringResourceResolver( + R.string.failed_saving_draft_online + ).format(message.subject) + _savingDraftError.postValue(errorMessage) + } + SaveDraftResult.UploadDraftAttachmentsFailed -> { + val errorMessage = stringResourceResolver(R.string.attachment_failed) + message.subject + _savingDraftError.postValue(errorMessage) + } + SaveDraftResult.SendingInProgressError -> { + } + } + } + } + + private suspend fun onDraftSaved(savedDraftId: String) { + val draft = requireNotNull(messageDetailsRepository.findMessageById(savedDraftId)) + + viewModelScope.launch(dispatchers.Main) { + _draftId.set(draft.messageId) + watchForMessageSent() + } + _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 // we need to compare them and find out which are new attachments if (uploadAttachments && localAttachmentsList.isNotEmpty()) { - newAttachments = saveAttachmentsToDatabase( - composeMessageRepository.createAttachmentList(localAttachmentsList, IO), uploadAttachments + newAttachments = filterUploadedAttachments( + composeMessageRepository.createAttachmentList(localAttachmentsList, dispatchers.Io), uploadAttachments ) } val currentAttachmentsList = messageDataResult.attachmentList @@ -484,18 +507,8 @@ class ComposeMessageViewModel @Inject constructor( return newAttachments } - private suspend fun removePendingDraft(messageDbId: Long) = - withContext(IO) { - 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) { + private suspend fun saveMessage(message: Message): Long = + withContext(dispatchers.Io) { messageDetailsRepository.saveMessageInDB(message) } @@ -565,8 +578,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) @@ -594,7 +607,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 @@ -645,10 +661,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() @@ -668,65 +684,11 @@ class ComposeMessageViewModel @Inject constructor( } } - 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 { - 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())) - removePendingDraft() + if (_draftId.get().isNotEmpty()) { + deleteMessage(listOf(_draftId.get())) + } } } @@ -748,12 +710,11 @@ 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) - // and sending the message - val savedMessage = messageDetailsRepository.findMessageByMessageDbId(_dbId!!, IO) + // 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.findMessageByMessageDbIdBlocking(_dbId!!, dispatchers.Io) message.dbId = _dbId savedMessage?.let { if (!TextUtils.isEmpty(it.localId)) { @@ -761,15 +722,20 @@ class ComposeMessageViewModel @Inject constructor( } else { message.messageId = _draftId.get() } - saveMessage(message, IO) + saveMessage(message) } } if (_dbId != null) { - messageDetailsRepository.deletePendingDraft(message.dbId!!) + val newAttachments = calculateNewAttachments(true) + // Cancel scheduled save draft work to allow attachments removal while offline + // This is needed to replace the logic to block draft creation while sending, which being in + // SaveDraft use case has no effect when CreateDraft is scheduled offline. As this logic + // was removed in the send refactor, this solution was adopted over moving the check in CreateDraftWorker + val saveDraftUniqueWorkId = "$SAVE_DRAFT_UNIQUE_WORK_ID_PREFIX-${message.messageId})" + workManager.cancelUniqueWork(saveDraftUniqueWorkId) - val newAttachments = calculateNewAttachments(true) postMessageServiceFactory.startSendingMessage( _dbId!!, messageDataResult.message.decryptedBody ?: "", @@ -791,7 +757,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) @@ -808,21 +774,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 (_messageDataResult.addressId.isNotEmpty()) { + 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) @@ -1103,7 +1067,7 @@ class ComposeMessageViewModel @Inject constructor( _androidContactsLoaded = true if (_androidContacts.size > 0) { _protonMailContacts.addAll(_androidContacts) - _androidMessageRecipientsResult.postValue(_protonMailContacts) + _androidMessageRecipientsResult.postValue(_protonMailContacts.toList()) _mergedContactsLiveData.removeSource(contactGroupsResult) _mergedContactsLiveData.addSource(androidMessageRecipientsResult) { value -> _mergedContactsLiveData.postValue(value) @@ -1139,13 +1103,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) @@ -1195,7 +1152,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) } @@ -1258,7 +1215,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()) @@ -1280,7 +1237,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 @@ -1289,4 +1246,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/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/contacts/details/ContactDetailsActivity.java b/app/src/main/java/ch/protonmail/android/contacts/details/ContactDetailsActivity.java index 52a5bc4de..9ef1e5e0e 100644 --- a/app/src/main/java/ch/protonmail/android/contacts/details/ContactDetailsActivity.java +++ b/app/src/main/java/ch/protonmail/android/contacts/details/ContactDetailsActivity.java @@ -602,7 +602,7 @@ public void onContactEvent(ContactEvent event) { public void onContactDetailsLoadedEvent(FetchContactDetailsResult result) { - Timber.v("FetchContactDetailsResult %s", result); + Timber.v("FetchContactDetailsResult received"); if (result instanceof FetchContactDetailsResult.Data) { if (mErrorEncryptedView != null) { mErrorEncryptedView.setVisibility(View.GONE); diff --git a/app/src/main/java/ch/protonmail/android/contacts/details/ContactDetailsRepository.kt b/app/src/main/java/ch/protonmail/android/contacts/details/ContactDetailsRepository.kt index 0e28b0ec5..811b73815 100644 --- a/app/src/main/java/ch/protonmail/android/contacts/details/ContactDetailsRepository.kt +++ b/app/src/main/java/ch/protonmail/android/contacts/details/ContactDetailsRepository.kt @@ -68,8 +68,8 @@ open class ContactDetailsRepository @Inject constructor( private fun getContactGroupsFromApi(): Observable> { return api.fetchContactGroupsAsObservable().doOnNext { - contactsDao.clearContactGroupsLabelsTable() - contactsDao.saveContactGroupsList(it) + contactsDao.clearContactGroupsLabelsTableBlocking() + contactsDao.saveContactGroupsListBlocking(it) } } @@ -78,7 +78,7 @@ open class ContactDetailsRepository @Inject constructor( .flatMap { list -> Observable.fromIterable(list) .map { - it.contactEmailsCount = contactsDao.countContactEmailsByLabelId(it.ID) + it.contactEmailsCount = contactsDao.countContactEmailsByLabelIdBlocking(it.ID) it } .toList() @@ -94,7 +94,7 @@ open class ContactDetailsRepository @Inject constructor( .doOnComplete { val joins = contactsDao.fetchJoins(contactLabel.ID) contactsDao.saveContactGroupLabel(contactLabel) - contactsDao.saveContactEmailContactLabel(joins) + contactsDao.saveContactEmailContactLabelBlocking(joins) } .doOnError { throwable -> if (throwable is IOException) { @@ -118,7 +118,7 @@ open class ContactDetailsRepository @Inject constructor( for (contactEmail in membersList) { joins.add(ContactEmailContactLabelJoin(contactEmail, contactGroupId)) } - contactsDao.saveContactEmailContactLabel(joins) + contactsDao.saveContactEmailContactLabelBlocking(joins) } .doOnError { throwable -> if (throwable is IOException) { @@ -168,7 +168,7 @@ open class ContactDetailsRepository @Inject constructor( contactId?.let { val localContactEmails = contactsDao.findContactEmailsByContactId(it) contactsDao.deleteAllContactsEmails(localContactEmails) - contactsDao.saveAllContactsEmails(contactServerEmails) + contactsDao.saveAllContactsEmailsBlocking(contactServerEmails) } } } diff --git a/app/src/main/java/ch/protonmail/android/contacts/groups/details/ContactGroupDetailsActivity.kt b/app/src/main/java/ch/protonmail/android/contacts/groups/details/ContactGroupDetailsActivity.kt index efa4fb0f0..83bffdbdb 100644 --- a/app/src/main/java/ch/protonmail/android/contacts/groups/details/ContactGroupDetailsActivity.kt +++ b/app/src/main/java/ch/protonmail/android/contacts/groups/details/ContactGroupDetailsActivity.kt @@ -28,7 +28,7 @@ import android.text.TextUtils import android.text.TextWatcher import android.view.Menu import android.view.MenuItem -import androidx.lifecycle.ViewModelProvider +import androidx.activity.viewModels import ch.protonmail.android.R import ch.protonmail.android.activities.BaseActivity import ch.protonmail.android.contacts.groups.ContactGroupEmailsAdapter @@ -54,10 +54,10 @@ const val EXTRA_CONTACT_GROUP = "extra_contact_group" class ContactGroupDetailsActivity : BaseActivity() { @Inject - lateinit var contactGroupDetailsViewModelFactory: ContactGroupDetailsViewModelFactory + lateinit var app: ProtonMailApplication - private lateinit var contactGroupDetailsViewModel: ContactGroupDetailsViewModel private lateinit var contactGroupEmailsAdapter: ContactGroupEmailsAdapter + private val contactGroupDetailsViewModel: ContactGroupDetailsViewModel by viewModels() override fun getLayoutId() = R.layout.activity_contact_group_details @@ -65,12 +65,8 @@ class ContactGroupDetailsActivity : BaseActivity() { super.onCreate(savedInstanceState) setSupportActionBar(animToolbar) - if (supportActionBar != null) - supportActionBar!!.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayHomeAsUpEnabled(true) - contactGroupDetailsViewModel = - ViewModelProvider(this, contactGroupDetailsViewModelFactory) - .get(ContactGroupDetailsViewModel::class.java) initAdapter() startObserving() val bundle = intent?.getBundleExtra(EXTRA_CONTACT_GROUP) @@ -86,12 +82,12 @@ class ContactGroupDetailsActivity : BaseActivity() { override fun onStart() { super.onStart() - ProtonMailApplication.getApplication().bus.register(this) + app.bus.register(this) } override fun onStop() { super.onStop() - ProtonMailApplication.getApplication().bus.unregister(this) + app.bus.unregister(this) } override fun onCreateOptionsMenu(menu: Menu?): Boolean { @@ -147,10 +143,11 @@ class ContactGroupDetailsActivity : BaseActivity() { ) private fun startObserving() { - contactGroupDetailsViewModel.contactGroupEmailsResult.observe(this) { - contactGroupEmailsAdapter.setData(it ?: ArrayList()) - if (it != null && TextUtils.isEmpty(filterView.text.toString())) { - setTitle(contactGroupDetailsViewModel.getData()?.name, it.size) + contactGroupDetailsViewModel.contactGroupEmailsResult.observe(this) { list -> + Timber.v("New contacts emails list size: ${list.size}") + contactGroupEmailsAdapter.setData(list ?: ArrayList()) + if (list != null && TextUtils.isEmpty(filterView.text.toString())) { + setTitle(contactGroupDetailsViewModel.getData()?.name, list.size) } } @@ -158,10 +155,10 @@ class ContactGroupDetailsActivity : BaseActivity() { contactGroupEmailsAdapter.setData(ArrayList()) } - contactGroupDetailsViewModel.setupUIData.observe(this) { - val colorString = UiUtil.normalizeColor(it?.color) + contactGroupDetailsViewModel.setupUIData.observe(this) { contactLabel -> + val colorString = UiUtil.normalizeColor(contactLabel?.color) val color = Color.parseColor(colorString) - initCollapsingToolbar(color, it.name, it.contactEmailsCount) + initCollapsingToolbar(color, contactLabel.name, contactLabel.contactEmailsCount) } contactGroupDetailsViewModel.deleteGroupStatus.observe(this) { @@ -192,7 +189,8 @@ class ContactGroupDetailsActivity : BaseActivity() { override fun afterTextChanged(editable: Editable?) { contactGroupDetailsViewModel.doFilter(filterView.text.toString()) } - }) + } + ) } } @@ -201,8 +199,8 @@ class ContactGroupDetailsActivity : BaseActivity() { R.id.action_delete -> consume { DialogUtils.showDeleteConfirmationDialog( this, getString(R.string.delete), - resources.getQuantityString(R.plurals.are_you_sure_delete_group, 1)) - { + resources.getQuantityString(R.plurals.are_you_sure_delete_group, 1) + ) { contactGroupDetailsViewModel.delete() } } diff --git a/app/src/main/java/ch/protonmail/android/contacts/groups/details/ContactGroupDetailsRepository.kt b/app/src/main/java/ch/protonmail/android/contacts/groups/details/ContactGroupDetailsRepository.kt index 55a18d252..0ac648d1d 100644 --- a/app/src/main/java/ch/protonmail/android/contacts/groups/details/ContactGroupDetailsRepository.kt +++ b/app/src/main/java/ch/protonmail/android/contacts/groups/details/ContactGroupDetailsRepository.kt @@ -27,14 +27,13 @@ import ch.protonmail.android.api.models.factories.makeInt import ch.protonmail.android.api.models.room.contacts.ContactEmail import ch.protonmail.android.api.models.room.contacts.ContactLabel import ch.protonmail.android.worker.PostLabelWorker -import com.birbit.android.jobqueue.JobManager import io.reactivex.Observable import io.reactivex.Single +import kotlinx.coroutines.flow.Flow import java.io.IOException import javax.inject.Inject class ContactGroupDetailsRepository @Inject constructor( - private val jobManager: JobManager, private val api: ProtonMailApiManager, private val databaseProvider: DatabaseProvider, private val workManager: WorkManager @@ -42,37 +41,39 @@ class ContactGroupDetailsRepository @Inject constructor( private val contactsDatabase by lazy { /*TODO*/ Log.d("PMTAG", "instantiating contactsDatabase in ContactGroupDetailsRepository"); databaseProvider.provideContactsDao() } - fun findContactGroupDetails(id: String): Single { - return contactsDatabase.findContactGroupByIdAsync(id) - } + fun findContactGroupDetailsBlocking(id: String): Single = + contactsDatabase.findContactGroupByIdAsync(id) + + suspend fun findContactGroupDetails(id: String): ContactLabel? = + contactsDatabase.findContactGroupById(id) - fun getContactGroupEmails(id: String): Observable> { + fun getContactGroupEmailsBlocking(id: String): Observable> { return contactsDatabase.findAllContactsEmailsByContactGroupAsyncObservable(id) - .toObservable() + .toObservable() } - fun filterContactGroupEmails(id: String, filter: String): Observable> { - val filterString = "%$filter%" - return contactsDatabase.filterContactsEmailsByContactGroupAsyncObservable(id, filterString) - .toObservable() - } + fun getContactGroupEmails(id: String): Flow> = + contactsDatabase.findAllContactsEmailsByContactGroupIdFlow(id) + + fun filterContactGroupEmails(id: String, filter: String): Flow> = + contactsDatabase.filterContactsEmailsByContactGroup(id, "%$filter%") fun createContactGroup(contactLabel: ContactLabel): Single { val contactLabelConverterFactory = ContactLabelFactory() val labelBody = contactLabelConverterFactory.createServerObjectFromDBObject(contactLabel) return api.createLabelCompletable(labelBody.labelBody) - .doOnSuccess { label -> contactsDatabase.saveContactGroupLabel(label) } - .doOnError { throwable -> - if (throwable is IOException) { - PostLabelWorker.Enqueuer(workManager).enqueue( - contactLabel.name, - contactLabel.color, - contactLabel.display, - contactLabel.exclusive.makeInt(), - false, - contactLabel.ID - ) - } + .doOnSuccess { label -> contactsDatabase.saveContactGroupLabel(label) } + .doOnError { throwable -> + if (throwable is IOException) { + PostLabelWorker.Enqueuer(workManager).enqueue( + contactLabel.name, + contactLabel.color, + contactLabel.display, + contactLabel.exclusive.makeInt(), + false, + contactLabel.ID + ) } + } } } diff --git a/app/src/main/java/ch/protonmail/android/contacts/groups/details/ContactGroupDetailsViewModel.kt b/app/src/main/java/ch/protonmail/android/contacts/groups/details/ContactGroupDetailsViewModel.kt index 65485ee8d..2e7c9b2ff 100644 --- a/app/src/main/java/ch/protonmail/android/contacts/groups/details/ContactGroupDetailsViewModel.kt +++ b/app/src/main/java/ch/protonmail/android/contacts/groups/details/ContactGroupDetailsViewModel.kt @@ -18,32 +18,40 @@ */ package ch.protonmail.android.contacts.groups.details -import android.annotation.SuppressLint +import android.database.SQLException +import androidx.hilt.lifecycle.ViewModelInject import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.liveData import androidx.lifecycle.map import androidx.lifecycle.switchMap +import androidx.lifecycle.viewModelScope import ch.protonmail.android.api.models.room.contacts.ContactEmail import ch.protonmail.android.api.models.room.contacts.ContactLabel -import ch.protonmail.android.api.rx.ThreadSchedulers import ch.protonmail.android.contacts.ErrorEnum import ch.protonmail.android.usecase.delete.DeleteLabel import ch.protonmail.android.utils.Event -import com.jakewharton.rxrelay2.PublishRelay -import java.util.concurrent.TimeUnit -import javax.inject.Inject - -class ContactGroupDetailsViewModel @Inject constructor( +import kotlinx.coroutines.channels.BroadcastChannel +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import timber.log.Timber +import kotlin.time.milliseconds + +class ContactGroupDetailsViewModel @ViewModelInject constructor( private val contactGroupDetailsRepository: ContactGroupDetailsRepository, private val deleteLabel: DeleteLabel ) : ViewModel() { private lateinit var _contactLabel: ContactLabel - private lateinit var _data: List private val _contactGroupEmailsResult: MutableLiveData> = MutableLiveData() - private val _filteringPublishSubject = PublishRelay.create() + private val filteringChannel = BroadcastChannel(1) private val _contactGroupEmailsEmpty: MutableLiveData> = MutableLiveData() private val _setupUIData = MutableLiveData() private val _deleteLabelIds: MutableLiveData> = MutableLiveData() @@ -85,69 +93,65 @@ class ContactGroupDetailsViewModel @Inject constructor( contactLabel?.let { newContact -> _contactLabel = newContact getContactGroupEmails(newContact) - watchForContactGroup() - _setupUIData.postValue(newContact) + _setupUIData.value = newContact } } - fun getData(): ContactLabel? = _contactLabel - - @SuppressLint("CheckResult") - private fun watchForContactGroup() { - contactGroupDetailsRepository.findContactGroupDetails(_contactLabel.ID) - .subscribeOn(ThreadSchedulers.io()) - .observeOn(ThreadSchedulers.main()) - .subscribe( - { - _contactLabel = it - if (::_data.isInitialized) { - _contactGroupEmailsResult.postValue(_data) - } - }, - { - _contactGroupEmailsEmpty.value = Event(it.message ?: ErrorEnum.DEFAULT_ERROR.name) - - } - ) - } + fun getData(): ContactLabel = _contactLabel - @SuppressLint("CheckResult") private fun getContactGroupEmails(contactLabel: ContactLabel) { contactGroupDetailsRepository.getContactGroupEmails(contactLabel.ID) - .subscribeOn(ThreadSchedulers.io()) - .observeOn(ThreadSchedulers.main()) - .subscribe( - { - _data = it - watchForContactGroup() - _contactGroupEmailsResult.postValue(it) + .onEach { list -> + updateContactGroup() + _contactGroupEmailsResult.postValue(list) + } + .catch { throwable -> + _contactGroupEmailsEmpty.value = Event( + throwable.message ?: ErrorEnum.INVALID_EMAIL_LIST.name + ) + } + .launchIn(viewModelScope) + } + + private suspend fun updateContactGroup() { + runCatching { contactGroupDetailsRepository.findContactGroupDetails(_contactLabel.ID) } + .fold( + onSuccess = { contactLabel -> + Timber.v("ContactLabel: $contactLabel retrieved") + contactLabel?.let { label -> + _contactLabel = label + _setupUIData.value = label + } }, - { - _contactGroupEmailsEmpty.value = Event(it.message ?: ErrorEnum.INVALID_EMAIL_LIST.name) + onFailure = { throwable -> + if (throwable is SQLException) { + _contactGroupEmailsEmpty.value = Event(throwable.message ?: ErrorEnum.DEFAULT_ERROR.name) + } else + throw throwable } ) } - @SuppressLint("CheckResult") private fun initFiltering() { - _filteringPublishSubject - .debounce(300, TimeUnit.MILLISECONDS) + filteringChannel + .asFlow() + .debounce(300.milliseconds) .distinctUntilChanged() - .switchMap { contactGroupDetailsRepository.filterContactGroupEmails(_contactLabel.ID, it) } - .subscribeOn(ThreadSchedulers.io()) - .observeOn(ThreadSchedulers.main()) - .subscribe( - { - _contactGroupEmailsResult.postValue(it) - }, - { - _contactGroupEmailsEmpty.value = Event(it.message ?: ErrorEnum.DEFAULT_ERROR.name) - } - ) + .flatMapLatest { contactGroupDetailsRepository.filterContactGroupEmails(_contactLabel.ID, it) } + .catch { + _contactGroupEmailsEmpty.value = Event(it.message ?: ErrorEnum.DEFAULT_ERROR.name) + } + .onEach { list -> + Timber.v("Filtered emails list size: ${list.size}") + _contactGroupEmailsResult.postValue(list) + } + .launchIn(viewModelScope) } fun doFilter(filter: String) { - _filteringPublishSubject.accept(filter.trim()) + viewModelScope.launch { + filteringChannel.send(filter.trim()) + } } fun delete() { diff --git a/app/src/main/java/ch/protonmail/android/contacts/groups/details/ContactGroupDetailsViewModelFactory.kt b/app/src/main/java/ch/protonmail/android/contacts/groups/details/ContactGroupDetailsViewModelFactory.kt deleted file mode 100644 index 258965718..000000000 --- a/app/src/main/java/ch/protonmail/android/contacts/groups/details/ContactGroupDetailsViewModelFactory.kt +++ /dev/null @@ -1,36 +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.contacts.groups.details - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import javax.inject.Inject - -/** - * Created by kadrikj on 8/23/18. */ -class ContactGroupDetailsViewModelFactory @Inject constructor( - private val contactGroupDetailsViewModel: ContactGroupDetailsViewModel): ViewModelProvider.NewInstanceFactory() { - - override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(ContactGroupDetailsViewModel::class.java)) { - return contactGroupDetailsViewModel as T - } - throw IllegalArgumentException("Unknown class name") - } -} \ No newline at end of file diff --git a/app/src/main/java/ch/protonmail/android/contacts/groups/edit/ContactGroupEditCreateRepository.kt b/app/src/main/java/ch/protonmail/android/contacts/groups/edit/ContactGroupEditCreateRepository.kt index c670c5d2d..0b3a2f76f 100644 --- a/app/src/main/java/ch/protonmail/android/contacts/groups/edit/ContactGroupEditCreateRepository.kt +++ b/app/src/main/java/ch/protonmail/android/contacts/groups/edit/ContactGroupEditCreateRepository.kt @@ -53,7 +53,7 @@ class ContactGroupEditCreateRepository @Inject constructor( .doOnComplete { val joins = contactsDao.fetchJoins(contactLabel.ID) contactsDao.saveContactGroupLabel(contactLabel) - contactsDao.saveContactEmailContactLabel(joins) + contactsDao.saveContactEmailContactLabelBlocking(joins) } .doOnError { throwable -> if (throwable is IOException) { @@ -102,7 +102,7 @@ class ContactGroupEditCreateRepository @Inject constructor( list.add(ContactEmailContactLabelJoin(contactEmail, contactGroupId)) } getContactGroupEmails(contactGroupId).test().values() - contactsDao.saveContactEmailContactLabel(list) + contactsDao.saveContactEmailContactLabelBlocking(list) }.doOnError { throwable -> if (throwable is IOException) { jobManager.addJobInBackground(SetMembersForContactGroupJob(contactGroupId, contactGroupName, membersList)) diff --git a/app/src/main/java/ch/protonmail/android/contacts/groups/list/ContactGroupsFragment.kt b/app/src/main/java/ch/protonmail/android/contacts/groups/list/ContactGroupsFragment.kt index 057a81033..ef42611df 100644 --- a/app/src/main/java/ch/protonmail/android/contacts/groups/list/ContactGroupsFragment.kt +++ b/app/src/main/java/ch/protonmail/android/contacts/groups/list/ContactGroupsFragment.kt @@ -24,19 +24,20 @@ import android.os.Bundle import android.view.ActionMode import android.view.Menu import android.view.MenuItem +import android.view.View import android.view.View.GONE import android.view.View.VISIBLE import androidx.annotation.Px import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.core.view.updatePadding +import androidx.fragment.app.viewModels import androidx.recyclerview.widget.LinearLayoutManager import ch.protonmail.android.R import ch.protonmail.android.activities.composeMessage.ComposeMessageActivity import ch.protonmail.android.activities.fragments.BaseFragment import ch.protonmail.android.api.models.MessageRecipient import ch.protonmail.android.api.models.room.contacts.ContactLabel -import ch.protonmail.android.api.rx.ThreadSchedulers import ch.protonmail.android.contacts.IContactsFragment import ch.protonmail.android.contacts.IContactsListFragmentListener import ch.protonmail.android.contacts.groups.details.ContactGroupDetailsActivity @@ -48,31 +49,23 @@ import ch.protonmail.android.utils.extensions.setDefaultIfEmpty import ch.protonmail.android.utils.extensions.showToast import ch.protonmail.android.utils.ui.dialogs.DialogUtils import ch.protonmail.android.utils.ui.selection.SelectionModeEnum -import ch.protonmail.libs.core.utils.ViewModelProvider import dagger.hilt.android.AndroidEntryPoint import kotlinx.android.synthetic.main.fragment_contacts_groups.* import timber.log.Timber import java.io.Serializable -import javax.inject.Inject // region constants private const val TAG_CONTACT_GROUPS_FRAGMENT = "ProtonMail.ContactGroupsFragment" // endregion -/* - * Created by kadrikj on 8/24/18. - */ - @AndroidEntryPoint class ContactGroupsFragment : BaseFragment(), IContactsFragment { - @Inject - lateinit var contactGroupsViewModelFactory: ContactGroupsViewModelFactory - private lateinit var contactGroupsViewModel: ContactGroupsViewModel private lateinit var contactGroupsAdapter: ContactsGroupsListAdapter + private val contactGroupsViewModel: ContactGroupsViewModel by viewModels() override var actionMode: ActionMode? = null private set - + private val listener: IContactsListFragmentListener by lazy { requireActivity() as IContactsListFragmentListener } @@ -82,9 +75,7 @@ class ContactGroupsFragment : BaseFragment(), IContactsFragment { position: Int, id: Long, checked: Boolean - ) { - // NOOP - } + ) = Unit override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { when (item?.itemId) { @@ -94,7 +85,8 @@ class ContactGroupsFragment : BaseFragment(), IContactsFragment { requireContext().resources.getQuantityString( R.plurals.are_you_sure_delete_group, contactGroupsAdapter.getSelectedItems!!.toList().size, - contactGroupsAdapter.getSelectedItems!!.toList().size) + contactGroupsAdapter.getSelectedItems!!.toList().size + ) ) { onDelete() mode!!.finish() @@ -135,13 +127,12 @@ class ContactGroupsFragment : BaseFragment(), IContactsFragment { UiUtil.setStatusBarColor( activity as AppCompatActivity, ContextCompat.getColor(requireContext(), R.color.dark_purple_statusbar) - ) listener.setTitle(getString(R.string.contacts)) } - override fun onContactPermissionChange(hasPermission: Boolean) { } + override fun onContactPermissionChange(hasPermission: Boolean) {} override fun getLayoutResourceId() = R.layout.fragment_contacts_groups @@ -149,11 +140,8 @@ class ContactGroupsFragment : BaseFragment(), IContactsFragment { override fun getSearchListener(): ISearchListenerViewModel = contactGroupsViewModel - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - - contactGroupsViewModel = ViewModelProvider(this, contactGroupsViewModelFactory) - .get(ContactGroupsViewModel::class.java) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) initAdapter() } @@ -162,11 +150,6 @@ class ContactGroupsFragment : BaseFragment(), IContactsFragment { startObserving() } - companion object { - - fun newInstance() = ContactGroupsFragment() - } - private fun initAdapter() { var actionMode: ActionMode? = null @@ -197,24 +180,24 @@ class ContactGroupsFragment : BaseFragment(), IContactsFragment { } private fun startObserving() { - contactGroupsViewModel.contactGroupsResult.observe(this, { - Timber.v("contactGroupsResult $it") - if (it.isEmpty()) { + contactGroupsViewModel.contactGroupsResult.observe(this) { list -> + Timber.d("contactGroupsResult size: ${list.size}") + if (list.isEmpty()) { noResults.visibility = VISIBLE } else { noResults.visibility = GONE } - listener.dataUpdated(1, it?.size ?: 0) - contactGroupsAdapter.setData(it ?: ArrayList()) - }) + listener.dataUpdated(1, list?.size ?: 0) + contactGroupsAdapter.setData(list ?: ArrayList()) + } - contactGroupsViewModel.contactGroupsError.observe(this, { event -> - event?.getContentIfNotHandled()?.let { it -> + contactGroupsViewModel.contactGroupsError.observe(this) { event -> + event?.getContentIfNotHandled()?.let { + Timber.i("contactGroupsResult Error: $it") context?.showToast(it.setDefaultIfEmpty(getString(R.string.default_error_message))) } - }) - contactGroupsViewModel.fetchContactGroups(ThreadSchedulers.main()) - contactGroupsViewModel.watchForJoins(ThreadSchedulers.main()) + } + contactGroupsViewModel.observeContactGroups() } private fun onContactGroupSelect() { @@ -234,7 +217,6 @@ class ContactGroupsFragment : BaseFragment(), IContactsFragment { startActivity(AppUtil.decorInAppIntent(detailsIntent)) } - override fun onDelete() { contactGroupsViewModel.deleteSelected(contactGroupsAdapter.getSelectedItems!!.toList()) } @@ -245,23 +227,31 @@ class ContactGroupsFragment : BaseFragment(), IContactsFragment { context?.showToast(R.string.paid_plan_needed) return } - contactGroupsViewModel.contactGroupEmailsResult.observe(this, { event -> - event?.getContentIfNotHandled()?.let { + contactGroupsViewModel.contactGroupEmailsResult.observe(this) { event -> + event?.getContentIfNotHandled()?.let { list -> + Timber.v("Contact email list received $list") composeIntent.putExtra( - ComposeMessageActivity.EXTRA_TO_RECIPIENT_GROUPS, it.asSequence().map { email -> - MessageRecipient(email.name, email.email, contactGroup.name) - }.toList() as Serializable + ComposeMessageActivity.EXTRA_TO_RECIPIENT_GROUPS, + list.asSequence().map { email -> + MessageRecipient(email.name, email.email, contactGroup.name) + }.toList() as Serializable ) startActivity(AppUtil.decorInAppIntent(composeIntent)) contactGroupsViewModel.contactGroupEmailsResult.removeObservers(this) } - }) + } - contactGroupsViewModel.contactGroupEmailsError.observe(this, { event -> - event?.getContentIfNotHandled()?.let { - context?.showToast(it.setDefaultIfEmpty(getString(R.string.default_error_message))) + contactGroupsViewModel.contactGroupEmailsError.observe(this) { event -> + event?.getContentIfNotHandled()?.let { message -> + context?.showToast( + if (message.isNotBlank()) { + message + } else { + getString(R.string.default_error_message) + } + ) } - }) + } contactGroupsViewModel.getContactGroupEmails(contactGroup) } @@ -273,4 +263,8 @@ class ContactGroupsFragment : BaseFragment(), IContactsFragment { actionMode = null } } + + companion object { + fun newInstance() = ContactGroupsFragment() + } } diff --git a/app/src/main/java/ch/protonmail/android/contacts/groups/list/ContactGroupsRepository.kt b/app/src/main/java/ch/protonmail/android/contacts/groups/list/ContactGroupsRepository.kt index 0786ec66d..9a436ffad 100644 --- a/app/src/main/java/ch/protonmail/android/contacts/groups/list/ContactGroupsRepository.kt +++ b/app/src/main/java/ch/protonmail/android/contacts/groups/list/ContactGroupsRepository.kt @@ -23,71 +23,43 @@ import ch.protonmail.android.api.models.room.contacts.ContactEmail import ch.protonmail.android.api.models.room.contacts.ContactEmailContactLabelJoin import ch.protonmail.android.api.models.room.contacts.ContactLabel import ch.protonmail.android.api.models.room.contacts.ContactsDao -import io.reactivex.Observable -import java.util.concurrent.TimeUnit +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import me.proton.core.util.kotlin.DispatcherProvider import javax.inject.Inject class ContactGroupsRepository @Inject constructor( private val api: ProtonMailApiManager, - private val contactsDao: ContactsDao + private val contactsDao: ContactsDao, + private val dispatchers: DispatcherProvider ) { - fun getJoins(): Observable> { - return contactsDao.fetchJoinsObservable().toObservable() - } - - fun getContactGroups(): Observable> { - return Observable.concatArrayDelayError( - getContactGroupsFromDB(), - getContactGroupsFromApi().debounce(400, TimeUnit.MILLISECONDS) - ) - } + fun getJoins(): Flow> = contactsDao.fetchJoins() - fun getContactGroups(filter: String): Observable> { - return getContactGroupsFromDB(filter) - } + fun observeContactGroups(filter: String): Flow> = + contactsDao.findContactGroupsFlow("$SEARCH_DELIMITER$filter$SEARCH_DELIMITER") + .map { labels -> + labels.map { label -> label.contactEmailsCount = contactsDao.countContactEmailsByLabelId(label.ID) } + labels + } + .flowOn(dispatchers.Io) - fun getContactGroupEmails(id: String): Observable> { - return contactsDao.findAllContactsEmailsByContactGroupAsyncObservable(id) - .toObservable() - } + suspend fun getContactGroupEmails(id: String): List = + contactsDao.findAllContactsEmailsByContactGroupId(id) fun saveContactGroup(contactLabel: ContactLabel) { contactsDao.saveContactGroupLabel(contactLabel) } - private fun getContactGroupsFromApi(): Observable> { - return api.fetchContactGroupsAsObservable().doOnNext { + suspend fun getContactGroupsFromApi(): List { + return api.fetchContactGroupsList().also { labels -> contactsDao.clearContactGroupsLabelsTable() - contactsDao.saveContactGroupsList(it) + contactsDao.saveContactGroupsList(labels) } } - private fun getContactGroupsFromDB(): Observable> { - return contactsDao.findContactGroupsObservable() - .flatMap { list -> - Observable.fromIterable(list) - .map { - it.contactEmailsCount = contactsDao.countContactEmailsByLabelId(it.ID) - it - } - .toList() - .toFlowable() - } - .toObservable() - } - - private fun getContactGroupsFromDB(filter: String): Observable> { - return contactsDao.findContactGroupsObservable(filter) - .flatMap { list -> - Observable.fromIterable(list) - .map { - it.contactEmailsCount = contactsDao.countContactEmailsByLabelId(it.ID) - it - } - .toList() - .toFlowable() - } - .toObservable() + private companion object { + private const val SEARCH_DELIMITER = "%" } } diff --git a/app/src/main/java/ch/protonmail/android/contacts/groups/list/ContactGroupsViewModel.kt b/app/src/main/java/ch/protonmail/android/contacts/groups/list/ContactGroupsViewModel.kt index 3b78f2a00..913833245 100644 --- a/app/src/main/java/ch/protonmail/android/contacts/groups/list/ContactGroupsViewModel.kt +++ b/app/src/main/java/ch/protonmail/android/contacts/groups/list/ContactGroupsViewModel.kt @@ -18,26 +18,29 @@ */ package ch.protonmail.android.contacts.groups.list -import android.annotation.SuppressLint -import android.text.TextUtils +import android.database.SQLException +import androidx.hilt.lifecycle.ViewModelInject import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import ch.protonmail.android.api.models.room.contacts.ContactEmail import ch.protonmail.android.api.models.room.contacts.ContactLabel -import ch.protonmail.android.api.rx.ThreadSchedulers import ch.protonmail.android.contacts.ErrorEnum import ch.protonmail.android.contacts.list.search.ISearchListenerViewModel import ch.protonmail.android.core.UserManager import ch.protonmail.android.usecase.delete.DeleteLabel import ch.protonmail.android.utils.Event -import io.reactivex.Scheduler +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import timber.log.Timber -import javax.inject.Inject -class ContactGroupsViewModel @Inject constructor( +class ContactGroupsViewModel @ViewModelInject constructor( private val contactGroupsRepository: ContactGroupsRepository, private val userManager: UserManager, private val deleteLabel: DeleteLabel @@ -45,12 +48,10 @@ class ContactGroupsViewModel @Inject constructor( private val _contactGroupsResult: MutableLiveData> = MutableLiveData() private val _contactGroupsError: MutableLiveData> = MutableLiveData() - private var _searchPhrase: String = "" + private var searchPhraseFlow = MutableStateFlow("") private val _contactGroupEmailsResult: MutableLiveData>> = MutableLiveData() private val _contactGroupEmailsError: MutableLiveData> = MutableLiveData() - private lateinit var _contactGroups: List - val contactGroupsResult: LiveData> get() = _contactGroupsResult @@ -63,51 +64,25 @@ class ContactGroupsViewModel @Inject constructor( val contactGroupEmailsError: LiveData> get() = _contactGroupEmailsError - @SuppressLint("CheckResult") - fun watchForJoins(schedulers: Scheduler) { + fun observeContactGroups() { + // observe db changes contactGroupsRepository.getJoins() - .subscribeOn(ThreadSchedulers.io()) - .observeOn(schedulers) - .subscribe { - fetchContactGroups(schedulers) + .combine(searchPhraseFlow) { _, searchPhrase -> searchPhrase } + .onEach { Timber.v("Search term: $it") } + .flatMapLatest { searchPhrase -> + contactGroupsRepository.observeContactGroups(searchPhrase) } + .catch { _contactGroupsError.value = Event(it.message ?: ErrorEnum.INVALID_EMAIL_LIST.name) } + .onEach { labels -> + Timber.d("Contacts groups labels received size: ${labels.size}") + _contactGroupsResult.value = labels + } + .launchIn(viewModelScope) } - @SuppressLint("CheckResult") - fun fetchContactGroups(schedulers: Scheduler) { - if (_searchPhrase.isEmpty()) { - contactGroupsRepository.getContactGroups().subscribeOn(ThreadSchedulers.io()) - .observeOn(schedulers).subscribe( - { - _contactGroups = it - _contactGroupsResult.postValue(it) - }, - { - _contactGroupsError.value = Event(it.message ?: ErrorEnum.INVALID_EMAIL_LIST.name) - } - ) - } else { - setSearchPhrase(_searchPhrase) - } - } - - @SuppressLint("CheckResult") override fun setSearchPhrase(searchPhrase: String?) { if (searchPhrase != null) { - _searchPhrase = searchPhrase - if (TextUtils.isEmpty(searchPhrase)) { - fetchContactGroups(ThreadSchedulers.main()) // todo move this out of a depedency - } - val searchPhraseQuery = "%$searchPhrase%" - contactGroupsRepository.getContactGroups(searchPhraseQuery).subscribeOn(ThreadSchedulers.io()) - .observeOn(ThreadSchedulers.main()).subscribe( - { - _contactGroupsResult.postValue(it) - }, - { - _contactGroupsError.value = Event(it.message ?: ErrorEnum.INVALID_GROUP_LIST.name) - } - ) + searchPhraseFlow.value = searchPhrase } } @@ -121,19 +96,25 @@ class ContactGroupsViewModel @Inject constructor( } } - @SuppressLint("CheckResult") fun getContactGroupEmails(contactLabel: ContactLabel) { - contactGroupsRepository.getContactGroupEmails(contactLabel.ID) - .subscribeOn(ThreadSchedulers.io()) - .observeOn(ThreadSchedulers.main()).subscribe( - { - _contactGroupEmailsResult.postValue(Event(it)) - }, - { - _contactGroupEmailsError.value = Event(it.message ?: ErrorEnum.INVALID_EMAIL_LIST.name) - } - ) + viewModelScope.launch { + runCatching { contactGroupsRepository.getContactGroupEmails(contactLabel.ID) } + .fold( + onSuccess = { list -> + Timber.v("Contacts groups emails list received size: ${list.size}") + _contactGroupEmailsResult.value = Event(list) + }, + onFailure = { throwable -> + if (throwable is SQLException) { + _contactGroupEmailsError.value = Event( + throwable.message ?: ErrorEnum.INVALID_EMAIL_LIST.name + ) + } else + throw throwable + } + ) + } } - fun isPaidUser(): Boolean = userManager.user?.isPaidUser ?: false + fun isPaidUser(): Boolean = userManager.user.isPaidUser } diff --git a/app/src/main/java/ch/protonmail/android/contacts/groups/list/ContactsGroupListAdapter.kt b/app/src/main/java/ch/protonmail/android/contacts/groups/list/ContactsGroupListAdapter.kt index de8ad0a55..8da300229 100644 --- a/app/src/main/java/ch/protonmail/android/contacts/groups/list/ContactsGroupListAdapter.kt +++ b/app/src/main/java/ch/protonmail/android/contacts/groups/list/ContactsGroupListAdapter.kt @@ -34,11 +34,11 @@ import ch.protonmail.android.utils.ui.selection.SelectionModeEnum import kotlinx.android.synthetic.main.contacts_v2_list_item.view.* class ContactsGroupsListAdapter( - var items: List, + private var items: List, private val onContactGroupClickListener: (ContactLabel) -> Unit, private val onWriteToGroupClickListener: (ContactLabel) -> Unit, private val onContactGroupSelect: (() -> Unit)?, - val onSelectionModeChange: ((SelectionModeEnum) -> Unit)? + private val onSelectionModeChange: ((SelectionModeEnum) -> Unit)? ) : RecyclerView.Adapter() { private var selectedItems: MutableSet? = null @@ -46,7 +46,7 @@ class ContactsGroupsListAdapter( val getSelectedItems get() = selectedItems override fun onBindViewHolder(holder: ViewHolder, position: Int) { - (holder).bind( + holder.bind( items[position], onContactGroupClickListener, onWriteToGroupClickListener, @@ -58,7 +58,7 @@ class ContactsGroupsListAdapter( selectedItems?.forEach { if (items.contains(it)) { items.find { contactLabel -> (contactLabel == it) }?.isSelected = - ContactEmailGroupSelectionState.DEFAULT + ContactEmailGroupSelectionState.DEFAULT } } selectedItems = null @@ -76,7 +76,7 @@ class ContactsGroupsListAdapter( itemView.contact_name.text = contactLabel.name val members = contactLabel.contactEmailsCount - itemView.contact_email.text = itemView.context.resources.getQuantityString( + itemView.contact_subtitle.text = itemView.context.resources.getQuantityString( R.plurals.contact_group_members, members, members @@ -198,9 +198,7 @@ class ContactsGroupsListAdapter( notifyDataSetChanged() } - override fun getItemCount(): Int { - return items.size - } + override fun getItemCount(): Int = items.size } class ViewHolder(view: View) : RecyclerView.ViewHolder(view) diff --git a/app/src/main/java/ch/protonmail/android/contacts/list/ContactsListFragment.kt b/app/src/main/java/ch/protonmail/android/contacts/list/ContactsListFragment.kt index 0d4653c8d..56c57d9c1 100644 --- a/app/src/main/java/ch/protonmail/android/contacts/list/ContactsListFragment.kt +++ b/app/src/main/java/ch/protonmail/android/contacts/list/ContactsListFragment.kt @@ -27,13 +27,13 @@ import android.os.Bundle import android.view.ActionMode import android.view.Menu import android.view.MenuItem +import android.view.View import android.view.View.GONE import android.view.View.VISIBLE import androidx.annotation.Px import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.core.view.updatePadding -import androidx.lifecycle.Observer import androidx.loader.app.LoaderManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.work.Operation @@ -197,7 +197,7 @@ class ContactsListFragment : BaseFragment(), IContactsFragment { override fun onStart() { super.onStart() listener.registerObject(this) - startObserving() + viewModel.fetchContactItems() } override fun onStop() { @@ -209,8 +209,8 @@ class ContactsListFragment : BaseFragment(), IContactsFragment { } } - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) val loaderManager = LoaderManager.getInstance(this) val application = activity!!.application @@ -225,41 +225,39 @@ class ContactsListFragment : BaseFragment(), IContactsFragment { initAdapter() listener.selectPage(0) listener.doRequestContactsPermission() + startObserving() } private fun startObserving() { - viewModel.contactItems.observe( - viewLifecycleOwner, - { contactItems -> - if (contactItems.isEmpty()) { - noResults.visibility = VISIBLE - } else { - noResults.visibility = GONE - } - contactsAdapter.apply { - setData(contactItems!!) - val count = contactItems.size - contactItems - .count { contactItem -> contactItem.contactId == "-1" } - listener.dataUpdated(0, count) - } + viewModel.contactItems.observe(viewLifecycleOwner) { contactItems -> + Timber.v("New Contact items: $contactItems size: ${contactItems.size}") + if (contactItems.isEmpty()) { + noResults.visibility = VISIBLE + } else { + noResults.visibility = GONE } - ) + contactsAdapter.apply { + setData(contactItems) + val count = contactItems.size - contactItems + .count { contactItem -> contactItem.contactId == "-1" } + listener.dataUpdated(0, count) + } + } + val progressDialogFactory = ProgressDialogFactory(requireContext()) - viewModel.uploadProgress.observe( - viewLifecycleOwner, + viewModel.uploadProgress.observe(viewLifecycleOwner) { UploadProgressObserver(progressDialogFactory::create) - ) - viewModel.contactToConvert.observe( - viewLifecycleOwner, - Observer { - val localContact = it?.getContentIfNotHandled() ?: return@Observer - val intent = EditContactDetailsActivity.startConvertContactActivity( - requireContext(), - localContact - ) - listener.doStartActivityForResult(intent, REQUEST_CODE_CONVERT_CONTACT) - } - ) + } + + viewModel.contactToConvert.observe(viewLifecycleOwner) { event -> + Timber.v("ContactToConvert event: $event") + val localContact = event?.getContentIfNotHandled() ?: return@observe + val intent = EditContactDetailsActivity.startConvertContactActivity( + requireContext(), + localContact + ) + listener.doStartActivityForResult(intent, REQUEST_CODE_CONVERT_CONTACT) + } } override fun getLayoutResourceId() = R.layout.fragment_contacts diff --git a/app/src/main/java/ch/protonmail/android/contacts/list/LocalContactsConverter.kt b/app/src/main/java/ch/protonmail/android/contacts/list/LocalContactsConverter.kt index 98904d67f..2d7e0d743 100644 --- a/app/src/main/java/ch/protonmail/android/contacts/list/LocalContactsConverter.kt +++ b/app/src/main/java/ch/protonmail/android/contacts/list/LocalContactsConverter.kt @@ -23,11 +23,13 @@ import ch.protonmail.android.contacts.list.viewModel.IContactsListViewModel import ch.protonmail.android.jobs.ConvertLocalContactsJob import com.birbit.android.jobqueue.JobManager -class LocalContactsConverter(private val jobManager:JobManager, - private val viewModel:IContactsListViewModel) { - fun startConversion(contacts:List){ - viewModel.setProgress(0) - viewModel.setProgressMax(contacts.size) - jobManager.addJobInBackground(ConvertLocalContactsJob(contacts)) - } -} \ No newline at end of file +class LocalContactsConverter( + private val jobManager: JobManager, + private val viewModel: IContactsListViewModel +) { + fun startConversion(contacts: List) { + viewModel.setProgress(0) + viewModel.setProgressMax(contacts.size) + jobManager.addJobInBackground(ConvertLocalContactsJob(contacts)) + } +} diff --git a/app/src/main/java/ch/protonmail/android/contacts/list/listView/ContactsListAdapter.kt b/app/src/main/java/ch/protonmail/android/contacts/list/listView/ContactsListAdapter.kt index 91ebb8498..85f7de157 100644 --- a/app/src/main/java/ch/protonmail/android/contacts/list/listView/ContactsListAdapter.kt +++ b/app/src/main/java/ch/protonmail/android/contacts/list/listView/ContactsListAdapter.kt @@ -19,7 +19,6 @@ package ch.protonmail.android.contacts.list.listView import android.content.Context -import android.util.Log import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView @@ -45,7 +44,7 @@ class ContactsListAdapter( val getSelectedItems get() = selectedItems override fun onBindViewHolder(holder: ViewHolder, position: Int) { - (holder).bind( + holder.bind( items[position], onContactGroupClickListener, onContactGroupSelect, @@ -114,15 +113,11 @@ class ContactsListAdapter( } private fun getItemType(position: Int): ItemType { - return if (position == 0) { + val contactItem = items[position] + return if (contactItem.contactId == "-1") { ItemType.HEADER - } else { - val previousContactItem = items[position - 1] - val contactItem = items[position] - if (previousContactItem.isProtonMailContact && !contactItem.isProtonMailContact) { - ItemType.HEADER - } else ItemType.CONTACT - } + } else + ItemType.CONTACT } override fun getItemViewType(position: Int) = getItemType(position).ordinal @@ -134,32 +129,25 @@ class ContactsListAdapter( } } - fun setChecked(position:Int,checked:Boolean) { - items[position].isChecked=checked - notifyDataSetChanged() - } - fun setData(items: List) { this.items = items notifyDataSetChanged() } - override fun getItemCount(): Int { - return items.size - } + override fun getItemCount(): Int = items.size fun endSelectionMode() { selectedItems?.forEach { if (items.contains(it)) { items.find { contactItem -> (contactItem == it) }?.isChecked = - false + false } } selectedItems = null notifyDataSetChanged() } - private fun selectDeselectItems(selectedItems : MutableSet, contactItem : ContactItem) { + private fun selectDeselectItems(selectedItems: MutableSet, contactItem: ContactItem) { if (selectedItems.contains(contactItem)) { selectedItems.remove(contactItem) contactItem.isChecked = false diff --git a/app/src/main/java/ch/protonmail/android/contacts/list/listView/ContactsLiveData.kt b/app/src/main/java/ch/protonmail/android/contacts/list/listView/ContactsLiveData.kt deleted file mode 100644 index a74f2c856..000000000 --- a/app/src/main/java/ch/protonmail/android/contacts/list/listView/ContactsLiveData.kt +++ /dev/null @@ -1,70 +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.contacts.list.listView - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MediatorLiveData -import java.util.* - -class ContactsLiveData(searchPhraseLiveData:LiveData, - protonmailContactsLiveData:LiveData>, - androidContactsLiveData:LiveData>):MediatorLiveData>() { - - private var searchPhrase: String? = null - private var protonmailContacts: List? = null - private var androidContacts: List? = null - init { - addSource(searchPhraseLiveData) { - searchPhrase = it - emit() - } - addSource(protonmailContactsLiveData) { - protonmailContacts = it - emit() - } - addSource(androidContactsLiveData) { - androidContacts = it - emit() - } - } - private fun emit() { - val searchPhrase = searchPhrase ?: "" - val protonmailContacts = protonmailContacts ?: emptyList() - val androidContacts = androidContacts ?: emptyList() - val filteredProtonMailEmails = protonmailContacts.filter { - searchPhrase.isEmpty() || it.getName().contains(searchPhrase, ignoreCase = true) || it.getEmail().contains(searchPhrase, ignoreCase = true) - } - val protonMailEmails = protonmailContacts.asSequence().map { it.getEmail().toLowerCase() }.toSet() - - val filteredAndroidContacts = androidContacts.filter { - !protonMailEmails.contains(it.getEmail().toLowerCase()) - } - - val mergedContacts = ArrayList() - if (filteredProtonMailEmails.isNotEmpty()) { - mergedContacts.add(ContactItem(contactId = "-1", isProtonMailContact = true)) // adding this for serving as a header item - mergedContacts.addAll(filteredProtonMailEmails) - } - if (filteredAndroidContacts.isNotEmpty()) { - mergedContacts.add(ContactItem(contactId = "-1", isProtonMailContact = false)) // adding this for serving as a header item - mergedContacts.addAll(filteredAndroidContacts) - } - value = mergedContacts - } -} \ No newline at end of file diff --git a/app/src/main/java/ch/protonmail/android/contacts/list/listView/ProtonMailContactsLiveData.kt b/app/src/main/java/ch/protonmail/android/contacts/list/listView/ProtonMailContactsLiveData.kt deleted file mode 100644 index 78277e1a6..000000000 --- a/app/src/main/java/ch/protonmail/android/contacts/list/listView/ProtonMailContactsLiveData.kt +++ /dev/null @@ -1,75 +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.contacts.list.listView - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MediatorLiveData -import ch.protonmail.android.api.models.room.contacts.ContactData -import ch.protonmail.android.api.models.room.contacts.ContactEmail - -internal class ProtonMailContactsLiveData(contactsDataLiveData:LiveData>, - contactsEmailsLiveData:LiveData>):MediatorLiveData>() { - private var contactsData:List?=null - private var contactsEmails:List?=null - - init { - addSource(contactsDataLiveData){ - contactsData=it - tryEmit() - } - addSource(contactsEmailsLiveData) { - contactsEmails=it - tryEmit() - } - } - private fun tryEmit() - { - val contactsData=contactsData?:return - val contactsEmails=contactsEmails?:return - value=contactsData.getAdapterItems(contactsEmails) - } - - private fun List.getAdapterItems( - emails:List?):List { - val emailsMap=emails?.groupBy(ContactEmail::contactId) - - return map { - val contactId=it.contactId - val name=it.name - var primaryEmail:String?=null - var additionalEmailsCount=0 - - emailsMap?.get(contactId)?.apply { - if(!isEmpty()) { - primaryEmail=get(0).email - } - if(size>1) { - additionalEmailsCount=kotlin.math.max( size-1,0) - } - } - - ContactItem(true, - contactId, - name, - primaryEmail, - additionalEmailsCount) - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/ch/protonmail/android/contacts/list/viewModel/ContactsListMapper.kt b/app/src/main/java/ch/protonmail/android/contacts/list/viewModel/ContactsListMapper.kt new file mode 100644 index 000000000..c69f83488 --- /dev/null +++ b/app/src/main/java/ch/protonmail/android/contacts/list/viewModel/ContactsListMapper.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.contacts.list.viewModel + +import ch.protonmail.android.api.models.room.contacts.ContactData +import ch.protonmail.android.api.models.room.contacts.ContactEmail +import ch.protonmail.android.contacts.list.listView.ContactItem +import timber.log.Timber +import java.util.Locale +import javax.inject.Inject + +class ContactsListMapper @Inject constructor() { + + fun mapToContactItems( + dataList: List, + emailsList: List + ): List { + val emailsMap = emailsList.groupBy(ContactEmail::contactId) + + return dataList.map { contactData -> + Timber.v("Map contactData: $contactData") + val contactId = contactData.contactId + val name = contactData.name + var primaryEmail: String? = null + var additionalEmailsCount = 0 + + emailsMap[contactId]?.apply { + if (!isEmpty()) { + primaryEmail = get(0).email + } + if (size > 1) { + additionalEmailsCount = kotlin.math.max(size - 1, 0) + } + } + + ContactItem( + true, + contactId, + name, + primaryEmail, + additionalEmailsCount + ) + } + } + + fun mergeContactItems( + protonmailContacts: List, + androidContacts: List + ): List { + val protonMailEmails = protonmailContacts.asSequence() + .map { it.getEmail().toLowerCase(Locale.ENGLISH) } + .toSet() + + val filteredAndroidContacts = androidContacts.filter { + !protonMailEmails.contains(it.getEmail().toLowerCase(Locale.ENGLISH)) + } + + val mergedContacts = mutableListOf() + if (protonmailContacts.isNotEmpty()) { + // adding this for serving as a header item + mergedContacts.add(ContactItem(contactId = "-1", isProtonMailContact = true)) + mergedContacts.addAll(protonmailContacts) + } + if (filteredAndroidContacts.isNotEmpty()) { + // adding this for serving as a header item + mergedContacts.add(ContactItem(contactId = "-1", isProtonMailContact = false)) + mergedContacts.addAll(filteredAndroidContacts) + } + return mergedContacts + } +} diff --git a/app/src/main/java/ch/protonmail/android/contacts/list/viewModel/ContactsListViewModel.kt b/app/src/main/java/ch/protonmail/android/contacts/list/viewModel/ContactsListViewModel.kt index 67fb73c9b..866ba9b9f 100644 --- a/app/src/main/java/ch/protonmail/android/contacts/list/viewModel/ContactsListViewModel.kt +++ b/app/src/main/java/ch/protonmail/android/contacts/list/viewModel/ContactsListViewModel.kt @@ -21,42 +21,78 @@ package ch.protonmail.android.contacts.list.viewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.asFlow +import androidx.lifecycle.viewModelScope import androidx.work.Operation import androidx.work.WorkManager -import ch.protonmail.android.api.models.room.contacts.ContactsDatabase +import ch.protonmail.android.api.models.room.contacts.ContactsDao import ch.protonmail.android.contacts.list.listView.ContactItem -import ch.protonmail.android.contacts.list.listView.ContactsLiveData -import ch.protonmail.android.contacts.list.listView.ProtonMailContactsLiveData import ch.protonmail.android.contacts.list.progress.ProgressLiveData import ch.protonmail.android.contacts.list.search.ISearchListenerViewModel import ch.protonmail.android.contacts.repositories.andorid.baseInfo.IAndroidContactsRepository import ch.protonmail.android.contacts.repositories.andorid.details.AndroidContactDetailsRepository import ch.protonmail.android.worker.DeleteContactWorker +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import me.proton.core.util.kotlin.EMPTY_STRING +import timber.log.Timber import javax.inject.Inject class ContactsListViewModel @Inject constructor( - contactsDatabase: ContactsDatabase, + private val contactsDao: ContactsDao, private val workManager: WorkManager, private val androidContactsRepository: IAndroidContactsRepository, - private val androidContactsDetailsRepository: AndroidContactDetailsRepository + private val androidContactsDetailsRepository: AndroidContactDetailsRepository, + private val contactsListMapper: ContactsListMapper ) : ViewModel(), IContactsListViewModel, ISearchListenerViewModel { private val progressMax = MutableLiveData() private val progress = MutableLiveData() - private val searchPhrase = MutableLiveData() - private val protonmailContactsData = contactsDatabase.findAllContactDataAsync() - private val protonmailContactsEmails = contactsDatabase.findAllContactsEmailsAsync() - private val protonmailContacts = ProtonMailContactsLiveData(protonmailContactsData, protonmailContactsEmails) - + private val searchPhraseLiveData = MutableLiveData() override val androidContacts = androidContactsRepository.androidContacts - override val contactItems = ContactsLiveData(searchPhrase, protonmailContacts, androidContacts) + override val contactItems = MutableLiveData>() override val uploadProgress = ProgressLiveData(progress, progressMax) override val contactToConvert = androidContactsDetailsRepository.contactDetails var hasPermission: Boolean = false private set + fun fetchContactItems() { + contactsDao.findAllContactData() + .combine(contactsDao.findAllContactsEmails()) { data, email -> + contactsListMapper.mapToContactItems(data, email) + } + .combine(searchPhraseLiveData.asFlow()) { contacts, searchPhrase -> + contacts.filter { contactItem -> + searchPhrase?.isEmpty() ?: true || + contactItem.getName().contains(searchPhrase ?: EMPTY_STRING, ignoreCase = true) || + contactItem.getEmail().contains(searchPhrase ?: EMPTY_STRING, ignoreCase = true) + } + } + .onEach { + // emit what we have until now, in case user did't agree to access android contacts in the next step + Timber.d("Display proton contacts size: ${it.size}") + contactItems.value = it + } + .combine(androidContacts.asFlow()) { protonContacts, androidContacts -> + Timber.d("protonContacts size: ${protonContacts.size} androidContacts size: ${androidContacts.size}") + contactsListMapper.mergeContactItems(protonContacts, androidContacts) + } + .onEach { + Timber.d("Display all contacts size: ${it.size}") + contactItems.value = it + } + .catch { Timber.w(it, "Error Fetching contacts") } + .launchIn(viewModelScope) + + if (searchPhraseLiveData.value.isNullOrBlank()) { + searchPhraseLiveData.value = EMPTY_STRING + } + } + override fun startConvertDetails(contactId: String) = androidContactsDetailsRepository.makeQuery(contactId) @@ -66,7 +102,7 @@ class ContactsListViewModel @Inject constructor( } override fun setSearchPhrase(searchPhrase: String?) { - this.searchPhrase.value = searchPhrase + this.searchPhraseLiveData.value = searchPhrase androidContactsRepository.setSearchPhrase(searchPhrase ?: "") } diff --git a/app/src/main/java/ch/protonmail/android/contacts/list/viewModel/ContactsListViewModelFactory.kt b/app/src/main/java/ch/protonmail/android/contacts/list/viewModel/ContactsListViewModelFactory.kt index 9ec430d45..82b3d08ad 100644 --- a/app/src/main/java/ch/protonmail/android/contacts/list/viewModel/ContactsListViewModelFactory.kt +++ b/app/src/main/java/ch/protonmail/android/contacts/list/viewModel/ContactsListViewModelFactory.kt @@ -52,8 +52,12 @@ class ContactsListViewModelFactory( val androidContactsDetailsRepository = AndroidContactDetailsRepository(loaderManager, androidContactsDetailsCallbacksFactory) - return ContactsListViewModel( - contactsDatabase, workManager, - androidContactsRepository, androidContactsDetailsRepository) as T - } + return ContactsListViewModel( + contactsDatabase, + workManager, + androidContactsRepository, + androidContactsDetailsRepository, + ContactsListMapper() + ) as T + } } 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/crypto/AddressCrypto.kt b/app/src/main/java/ch/protonmail/android/crypto/AddressCrypto.kt index 9248ba406..f50bbdd28 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 username: Name, + @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, username: Name): 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/di/ApplicationModule.kt b/app/src/main/java/ch/protonmail/android/di/ApplicationModule.kt index e8ad4eb70..d0136f04b 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 @@ -32,6 +33,8 @@ 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.ServerLabel import ch.protonmail.android.api.models.room.contacts.ContactLabel import ch.protonmail.android.attachments.Armorer @@ -44,8 +47,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 @@ -148,6 +156,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( @@ -196,6 +209,18 @@ object ApplicationModule { @Provides fun providesArmorer(): Armorer = OpenPgpArmorer() + + @Provides + fun attachmentFactory(): IAttachmentFactory = AttachmentFactory() + + @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/di/DatabaseModule.kt b/app/src/main/java/ch/protonmail/android/di/DatabaseModule.kt index 12e286212..6ccd42010 100644 --- a/app/src/main/java/ch/protonmail/android/di/DatabaseModule.kt +++ b/app/src/main/java/ch/protonmail/android/di/DatabaseModule.kt @@ -24,6 +24,8 @@ import ch.protonmail.android.api.models.room.attachmentMetadata.AttachmentMetada import ch.protonmail.android.api.models.room.attachmentMetadata.AttachmentMetadataDatabaseFactory import ch.protonmail.android.api.models.room.contacts.ContactsDatabase import ch.protonmail.android.api.models.room.contacts.ContactsDatabaseFactory +import ch.protonmail.android.api.models.room.counters.CountersDatabase +import ch.protonmail.android.api.models.room.counters.CountersDatabaseFactory 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 @@ -81,4 +83,12 @@ object DatabaseModule { @Provides fun provideContactsDatabase(factory: ContactsDatabaseFactory): ContactsDatabase = factory.getDatabase() + + @Provides + fun provideCountersDatabaseFactory(context: Context, userManager: UserManager): CountersDatabaseFactory = + CountersDatabaseFactory.getInstance(context, userManager.username) + + @Provides + fun provideCountersDatabase(factory: CountersDatabaseFactory): CountersDatabase = + factory.getDatabase() } diff --git a/app/src/main/java/ch/protonmail/android/di/ViewModelModule.kt b/app/src/main/java/ch/protonmail/android/di/ViewModelModule.kt index 0f430dcf5..873aee784 100644 --- a/app/src/main/java/ch/protonmail/android/di/ViewModelModule.kt +++ b/app/src/main/java/ch/protonmail/android/di/ViewModelModule.kt @@ -28,10 +28,8 @@ import ch.protonmail.android.activities.settings.NotificationSettingsViewModel import ch.protonmail.android.api.AccountManager import ch.protonmail.android.compose.ComposeMessageViewModelFactory import ch.protonmail.android.compose.recipients.GroupRecipientsViewModelFactory -import ch.protonmail.android.contacts.groups.details.ContactGroupDetailsViewModelFactory import ch.protonmail.android.contacts.groups.edit.ContactGroupEditCreateViewModelFactory import ch.protonmail.android.contacts.groups.edit.chooser.AddressChooserViewModelFactory -import ch.protonmail.android.contacts.groups.list.ContactGroupsViewModelFactory import ch.protonmail.android.core.ProtonMailApplication import ch.protonmail.android.core.UserManager import ch.protonmail.android.settings.pin.viewmodel.PinFragmentViewModelFactory @@ -56,17 +54,6 @@ internal class ViewModelModule { return addressChooserViewModelFactory } - @Provides - fun provideContactGroupsViewModelFactory(contactGroupsViewModelFactory: ContactGroupsViewModelFactory): - ViewModelProvider.NewInstanceFactory { - return contactGroupsViewModelFactory - } - - @Provides - fun provideContactGroupDetailsViewModelFactory(contactGroupDetailsViewModelFactory: ContactGroupDetailsViewModelFactory): ViewModelProvider.NewInstanceFactory { - return contactGroupDetailsViewModelFactory - } - @Provides fun provideContactGroupEditCreateViewModelFactory(contactGroupEditCreateViewModelFactory: ContactGroupEditCreateViewModelFactory): ViewModelProvider.NewInstanceFactory { return contactGroupEditCreateViewModelFactory 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 2d0bf058e..000000000 --- a/app/src/main/java/ch/protonmail/android/events/AttachmentFailedEvent.java +++ /dev/null @@ -1,46 +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; - -/** - * 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){ - this.messageId = messageId; - this.attachmentName = attachmentName; - this.messageSubject = messageSubject; - } - - public String getMessageId(){ - return messageId; - } - - public String getMessageSubject() { - return messageSubject; - } - - public String getAttachmentName() { - return attachmentName; - } -} 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/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()); } 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/ConvertLocalContactsJob.kt b/app/src/main/java/ch/protonmail/android/jobs/ConvertLocalContactsJob.kt index e100ecaf5..5f534786c 100644 --- a/app/src/main/java/ch/protonmail/android/jobs/ConvertLocalContactsJob.kt +++ b/app/src/main/java/ch/protonmail/android/jobs/ConvertLocalContactsJob.kt @@ -287,7 +287,7 @@ class ConvertLocalContactsJob(localContacts: List) : ProtonMailEndl val responses = response.responses for (contactResponse in responses) { val contact = contactResponse.response.contact - contactsDatabase.saveAllContactsEmails(contact.emails!!) + contactsDatabase.saveAllContactsEmailsBlocking(contact.emails!!) contactGroupIds.forEach { contactGroupId -> val emailsList = contact.emails!!.map { it.contactEmailId } getApi().labelContacts(LabelContactsBody(contactGroupId, emailsList)) @@ -296,7 +296,7 @@ class ConvertLocalContactsJob(localContacts: List) : ProtonMailEndl for (contactEmail in emailsList) { joins.add(ContactEmailContactLabelJoin(contactEmail, contactGroupId)) } - contactsDatabase.saveContactEmailContactLabel(joins) + contactsDatabase.saveContactEmailContactLabelBlocking(joins) } .blockingAwait() } 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 d2a5a8ec6..000000000 --- a/app/src/main/java/ch/protonmail/android/jobs/CreateAndPostDraftJob.java +++ /dev/null @@ -1,317 +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().findMessageById(mParentId); - } else { - parentMessage = getMessageDetailsRepository().findSearchMessageById(mParentId); - } - } - String addressId = message.getAddressID(); - String encryptedMessage = message.getMessageBody(); - if (!TextUtils.isEmpty(message.getMessageId())) { - Message savedMessage = getMessageDetailsRepository().findMessageById(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().createDraft(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().findMessageById(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()); - } - } - } - } - - 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().findMessageById(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/jobs/FetchDraftDetailJob.java b/app/src/main/java/ch/protonmail/android/jobs/FetchDraftDetailJob.java index a54b12d36..208049698 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()); } @@ -55,7 +55,7 @@ public void onRun() throws Throwable { long messageDbId = getMessageDetailsRepository().saveMessageInDB(message); final FetchDraftDetailEvent event = new FetchDraftDetailEvent(true); // we need to re-query MessageRepository, because after saving, messageBody may be replaced with uri to file - event.setMessage(getMessageDetailsRepository().findMessageByMessageDbId(messageDbId)); + event.setMessage(getMessageDetailsRepository().findMessageByMessageDbIdBlocking(messageDbId)); AppUtil.postEventOnUi(event); } catch (Exception e) { AppUtil.postEventOnUi(new FetchDraftDetailEvent(false)); 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/jobs/UpdateAndPostDraftJob.java b/app/src/main/java/ch/protonmail/android/jobs/UpdateAndPostDraftJob.java deleted file mode 100644 index 864abd171..000000000 --- a/app/src/main/java/ch/protonmail/android/jobs/UpdateAndPostDraftJob.java +++ /dev/null @@ -1,244 +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(); - - if (mUploadAttachments && (mNewAttachments != null && mNewAttachments.size() > 0)) { - pendingActionsDatabase.deletePendingDraftById(mMessageDbId); - } - 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().updateDraft(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()); - actionsDatabase.deletePendingDraftById(mMessageDbId); - } - - 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(), attachment.getFileName())); - } - } - pendingActionsDatabase.deletePendingUploadByMessageId(mMessageId); - } - return updatedAttachments; - } -} diff --git a/app/src/main/java/ch/protonmail/android/jobs/UpdateContactJob.java b/app/src/main/java/ch/protonmail/android/jobs/UpdateContactJob.java index 4e28f3f81..f18960fc7 100644 --- a/app/src/main/java/ch/protonmail/android/jobs/UpdateContactJob.java +++ b/app/src/main/java/ch/protonmail/android/jobs/UpdateContactJob.java @@ -145,7 +145,7 @@ private void updateContact(@NonNull String contactName, @NonNull List> mapContactGroupContactEmails = new HashMap<>(); if (updateJoins) { for (ContactEmail email : contactEmails) { @@ -203,7 +203,7 @@ private void updateJoins(String contactGroupId, String contactGroupName, List getJobManager().addJobInBackground(new SetMembersForContactGroupJob(contactGroupId, contactGroupName, membersList))) 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..d0380c1e8 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; @@ -144,7 +144,7 @@ protected int getRetryLimit() { @Override protected void onProtonCancel(int cancelReason, @Nullable Throwable throwable) { PendingActionsDatabase pendingActionsDatabase = PendingActionsDatabaseFactory.Companion.getInstance(getApplicationContext(), mUsername).getDatabase(); - Message message = getMessageDetailsRepository().findMessageByMessageDbId(mMessageDbId); + Message message = getMessageDetailsRepository().findMessageByMessageDbIdBlocking(mMessageDbId); if (message != null) { if (!BuildConfig.DEBUG) { EventBuilder eventBuilder = new EventBuilder() @@ -200,7 +200,7 @@ public void onRun() throws Throwable { } throw e; } - Message message = getMessageDetailsRepository().findMessageByMessageDbId(mMessageDbId); + Message message = getMessageDetailsRepository().findMessageByMessageDbIdBlocking(mMessageDbId); if (!BuildConfig.DEBUG && message == null) { EventBuilder eventBuilder = new EventBuilder() .withTag(SENDING_FAILED_TAG, getAppVersion()) @@ -235,7 +235,9 @@ 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 - final MessageResponse draftResponse = getApi().createDraft(newMessage); + + // TODO verify whether this is actually needed or can be done through saveDtaft use case + final MessageResponse draftResponse = getApi().createDraftBlocking(newMessage); message.setMessageId(draftResponse.getMessageId()); } message.setTime(ServerTime.currentTimeMillis() / 1000); @@ -243,8 +245,8 @@ public void onRun() throws Throwable { MailSettings mailSettings = getUserManager().getMailSettings(mUsername); - UploadAttachments uploadAttachments = buildUploadAttachmentsUseCase(); - UploadAttachments.Result result = uploadAttachments.blocking(mNewAttachments, message, crypto); + UploadAttachments uploadAttachments = buildUploadAttachmentsUseCase(pendingActionsDatabase); + UploadAttachments.Result result = uploadAttachments.blocking(mNewAttachments, message, crypto, true); if (result instanceof UploadAttachments.Result.Failure) { UploadAttachments.Result.Failure failureResult = (UploadAttachments.Result.Failure) result; @@ -253,6 +255,14 @@ public void onRun() throws Throwable { String error = "Failed uploading attachments for message \"" + message.getSubject() + "\""; ProtonMailApplication.getApplication().notifySingleErrorSendingMessage(message, error, getUserManager().getUser()); return; + } else if (result instanceof UploadAttachments.Result.UploadInProgress) { + Timber.w("Failed uploading attachments as upload is already in progress"); + pendingActionsDatabase.deletePendingUploadByMessageId(message.getMessageId()); + pendingActionsDatabase.deletePendingSendByMessageId(message.getMessageId()); + String error = "Failed uploading attachments for message \"" + message.getSubject() + "\""; + Thread.sleep(500); + ProtonMailApplication.getApplication().notifySingleErrorSendingMessage(message, error, getUserManager().getUser()); + return; } try { @@ -271,7 +281,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() { @@ -298,6 +308,7 @@ private UploadAttachments buildUploadAttachmentsUseCase() { return new UploadAttachments( dispatchers, attachmentsRepository, + pendingActionsDatabase, getMessageDetailsRepository(), getUserManager() ); @@ -327,7 +338,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) @@ -503,7 +514,7 @@ private void fetchMissingSendPreferences(ContactsDatabase contactsDatabase, Mess private void sendErrorSending(String error) { if (error == null) error = ""; - Message message = getMessageDetailsRepository().findMessageByMessageDbId(mMessageDbId); + Message message = getMessageDetailsRepository().findMessageByMessageDbIdBlocking(mMessageDbId); if (message != null) { String messageId = message.getMessageId(); if (messageId != null) { 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/servers/notification/INotificationServer.kt b/app/src/main/java/ch/protonmail/android/servers/notification/INotificationServer.kt index 09fab463c..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 @@ -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,33 @@ 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 ) + + 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 9c0530acb..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 @@ -43,7 +43,6 @@ import ch.protonmail.android.R import ch.protonmail.android.activities.EXTRA_SWITCHED_TO_USER import ch.protonmail.android.activities.EXTRA_SWITCHED_USER import ch.protonmail.android.activities.composeMessage.ComposeMessageActivity -import ch.protonmail.android.activities.guest.LoginActivity import ch.protonmail.android.activities.mailbox.MailboxActivity import ch.protonmail.android.activities.messageDetails.MessageDetailsActivity import ch.protonmail.android.api.models.User @@ -58,26 +57,23 @@ import ch.protonmail.android.utils.buildReplyIntent import ch.protonmail.android.utils.buildTrashIntent import ch.protonmail.android.utils.extensions.getColorCompat 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 NOTIFICATION_ID_SAVE_DRAFT_ERROR = 6812 const val EXTRA_MAILBOX_LOCATION = "mailbox_location" const val EXTRA_USERNAME = "username" -// endregion /** - * Created by Kamil Rajtar on 13.07.18. + * A class that is responsible for creating notification channels, and creating and showing notifications. */ - -class NotificationServer( +class NotificationServer @Inject constructor( private val context: Context, private val notificationManager: NotificationManager ) : INotificationServer { @@ -163,11 +159,13 @@ class NotificationServer( return CHANNEL_ID_ONGOING_OPS } - override fun notifyVerificationNeeded(user: User?, - messageTitle: String, - messageId: String, - messageInline: Boolean, - messageAddressId: String) { + override fun notifyVerificationNeeded( + user: User?, + messageTitle: String, + messageId: String, + messageInline: Boolean, + messageAddressId: String + ) { val inboxStyle = NotificationCompat.BigTextStyle() inboxStyle.setBigContentTitle(context.getString(R.string.verification_needed)) inboxStyle.bigText(String.format( @@ -229,8 +227,8 @@ class NotificationServer( val channelId = createAccountChannel() - val mBuilder = NotificationCompat.Builder(context, - channelId).setSmallIcon(R.drawable.notification_icon) + val mBuilder = NotificationCompat.Builder(context, channelId) + .setSmallIcon(R.drawable.notification_icon) .setColor(ContextCompat.getColor(context, R.color.ocean_blue)) .setStyle(inboxStyle) .setLights(ContextCompat.getColor(context, R.color.light_indicator), @@ -242,8 +240,12 @@ class NotificationServer( notificationManager.notify(3, notification) } - override fun notifyAboutAttachment(filename: String, uri: Uri, - mimeType: String?, showNotification: Boolean) { + override fun notifyAboutAttachment( + filename: String, + uri: Uri, + mimeType: String?, + showNotification: Boolean + ) { val channelId = createAttachmentsChannel() val mBuilder = NotificationCompat.Builder(context, channelId) @@ -426,20 +428,20 @@ class NotificationServer( * 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) @@ -450,7 +452,7 @@ class NotificationServer( // 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 @@ -458,7 +460,7 @@ class NotificationServer( } // Create Notification's Builder with the prepared params - val builder = createGenericEmailNotification(user) + val builder = createGenericEmailNotification(loggedInUser) .setContentTitle(notificationTitle) .setContentIntent(contentPendingIntent) .setStyle(inboxStyle) @@ -466,7 +468,7 @@ class NotificationServer( // 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] */ @@ -477,7 +479,9 @@ class NotificationServer( return spannableText } - private fun createSpannableBigText(sendingFailedNotifications: List): Spannable { + private fun createSpannableBigText( + sendingFailedNotifications: List + ): Spannable { val spannableStringBuilder = SpannableStringBuilder() sendingFailedNotifications.reversed().forEach { sendingFailedNotification -> spannableStringBuilder.append(createSpannableLine( @@ -489,7 +493,7 @@ class NotificationServer( } private fun createGenericErrorSendingMessageNotification( - user: User + username: String ): NotificationCompat.Builder { // Create channel and get id @@ -498,13 +502,13 @@ class NotificationServer( // 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) @@ -532,7 +536,7 @@ class NotificationServer( .bigText(error) // Create notification builder - val notificationBuilder = createGenericErrorSendingMessageNotification(user) + val notificationBuilder = createGenericErrorSendingMessageNotification(user.username) .setStyle(bigTextStyle) // Build and show notification @@ -554,12 +558,26 @@ class NotificationServer( .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/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) - } } } diff --git a/app/src/main/java/ch/protonmail/android/uiModel/SettingsItemUiModel.kt b/app/src/main/java/ch/protonmail/android/uiModel/SettingsItemUiModel.kt index 74d140141..d1b0e8e9e 100644 --- a/app/src/main/java/ch/protonmail/android/uiModel/SettingsItemUiModel.kt +++ b/app/src/main/java/ch/protonmail/android/uiModel/SettingsItemUiModel.kt @@ -41,6 +41,7 @@ class SettingsItemUiModel { var settingDisabled: Boolean = false var toggleListener: ((View, Boolean) -> 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/usecase/compose/SaveDraft.kt b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt new file mode 100644 index 000000000..ec19ba493 --- /dev/null +++ b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt @@ -0,0 +1,148 @@ +/* + * 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 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.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 +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.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 +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 +import timber.log.Timber +import javax.inject.Inject + +class SaveDraft @Inject constructor( + private val addressCryptoFactory: AddressCrypto.Factory, + private val messageDetailsRepository: MessageDetailsRepository, + private val dispatchers: DispatcherProvider, + private val pendingActionsDao: PendingActionsDao, + private val createDraftWorker: CreateDraftWorker.Enqueuer, + @CurrentUsername private val username: String, + private val uploadAttachments: UploadAttachments, + private val errorNotifier: ErrorNotifier +) { + + suspend operator fun invoke( + params: SaveDraftParameters + ): Flow = withContext(dispatchers.Io) { + Timber.i("Save Draft for messageId ${params.message.messageId}") + + val message = params.message + 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 + + message.messageBody = encryptedBody + setMessageAsDraft(message) + + val messageDbId = messageDetailsRepository.saveMessageLocally(message) + pendingActionsDao.findPendingSendByDbId(messageDbId)?.let { + return@withContext flowOf(SaveDraftResult.SendingInProgressError) + } + + return@withContext saveDraftOnline(message, params, messageId, addressCrypto) + } + + private suspend 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 == true && it.state != WorkInfo.State.CANCELLED } + .map { workInfo -> + 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) + + messageDetailsRepository.findMessageById(createdDraftId)?.let { + val uploadResult = uploadAttachments(params.newAttachmentIds, it, addressCrypto, false) + + if (uploadResult is UploadAttachments.Result.Failure) { + errorNotifier.showPersistentError(uploadResult.error, localDraft.subject) + return@map SaveDraftResult.UploadDraftAttachmentsFailed + } + return@map SaveDraftResult.Success(createdDraftId) + } + } + + Timber.e("Save Draft to API for messageId $localDraftId FAILED.") + return@map SaveDraftResult.OnlineDraftCreationFailed + } + .flowOn(dispatchers.Io) + } + + private fun updatePendingForSendMessage(createdDraftId: String, messageId: String) { + val pendingForSending = pendingActionsDao.findPendingSendByOfflineMessageId(messageId) + pendingForSending?.let { + pendingForSending.messageId = createdDraftId + pendingActionsDao.insertPendingForSend(pendingForSending) + } + } + + private fun setMessageAsDraft(message: Message) { + message.setLabelIDs( + listOf( + ALL_DRAFT.messageLocationTypeValue.toString(), + ALL_MAIL.messageLocationTypeValue.toString(), + DRAFT.messageLocationTypeValue.toString() + ) + ) + } + + data class SaveDraftParameters( + val message: Message, + val newAttachmentIds: List, + val parentId: String?, + val actionType: Constants.MessageActionType, + val previousSenderAddressId: String + ) +} diff --git a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/IMessageSenderFactory.kt b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraftResult.kt similarity index 65% rename from app/src/main/java/ch/protonmail/android/api/models/messages/receive/IMessageSenderFactory.kt rename to app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraftResult.kt index fa83a0b0b..5b34a03be 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/messages/receive/IMessageSenderFactory.kt +++ b/app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraftResult.kt @@ -1,28 +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.api.models.messages.receive -import ch.protonmail.android.api.models.room.messages.MessageSender +package ch.protonmail.android.usecase.compose -/** - * Created by Kamil Rajtar on 25.07.18. */ -interface IMessageSenderFactory{ - fun createMessageSender(serverMessageSender:ServerMessageSender):MessageSender - fun createServerMessageSender(messageSender:MessageSender):ServerMessageSender +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/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/utils/AppUtil.java b/app/src/main/java/ch/protonmail/android/utils/AppUtil.java index 13767de46..a074eb556 100644 --- a/app/src/main/java/ch/protonmail/android/utils/AppUtil.java +++ b/app/src/main/java/ch/protonmail/android/utils/AppUtil.java @@ -328,9 +328,9 @@ protected Void doInBackground(Void... voids) { pendingActionsDatabase.clearPendingUploadCache(); if (clearContacts) { contactsDatabase.clearContactEmailsLabelsJoin(); - contactsDatabase.clearContactEmailsCache(); + contactsDatabase.clearContactEmailsCacheBlocking(); contactsDatabase.clearContactDataCache(); - contactsDatabase.clearContactGroupsLabelsTable(); + contactsDatabase.clearContactGroupsLabelsTableBlocking(); contactsDatabase.clearFullContactDetailsCache(); } messagesDatabase.clearMessagesCache(); diff --git a/app/src/main/java/ch/protonmail/android/api/models/room/pendingActions/PendingDraft.kt b/app/src/main/java/ch/protonmail/android/utils/base64/AndroidBase64Encoder.kt similarity index 59% rename from app/src/main/java/ch/protonmail/android/api/models/room/pendingActions/PendingDraft.kt rename to app/src/main/java/ch/protonmail/android/utils/base64/AndroidBase64Encoder.kt index b85e201ba..c200c942d 100644 --- a/app/src/main/java/ch/protonmail/android/api/models/room/pendingActions/PendingDraft.kt +++ b/app/src/main/java/ch/protonmail/android/utils/base64/AndroidBase64Encoder.kt @@ -1,34 +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.api.models.room.pendingActions -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey +package ch.protonmail.android.utils.base64 -// region constants -const val TABLE_PENDING_DRAFT = "pending_draft" -const val COLUMN_PENDING_DRAFT_MESSAGE_ID = "message_db_id" -// endregion +import android.util.Base64 -@Entity(tableName = TABLE_PENDING_DRAFT) -data class PendingDraft( - @PrimaryKey - @ColumnInfo(name = COLUMN_PENDING_DRAFT_MESSAGE_ID) - var messageDbId: Long) +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/contacts/groups/list/ContactGroupsViewModelFactory.kt b/app/src/main/java/ch/protonmail/android/utils/notifier/AndroidErrorNotifier.kt similarity index 54% rename from app/src/main/java/ch/protonmail/android/contacts/groups/list/ContactGroupsViewModelFactory.kt rename to app/src/main/java/ch/protonmail/android/utils/notifier/AndroidErrorNotifier.kt index ac7b21f09..8d35c855f 100644 --- a/app/src/main/java/ch/protonmail/android/contacts/groups/list/ContactGroupsViewModelFactory.kt +++ b/app/src/main/java/ch/protonmail/android/utils/notifier/AndroidErrorNotifier.kt @@ -1,36 +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.contacts.groups.list -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider +package ch.protonmail.android.utils.notifier + +import ch.protonmail.android.core.UserManager +import ch.protonmail.android.servers.notification.INotificationServer import javax.inject.Inject -/** - * Created by kadrikj on 8/22/18. */ -class ContactGroupsViewModelFactory @Inject constructor( - private val contactGroupsViewModel: ContactGroupsViewModel): ViewModelProvider.NewInstanceFactory() { +class AndroidErrorNotifier @Inject constructor( + private val notificationServer: INotificationServer, + private val userManager: UserManager +) : ErrorNotifier { - override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(ContactGroupsViewModel::class.java)) { - return contactGroupsViewModel as T - } - throw IllegalArgumentException("Unknown class name") + override fun showPersistentError(errorMessage: String, messageSubject: String?) { + notificationServer.notifySaveDraftError(errorMessage, messageSubject, userManager.username) } -} \ No newline at end of file + +} 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/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/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; - } } 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 diff --git a/app/src/main/java/ch/protonmail/android/views/contactsList/ContactListItemView.kt b/app/src/main/java/ch/protonmail/android/views/contactsList/ContactListItemView.kt index 6ed0725ff..a152e47d6 100644 --- a/app/src/main/java/ch/protonmail/android/views/contactsList/ContactListItemView.kt +++ b/app/src/main/java/ch/protonmail/android/views/contactsList/ContactListItemView.kt @@ -38,12 +38,18 @@ import ch.protonmail.android.utils.extensions.showToast import kotlinx.android.synthetic.main.contacts_v2_list_item.view.* import kotlinx.android.synthetic.main.contacts_v2_list_item_header.view.* -/** - * Created by Kamil Rajtar on 06.07.18. */ -sealed class ContactListItemView(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : RelativeLayout(context, attrs, defStyleAttr) { +sealed class ContactListItemView( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : RelativeLayout(context, attrs, defStyleAttr) { abstract fun bind(item: ContactItem) - class ContactsHeaderView(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : ContactListItemView(context, attrs, defStyleAttr) { + class ContactsHeaderView( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 + ) : ContactListItemView(context, attrs, defStyleAttr) { init { inflate(context, R.layout.contacts_v2_list_item_header, this) @@ -58,46 +64,49 @@ sealed class ContactListItemView(context: Context, attrs: AttributeSet? = null, } } - class ContactView(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : ContactListItemView(context, attrs, defStyleAttr) { + class ContactView( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 + ) : ContactListItemView(context, attrs, defStyleAttr) { - private val mEmptyEmailList by lazy { context.getString(R.string.empty_email_list) } + private val emptyEmailList by lazy { context.getString(R.string.empty_email_list) } - private val mSelectedColor by lazy { + private val selectedColor by lazy { ContextCompat.getColor(context, R.color.white) } - init { - inflate( context, R.layout.contacts_v2_list_item, this ) + inflate(context, R.layout.contacts_v2_list_item, this) contactIcon.apply { isClickable = false - setImageResource( R.drawable.ic_contacts_checkmark ) - setBackgroundResource( R.drawable.bg_circle ) + setImageResource(R.drawable.ic_contacts_checkmark) + setBackgroundResource(R.drawable.bg_circle) drawable.setColorFilter( - ContextCompat.getColor( context, R.color.contact_action ), - PorterDuff.Mode.SRC_IN + ContextCompat.getColor(context, R.color.contact_action), + PorterDuff.Mode.SRC_IN ) } } - private fun handleSelectionUi( isSelected: Boolean ) { + private fun handleSelectionUi(isSelected: Boolean) { contactIcon.isVisible = isSelected - contactIconLetter.isVisible = ! isSelected + contactIconLetter.isVisible = !isSelected rowWrapper.setBackgroundColor( - if ( isSelected ) context.getColorCompat( R.color.selectable_color ) - else mSelectedColor + if (isSelected) context.getColorCompat(R.color.selectable_color) + else selectedColor ) } override fun bind(item: ContactItem) { local_contact_icon.visibility = - if (item.isProtonMailContact) View.GONE else View.VISIBLE + if (item.isProtonMailContact) View.GONE else View.VISIBLE contact_name.text = item.getName() contactIconLetter.text = UiUtil.extractInitials(item.getName()) val contactEmails = if (item.getEmail().isEmpty()) { - mEmptyEmailList + emptyEmailList } else { val additionalEmailsText = if (item.additionalEmailsCount > 0) ", +" + item.additionalEmailsCount.toString() @@ -105,12 +114,13 @@ sealed class ContactListItemView(context: Context, attrs: AttributeSet? = null, "" item.getEmail() + additionalEmailsText } - contact_email.text = contactEmails + contact_subtitle.text = contactEmails writeButton.setOnClickListener { val emailValue = item.getEmail() if (!TextUtils.isEmpty(emailValue)) { val intent = AppUtil.decorInAppIntent(Intent(context, ComposeMessageActivity::class.java)) - intent.putExtra(ComposeMessageActivity.EXTRA_TO_RECIPIENTS, arrayOf(item.getEmail()) + intent.putExtra( + ComposeMessageActivity.EXTRA_TO_RECIPIENTS, arrayOf(item.getEmail()) ) context.startActivity(intent) } else { 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/main/java/ch/protonmail/android/worker/FetchContactsDataWorker.kt b/app/src/main/java/ch/protonmail/android/worker/FetchContactsDataWorker.kt index 91b1b6e3a..28e951833 100644 --- a/app/src/main/java/ch/protonmail/android/worker/FetchContactsDataWorker.kt +++ b/app/src/main/java/ch/protonmail/android/worker/FetchContactsDataWorker.kt @@ -35,9 +35,8 @@ import ch.protonmail.android.api.ProtonMailApiManager import ch.protonmail.android.api.models.room.contacts.ContactsDao import ch.protonmail.android.api.segments.TEN_SECONDS import ch.protonmail.android.core.Constants.CONTACTS_PAGE_SIZE -import kotlinx.coroutines.withContext -import me.proton.core.util.kotlin.DispatcherProvider import timber.log.Timber +import java.util.concurrent.CancellationException import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -55,30 +54,29 @@ class FetchContactsDataWorker @WorkerInject constructor( @Assisted context: Context, @Assisted params: WorkerParameters, private val api: ProtonMailApiManager, - private val contactsDao: ContactsDao, - private val dispatchers: DispatcherProvider + private val contactsDao: ContactsDao ) : CoroutineWorker(context, params) { override suspend fun doWork(): Result = runCatching { - withContext(dispatchers.Io) { - Timber.v("Fetch Contacts Worker started") - var page = 0 - var response = api.fetchContacts(page, CONTACTS_PAGE_SIZE) - response.contacts?.let { contacts -> - val total = response.total - var fetched = contacts.size - while (total > fetched) { - ++page - response = api.fetchContacts(page, CONTACTS_PAGE_SIZE) - val contactDataList = response.contacts - if (contactDataList.isNullOrEmpty()) { - break - } - contacts.addAll(contactDataList) - fetched = contacts.size + Timber.v("Fetch Contacts Worker started") + var page = 0 + var response = api.fetchContacts(page, CONTACTS_PAGE_SIZE) + response.contacts?.let { contacts -> + val total = response.total + var fetched = contacts.size + while (total > fetched) { + ++page + response = api.fetchContacts(page, CONTACTS_PAGE_SIZE) + val contactDataList = response.contacts + if (contactDataList.isNullOrEmpty()) { + break } + contacts.addAll(contactDataList) + fetched = contacts.size + } + if (contacts.isNotEmpty()) { contactsDao.saveAllContactsData(contacts) } } @@ -91,13 +89,18 @@ class FetchContactsDataWorker @WorkerInject constructor( } ) - private fun shouldReRunOnThrowable(throwable: Throwable): Result = - if (runAttemptCount < MAX_RETRY_COUNT) { + private fun shouldReRunOnThrowable(throwable: Throwable): Result { + if (throwable is CancellationException) { + throw throwable + } + + return if (runAttemptCount < MAX_RETRY_COUNT) { Timber.d(throwable, "Fetch Contacts Worker failure, retrying count: $runAttemptCount") Result.retry() } else { failure(throwable) } + } class Enqueuer @Inject constructor(private val workManager: WorkManager) { fun enqueue(): LiveData { diff --git a/app/src/main/java/ch/protonmail/android/worker/FetchContactsEmailsWorker.kt b/app/src/main/java/ch/protonmail/android/worker/FetchContactsEmailsWorker.kt index 91c9bb52f..f769fd2e4 100644 --- a/app/src/main/java/ch/protonmail/android/worker/FetchContactsEmailsWorker.kt +++ b/app/src/main/java/ch/protonmail/android/worker/FetchContactsEmailsWorker.kt @@ -33,6 +33,7 @@ import androidx.work.WorkManager import androidx.work.WorkerParameters import ch.protonmail.android.api.segments.contact.ContactEmailsManager import timber.log.Timber +import java.util.concurrent.CancellationException import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -48,8 +49,11 @@ class FetchContactsEmailsWorker @WorkerInject constructor( onSuccess = { success() }, - onFailure = { - failure(it) + onFailure = { throwable -> + if (throwable is CancellationException) { + throw throwable + } + failure(throwable) } ) } 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 new file mode 100644 index 000000000..f052fde99 --- /dev/null +++ b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorker.kt @@ -0,0 +1,296 @@ +/* + * 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 + +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.ExistingWorkPolicy +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.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 +import ch.protonmail.android.api.models.room.messages.MessageSender +import ch.protonmail.android.api.segments.TEN_SECONDS +import ch.protonmail.android.core.Constants +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.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.MessageUtils +import ch.protonmail.android.utils.base64.Base64Encoder +import ch.protonmail.android.utils.notifier.ErrorNotifier +import kotlinx.coroutines.flow.Flow +import timber.log.Timber +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +public const val SAVE_DRAFT_UNIQUE_WORK_ID_PREFIX = "saveDraftUniqueWork" + +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 = "keySaveDraftMessageActionType" +internal const val KEY_INPUT_SAVE_DRAFT_PREV_SENDER_ADDR_ID = "keySaveDraftPreviousSenderAddressId" + +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 + +class CreateDraftWorker @WorkerInject constructor( + @Assisted context: Context, + @Assisted params: WorkerParameters, + private val messageDetailsRepository: MessageDetailsRepository, + private val messageFactory: MessageFactory, + private val userManager: UserManager, + private val addressCryptoFactory: AddressCrypto.Factory, + private val base64: Base64Encoder, + private val apiManager: ProtonMailApiManager, + private val errorNotifier: ErrorNotifier +) : CoroutineWorker(context, params) { + + override suspend fun doWork(): Result { + val message = messageDetailsRepository.findMessageByMessageDbIdBlocking(getInputMessageDbId()) + ?: return failureWithError(CreateDraftWorkerErrors.MessageNotFound) + val senderAddressId = requireNotNull(message.addressID) + val senderAddress = requireNotNull(getSenderAddress(senderAddressId)) + val parentId = getInputParentId() + val createDraftRequest = messageFactory.createDraftApiRequest(message) + + parentId?.let { + createDraftRequest.parentID = parentId + createDraftRequest.action = getInputActionType().messageActionTypeValue + val parentMessage = messageDetailsRepository.findMessageByIdBlocking(parentId) + val attachments = parentMessage?.attachments(messageDetailsRepository.databaseProvider.provideMessagesDao()) + + buildDraftRequestParentAttachments(attachments, senderAddress).forEach { + createDraftRequest.addAttachmentKeyPacket(it.key, it.value) + } + } + + val encryptedMessage = requireNotNull(message.messageBody) + 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( + messageId, + createDraftRequest, + RetrofitTag(userManager.username) + ) + } + }.fold( + onSuccess = { response -> + if (response.code != Constants.RESPONSE_CODE_OK) { + 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 + updateStoredLocalDraft(responseDraft, message) + + Timber.i("Create Draft Worker API call succeeded") + Result.success( + workDataOf(KEY_OUTPUT_RESULT_SAVE_DRAFT_MESSAGE_ID to response.messageId) + ) + }, + onFailure = { + retryOrFail(it.message, createDraftRequest.message.subject) + } + ) + } + + private suspend fun updateStoredLocalDraft(apiDraft: Message, localDraft: Message) { + 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.saveMessageLocally(apiDraft) + } + + private fun retryOrFail(error: String?, messageSubject: String?): Result { + if (runAttemptCount <= SAVE_DRAFT_MAX_RETRIES) { + 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) + } + + private fun buildDraftRequestParentAttachments( + attachments: List?, + senderAddress: Address + ): Map { + if (shouldAddParentAttachments().not()) { + return emptyMap() + } + + val draftAttachments = mutableMapOf() + attachments?.forEach { attachment -> + if (isReplyActionAndAttachmentNotInline(attachment)) { + return@forEach + } + val keyPackets = if (isSenderAddressChanged()) { + reEncryptAttachment(senderAddress, attachment) + } else { + attachment.keyPackets + } + + keyPackets?.let { + draftAttachments[attachment.attachmentId!!] = keyPackets + } + } + return draftAttachments + } + + 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) + + 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 { + 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? { + val user = userManager.getUser(userManager.username).loadNew(userManager.username) + return user.findAddressById(Id(senderAddressId)) + } + + private fun buildMessageSender(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 { + val errorData = workDataOf(KEY_OUTPUT_RESULT_SAVE_DRAFT_ERROR_ENUM to error.name) + return Result.failure(errorData) + } + + private fun getInputActionType(): Constants.MessageActionType = + Constants.MessageActionType.fromInt(inputData.getInt(KEY_INPUT_SAVE_DRAFT_ACTION_TYPE, -1)) + + private fun getInputPreviousSenderAddressId() = + inputData.getString(KEY_INPUT_SAVE_DRAFT_PREV_SENDER_ADDR_ID) + + private fun getInputParentId() = + inputData.getString(KEY_INPUT_SAVE_DRAFT_MSG_PARENT_ID) + + private fun getInputMessageDbId() = + inputData.getLong(KEY_INPUT_SAVE_DRAFT_MSG_DB_ID, INPUT_MESSAGE_DB_ID_NOT_FOUND) + + class Enqueuer @Inject constructor(private val workManager: WorkManager) { + + fun enqueue( + message: Message, + parentId: String?, + actionType: Constants.MessageActionType, + previousSenderAddressId: String + ): Flow { + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + val createDraftRequest = OneTimeWorkRequestBuilder() + .setConstraints(constraints) + .setInputData( + workDataOf( + 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 to actionType.messageActionTypeValue, + KEY_INPUT_SAVE_DRAFT_PREV_SENDER_ADDR_ID to previousSenderAddressId + ) + ) + .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 2 * TEN_SECONDS, TimeUnit.SECONDS) + .build() + + val uniqueWorkId = "$SAVE_DRAFT_UNIQUE_WORK_ID_PREFIX-${message.messageId}" + workManager.enqueueUniqueWork( + uniqueWorkId, + ExistingWorkPolicy.REPLACE, + createDraftRequest + ) + return workManager.getWorkInfoByIdLiveData(createDraftRequest.id).asFlow() + } + } + +} 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..ac94bc24e --- /dev/null +++ b/app/src/main/java/ch/protonmail/android/worker/drafts/CreateDraftWorkerErrors.kt @@ -0,0 +1,26 @@ +/* + * 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, + BadResponseCodeError +} diff --git a/app/src/main/res/layout/contacts_list_item.xml b/app/src/main/res/layout/contacts_list_item.xml index bffa8366d..a8cfe033e 100644 --- a/app/src/main/res/layout/contacts_list_item.xml +++ b/app/src/main/res/layout/contacts_list_item.xml @@ -40,7 +40,7 @@ along with ProtonMail. If not, see https://www.gnu.org/licenses/. android:textSize="@dimen/h4" /> 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 0ea10d495..aadf2f6e4 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 @@ -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 e0f3a18ea..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 @@ -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 @@ -979,19 +979,19 @@ 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>. 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 @@ -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 0ada1ec90..9c1348b3a 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>. @@ -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 b22202fdf..834e8ccf4 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. @@ -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」 + 傳送時無法開啟此郵件 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f3b6385d0..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" + Can\'t open this message while it\'s being sent 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 7b29fe19c..59fb0ab89 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 @@ -19,6 +19,7 @@ package ch.protonmail.android.api.interceptors +import android.content.Context import android.content.SharedPreferences import ch.protonmail.android.api.TokenManager import ch.protonmail.android.api.models.RefreshBody @@ -26,30 +27,25 @@ import ch.protonmail.android.api.models.RefreshResponse import ch.protonmail.android.api.models.User import ch.protonmail.android.api.models.doh.PREF_DNS_OVER_HTTPS_API_URL_LIST import ch.protonmail.android.api.segments.HEADER_AUTH -import ch.protonmail.android.core.ProtonMailApplication import ch.protonmail.android.core.UserManager -import ch.protonmail.android.utils.AppUtil +import ch.protonmail.android.utils.extensions.app import com.birbit.android.jobqueue.JobManager import io.mockk.MockKAnnotations -import io.mockk.coEvery import io.mockk.every import io.mockk.just import io.mockk.mockk -import io.mockk.mockkStatic import io.mockk.runs -import io.mockk.unmockkStatic import okhttp3.Request import okhttp3.Response -import org.junit.After import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Before import org.junit.Test -const val AUTH_ACCESS_TOKEN = "auth_access_token" -const val TEST_URL = "https://legit_url" -const val TEST_USERNAME = "testuser" -const val NON_NULL_ERROR_MESSAGE = "non null error message" +private const val AUTH_ACCESS_TOKEN = "auth_access_token" +private const val TEST_URL = "https://legit_url" +private const val TEST_USERNAME = "testuser" +private const val NON_NULL_ERROR_MESSAGE = "non null error message" class ProtonMailAuthenticatorTest { @@ -82,29 +78,20 @@ class ProtonMailAuthenticatorTest { private val jobManagerMock = mockk (relaxed = true) + private val appContextMock = mockk (relaxed = true) + private val authenticator = - ProtonMailAuthenticator(userManagerMock, jobManagerMock) + ProtonMailAuthenticator(userManagerMock, jobManagerMock, appContextMock) @Before fun setup() { - mockkStatic(AppUtil::class) - mockkStatic(ProtonMailApplication::class) MockKAnnotations.init(this) - every { AppUtil.postEventOnUi(any()) } answers { mockk() } - every { AppUtil.buildUserAgent() } answers { "user agent" } - - every { ProtonMailApplication.getApplication().currentLocale } returns "current locale" + every { appContextMock.app.currentLocale } returns "current locale" every { - ProtonMailApplication.getApplication().defaultSharedPreferences + appContextMock.app.defaultSharedPreferences } returns prefsMock - every { ProtonMailApplication.getApplication().notifyLoggedOut(TEST_USERNAME) } just runs - } - - @After - fun teardown() { - unmockkStatic(AppUtil::class) - unmockkStatic(ProtonMailApplication::class) + every { appContextMock.app.notifyLoggedOut(TEST_USERNAME) } just runs } @Test @@ -147,7 +134,7 @@ class ProtonMailAuthenticatorTest { } every { - ProtonMailApplication.getApplication().api.refreshAuthBlocking(any(), any()) + appContextMock.app.api.refreshAuthBlocking(any(), any()) } returns authResponseMock // when @@ -177,7 +164,7 @@ class ProtonMailAuthenticatorTest { } every { - ProtonMailApplication.getApplication().api.refreshAuthBlocking(any(), any()) + appContextMock.app.api.refreshAuthBlocking(any(), any()) } returns authResponseMock // when @@ -207,7 +194,7 @@ class ProtonMailAuthenticatorTest { } every { - ProtonMailApplication.getApplication().api.refreshAuthBlocking(any(), any()) + appContextMock.app.api.refreshAuthBlocking(any(), any()) } returns authResponseMock // when @@ -239,7 +226,7 @@ class ProtonMailAuthenticatorTest { } every { - ProtonMailApplication.getApplication().api.refreshAuthBlocking(any(), any()) + appContextMock.app.api.refreshAuthBlocking(any(), any()) } returns authResponseMock // when 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 84dffc1d3..722a119a6 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 @@ -36,7 +36,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() @@ -48,7 +49,8 @@ class ServerMessageTest { "Body", listOf(MessageRecipient("User1", "user1@protonmail.com"), MessageRecipient("User2", "user2@pm.me")), listOf(MessageRecipient("User3", "user3@protonmail.com")), - listOf() + listOf(), + 1 ) assertMessagePayloadEquality(expected, actual) diff --git a/app/src/test/java/ch/protonmail/android/api/segments/contact/ContactEmailsManagerTest.kt b/app/src/test/java/ch/protonmail/android/api/segments/contact/ContactEmailsManagerTest.kt new file mode 100644 index 000000000..f3e080fa0 --- /dev/null +++ b/app/src/test/java/ch/protonmail/android/api/segments/contact/ContactEmailsManagerTest.kt @@ -0,0 +1,138 @@ +/* + * 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.segments.contact + +import ch.protonmail.android.api.ProtonMailApiManager +import ch.protonmail.android.api.models.ContactEmailsResponseV2 +import ch.protonmail.android.api.models.room.contacts.ContactEmail +import ch.protonmail.android.api.models.room.contacts.ContactEmailContactLabelJoin +import ch.protonmail.android.api.models.room.contacts.ContactLabel +import ch.protonmail.android.api.models.room.contacts.ContactsDao +import ch.protonmail.android.core.Constants +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockk +import kotlinx.coroutines.test.runBlockingTest +import me.proton.core.test.android.ArchTest +import me.proton.core.test.kotlin.CoroutinesTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +class ContactEmailsManagerTest : CoroutinesTest, ArchTest { + + private lateinit var manager: ContactEmailsManager + + @MockK + private lateinit var api: ProtonMailApiManager + + @MockK + private lateinit var contactsDao: ContactsDao + + @BeforeTest + fun setUp() { + MockKAnnotations.init(this) + manager = ContactEmailsManager(api, contactsDao) + } + + @Test + fun verifyThatCanFetchAndUpdateDbWithFreshContactsDataWithJustOnePage() = runBlockingTest { + // given + val pageSize = Constants.CONTACTS_PAGE_SIZE + val labelId1 = "labelId1" + val contactLabel = ContactLabel(labelId1) + val contactLabelList = listOf(contactLabel) + val contactEmailId = "emailId1" + val labelIds = listOf(labelId1) + val contactEmail = ContactEmail(contactEmailId, "test1@abc.com", "name1", labelIds = labelIds) + val newContactEmails = listOf(contactEmail) + val join1 = ContactEmailContactLabelJoin(contactEmailId, labelId1) + val newJoins = listOf(join1) + coEvery { api.fetchContactGroupsList() } returns contactLabelList + val emailsResponse = mockk { + every { contactEmails } returns newContactEmails + every { total } returns 0 + } + coEvery { api.fetchContactEmails(any(), pageSize) } returns emailsResponse + coEvery { contactsDao.insertNewContactsAndLabels(newContactEmails, contactLabelList, newJoins) } returns Unit + + // when + manager.refresh(pageSize) + + // then + coVerify { contactsDao.insertNewContactsAndLabels(newContactEmails, contactLabelList, newJoins) } + } + + @Test + fun verifyThatCanFetchAndUpdateDbWithFreshContactsDataWithJustTwoPages() = runBlockingTest { + // given + val pageSize = 2 + val labelId1 = "labelId1" + val contactLabel = ContactLabel(labelId1) + val contactLabelList = listOf(contactLabel) + val contactEmailId1 = "emailId1" + val contactEmailId2 = "emailId2" + val contactEmailId3 = "emailId3" + val contactEmailId4 = "emailId4" + val contactEmailId5 = "emailId5" + val labelIds = listOf(labelId1) + val contactEmail1 = ContactEmail(contactEmailId1, "test1@abc.com", "name1", labelIds = labelIds) + val contactEmail2 = ContactEmail(contactEmailId2, "test2@abc.com", "name2", labelIds = labelIds) + val contactEmail3 = ContactEmail(contactEmailId3, "test3@abc.com", "name3", labelIds = labelIds) + val contactEmail4 = ContactEmail(contactEmailId4, "test4@abc.com", "name4", labelIds = labelIds) + val contactEmail5 = ContactEmail(contactEmailId5, "test5@abc.com", "name5", labelIds = labelIds) + val newContactEmails1 = listOf(contactEmail1, contactEmail2) + val newContactEmails2 = listOf(contactEmail3, contactEmail4) + val newContactEmails3 = listOf(contactEmail5) + val join1 = ContactEmailContactLabelJoin(contactEmailId1, labelId1) + val join2 = ContactEmailContactLabelJoin(contactEmailId2, labelId1) + val join3 = ContactEmailContactLabelJoin(contactEmailId3, labelId1) + val join4 = ContactEmailContactLabelJoin(contactEmailId4, labelId1) + val join5 = ContactEmailContactLabelJoin(contactEmailId5, labelId1) + val allContactEmails = listOf(contactEmail1, contactEmail2, contactEmail3, contactEmail4, contactEmail5) + val newJoins = listOf(join1, join2, join3, join4, join5) + coEvery { api.fetchContactGroupsList() } returns contactLabelList + val emailsResponse1 = mockk { + every { contactEmails } returns newContactEmails1 + every { total } returns allContactEmails.size + } + val emailsResponse2 = mockk { + every { contactEmails } returns newContactEmails2 + every { total } returns allContactEmails.size + } + val emailsResponse3 = mockk { + every { contactEmails } returns newContactEmails3 + every { total } returns allContactEmails.size + } + coEvery { api.fetchContactEmails(0, pageSize) } returns emailsResponse1 + coEvery { api.fetchContactEmails(1, pageSize) } returns emailsResponse2 + coEvery { api.fetchContactEmails(2, pageSize) } returns emailsResponse3 + coEvery { contactsDao.insertNewContactsAndLabels(allContactEmails, contactLabelList, newJoins) } returns Unit + + // when + manager.refresh(pageSize) + + // then + coVerify { contactsDao.insertNewContactsAndLabels(allContactEmails, contactLabelList, newJoins) } + } + +} 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/AttachmentsViewModelTest.kt b/app/src/test/java/ch/protonmail/android/attachments/AttachmentsViewModelTest.kt new file mode 100644 index 000000000..77d42ab7f --- /dev/null +++ b/app/src/test/java/ch/protonmail/android/attachments/AttachmentsViewModelTest.kt @@ -0,0 +1,223 @@ +/* + * 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.attachments + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.Observer +import androidx.lifecycle.SavedStateHandle +import ch.protonmail.android.activities.AddAttachmentsActivity +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.attachments.AttachmentsViewState.MissingConnectivity +import ch.protonmail.android.attachments.AttachmentsViewState.UpdateAttachments +import ch.protonmail.android.core.NetworkConnectivityManager +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.coVerifySequence +import io.mockk.every +import io.mockk.impl.annotations.RelaxedMockK +import io.mockk.mockk +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runBlockingTest +import me.proton.core.test.kotlin.CoroutinesTest +import org.junit.Before +import org.junit.Rule +import kotlin.test.Test + + +class AttachmentsViewModelTest : CoroutinesTest { + + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @RelaxedMockK + lateinit var messageRepository: MessageDetailsRepository + + @RelaxedMockK + lateinit var networkConnectivityManager: NetworkConnectivityManager + + @RelaxedMockK + private lateinit var mockObserver: Observer + + @RelaxedMockK + private lateinit var savedState: SavedStateHandle + + private lateinit var viewModel: AttachmentsViewModel + + @Before + fun setUp() { + MockKAnnotations.init(this) + viewModel = AttachmentsViewModel( + savedState, + dispatchers, + messageRepository, + networkConnectivityManager + ) + viewModel.viewState.observeForever(mockObserver) + every { networkConnectivityManager.isInternetConnectionPossible() } returns true + } + + @Test + fun initFindsMessageInDatabase() = runBlockingTest { + val messageId = "draftId3214" + coEvery { messageRepository.findMessageById(messageId) } returns mockk(relaxed = true) + every { savedState.get(AddAttachmentsActivity.EXTRA_DRAFT_ID) } returns messageId + + viewModel.init() + + coVerify { messageRepository.findMessageById(messageId) } + } + + @Test + fun initObservesMessageRepositoryByMessageDbIdWhenGivenMessageIdIsFound() = runBlockingTest { + val messageId = "draftId234" + val messageDbId = 124L + val message = Message(messageId = messageId).apply { dbId = messageDbId } + coEvery { messageRepository.findMessageById(messageId) } returns message + every { savedState.get(AddAttachmentsActivity.EXTRA_DRAFT_ID) } returns messageId + + viewModel.init() + + coVerify { messageRepository.findMessageByDbId(messageDbId) } + } + + @Test + fun initUpdatesViewStateWhenMessageIsUpdatedInDbAsAResultOfDraftCreationCompleting() = runBlockingTest { + val messageId = "91bbb263-2bf2-43dd-a079-233a305e69df" + val messageDbId = 124L + val message = Message(messageId = messageId).apply { dbId = messageDbId } + val updatedMessageAttachments = listOf(Attachment(attachmentId = "updatedAttId")) + val remoteMessage = message.copy(messageId = "Remote message id").apply { + Attachments = updatedMessageAttachments + } + coEvery { messageRepository.findMessageById(messageId) } returns message + coEvery { messageRepository.findMessageByDbId(messageDbId) } returns flowOf(remoteMessage) + every { savedState.get(AddAttachmentsActivity.EXTRA_DRAFT_ID) } returns messageId + + viewModel.init() + + val expectedState = UpdateAttachments(updatedMessageAttachments) + coVerify { mockObserver.onChanged(expectedState) } + } + + @Test + fun initStopsListeningForMessageUpdatesWhenDraftCreationCompletedEventWasReceived() = runBlockingTest { + val messageId = "91bbb263-2bf2-43dd-a079-233a305e69df" + val messageDbId = 124L + val message = Message(messageId = messageId).apply { dbId = messageDbId } + val updatedMessageAttachments = listOf(Attachment(attachmentId = "updatedAttId")) + val remoteMessage = message.copy(messageId = "Remote message id").apply { + Attachments = updatedMessageAttachments + } + val updatedDraftMessage = remoteMessage.copy(messageId = "Updated remote messageID") + coEvery { messageRepository.findMessageById(messageId) } returns message + coEvery { messageRepository.findMessageByDbId(messageDbId) } returns flowOf( + remoteMessage, updatedDraftMessage + ) + every { savedState.get(AddAttachmentsActivity.EXTRA_DRAFT_ID) } returns messageId + + viewModel.init() + + val expectedState = UpdateAttachments(updatedMessageAttachments) + coVerifySequence { mockObserver.onChanged(expectedState) } + } + + @Test + fun initDoesNotUpdateViewStateWhenMessageIsUpdatedInDbAsAResultOfDraftUpdateCompleting() = runBlockingTest { + val messageId = "remote-draft-message ID" + val messageDbId = 2384L + val message = Message().apply { dbId = messageDbId } + val updatedMessageAttachments = listOf(Attachment(attachmentId = "updatedAttId")) + val remoteMessage = message.copy(messageId = "Updated Draft Remote message id").apply { + Attachments = updatedMessageAttachments + } + coEvery { messageRepository.findMessageById(messageId) } returns message + coEvery { messageRepository.findMessageByDbId(messageDbId) } returns flowOf(remoteMessage) + every { savedState.get(AddAttachmentsActivity.EXTRA_DRAFT_ID) } returns messageId + + viewModel.init() + + coVerify(exactly = 0) { mockObserver.onChanged(any()) } + } + + @Test + fun initDoesNotUpdateViewStateWhenMessageThatWasUpdatedInDbIsNotARemoteMessage() = runBlockingTest { + val messageId = "91bbb263-2bf2-43dd-a079-233a305e69df" + val messageDbId = 124L + val message = Message(messageId = messageId).apply { dbId = messageDbId } + val updatedLocalMessage = message.copy(messageId = "82ccc723-2bf2-43dd-f834-233a305e69df") + coEvery { messageRepository.findMessageById(messageId) } returns message + coEvery { messageRepository.findMessageByDbId(messageDbId) } returns flowOf(updatedLocalMessage) + every { savedState.get(AddAttachmentsActivity.EXTRA_DRAFT_ID) } returns messageId + + viewModel.init() + + coVerify(exactly = 0) { mockObserver.onChanged(any()) } + } + + @Test + fun initPostsOfflineViewStateWhenThereIsNoConnection() = runBlockingTest { + val messageId = "91bbb263-2bf2-43dd-a079-233a305e69df" + val messageDbId = 124L + val message = Message(messageId = messageId).apply { dbId = messageDbId } + coEvery { messageRepository.findMessageById(messageId) } returns message + coEvery { messageRepository.findMessageByDbId(messageDbId) } returns flowOf() + every { networkConnectivityManager.isInternetConnectionPossible() } returns false + every { savedState.get(AddAttachmentsActivity.EXTRA_DRAFT_ID) } returns messageId + + viewModel.init() + + coVerifySequence { mockObserver.onChanged(MissingConnectivity) } + } + + @Test + fun initLogsWarningAndStopsExecutionIfDraftIdWasNotPassed() = runBlockingTest { + every { savedState.get(AddAttachmentsActivity.EXTRA_DRAFT_ID) } returns null + + viewModel.init() + + // test warning is logged here + coVerify(exactly = 0) { messageRepository.findMessageByDbId(any()) } + coVerify(exactly = 0) { mockObserver.onChanged(any()) } + } + + @Test + fun initIgnoresAnyNullMessagesReturnedByDatabaseFlow() = runBlockingTest { + val messageId = "91bbb263-2bf2-43dd-a079-233a305e79df" + val messageDbId = 113L + val message = Message(messageId = messageId).apply { dbId = messageDbId } + val updatedMessageAttachments = listOf(Attachment(attachmentId = "updatedAttId")) + val remoteMessage = message.copy(messageId = "Remote message id").apply { + Attachments = updatedMessageAttachments + } + coEvery { messageRepository.findMessageById(messageId) } returns message + coEvery { messageRepository.findMessageByDbId(messageDbId) } returns flowOf( + remoteMessage, null + ) + every { savedState.get(AddAttachmentsActivity.EXTRA_DRAFT_ID) } returns messageId + + viewModel.init() + + val expectedState = UpdateAttachments(updatedMessageAttachments) + coVerifySequence { mockObserver.onChanged(expectedState) } + } +} 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..de0167596 100644 --- a/app/src/test/java/ch/protonmail/android/attachments/UploadAttachmentsTest.kt +++ b/app/src/test/java/ch/protonmail/android/attachments/UploadAttachmentsTest.kt @@ -19,10 +19,11 @@ 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 +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 @@ -34,18 +35,18 @@ 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 import me.proton.core.test.kotlin.CoroutinesTest -import org.junit.Rule import kotlin.test.BeforeTest import kotlin.test.Test class UploadAttachmentsTest : CoroutinesTest { - @get:Rule - val instantTaskExecutorRule = InstantTaskExecutorRule() + @RelaxedMockK + private lateinit var pendingActionsDao: PendingActionsDao @RelaxedMockK private lateinit var attachmentsRepository: AttachmentsRepository @@ -65,7 +66,38 @@ 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 + } + + @Test + fun uploadAttachmentsSetsMessageAsPendingToUploadWhenStartingToUpload() { + runBlockingTest { + val attachmentIds = listOf("1") + val messageId = "message-id-238237" + val message = Message(messageId) + + uploadAttachments(attachmentIds, message, crypto, false) + + 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, false) + + verify(exactly = 0) { pendingActionsDao.insertPendingForUpload(any()) } + assertEquals(UploadAttachments.Result.UploadInProgress, result) + } } @Test @@ -84,17 +116,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) + val result = uploadAttachments(attachmentIds, message, crypto, false) 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) } @@ -116,17 +150,18 @@ 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 { AttachmentsRepository.Result.Failure("Failed to upload attachment2") } - val result = uploadAttachments(attachmentIds, message, crypto) + val result = uploadAttachments(attachmentIds, message, crypto, false) val expected = UploadAttachments.Result.Failure("Failed to upload attachment2") assertEquals(expected, result) + verify { pendingActionsDao.deletePendingUploadByMessageId("messageId8237") } } } @@ -134,7 +169,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 @@ -142,10 +177,12 @@ class UploadAttachmentsTest : CoroutinesTest { AttachmentsRepository.Result.Failure("Failed to upload public key") } - val result = uploadAttachments(attachmentIds, message, crypto) + val result = uploadAttachments(attachmentIds, message, crypto, true) val expected = UploadAttachments.Result.Failure("Failed to upload public key") assertEquals(expected, result) + coVerify { attachmentsRepository.uploadPublicKey(message, crypto) } + verify { pendingActionsDao.deletePendingUploadByMessageId("messageId9273585") } } } @@ -159,11 +196,11 @@ 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 - uploadAttachments(attachmentIds, message, crypto) + uploadAttachments(attachmentIds, message, crypto, false) coVerifySequence { attachmentsRepository.upload(attachment2, crypto) } } @@ -185,11 +222,11 @@ 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 - uploadAttachments(attachmentIds, message, crypto) + uploadAttachments(attachmentIds, message, crypto, false) coVerify(exactly = 0) { attachmentsRepository.upload(attachment1, crypto) } coVerify { attachmentsRepository.upload(attachment2, crypto) } @@ -212,11 +249,11 @@ 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 - uploadAttachments(attachmentIds, message, crypto) + uploadAttachments(attachmentIds, message, crypto, false) coVerify { attachmentsRepository.upload(attachment1, crypto) } coVerify(exactly = 0) { attachmentsRepository.upload(attachment2, crypto) } @@ -227,7 +264,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" @@ -243,7 +280,7 @@ class UploadAttachmentsTest : CoroutinesTest { every { messageDetailsRepository.findAttachmentById("1") } returns attachmentMock1 every { messageDetailsRepository.findAttachmentById("2") } returns attachmentMock2 - uploadAttachments(attachmentIds, message, crypto) + uploadAttachments(attachmentIds, message, crypto, false) coVerify { attachmentsRepository.upload(attachmentMock1, crypto) } coVerify(exactly = 0) { attachmentsRepository.upload(attachmentMock2, crypto) } @@ -251,17 +288,17 @@ class UploadAttachmentsTest : CoroutinesTest { } @Test - fun uploadAttachmentsCallRepositoryUploadPublicKeyWhenMailSettingsGetAttachPublicKeyIsTrue() { + fun uploadAttachmentsCallRepositoryUploadPublicKeyWhenMailSettingsGetAttachPublicKeyIsTrueAndMessageIsSending() { 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 { - AttachmentsRepository.Result.Success + AttachmentsRepository.Result.Success("23421") } - uploadAttachments(emptyList(), message, crypto) + uploadAttachments(emptyList(), message, crypto, true) coVerify { attachmentsRepository.uploadPublicKey(message, crypto) } } @@ -271,7 +308,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" @@ -287,19 +324,188 @@ 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") } - uploadAttachments(attachmentIds, message, crypto) + uploadAttachments(attachmentIds, message, crypto, false) verify { attachmentMock1.deleteLocalFile() } verify(exactly = 0) { attachmentMock2.deleteLocalFile() } } } + @Test + fun uploadAttachmentsUpdatesLocalMessageWithUploadedAttachmentWhenUploadSucceeds() { + runBlockingTest { + 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" + every { filePath } returns "filePath1" + 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" + every { filePath } returns "filePath2" + every { isUploaded } returns false + every { doesFileExist } returns true + } + val uploadedAttachmentMock2 = mockk(relaxed = true) { + every { fileName } returns "att2FileName" + 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)) + every { messageDetailsRepository.findAttachmentById("1") } returns attachmentMock1 + every { messageDetailsRepository.findAttachmentById("2") } returns attachmentMock2 + every { messageDetailsRepository.findAttachmentById(uploadedAttachmentMock1Id) } returns uploadedAttachmentMock1 + every { messageDetailsRepository.findAttachmentById(uploadedAttachmentMock2Id) } returns uploadedAttachmentMock2 + coEvery { attachmentsRepository.upload(attachmentMock1, crypto) } answers { + AttachmentsRepository.Result.Success(uploadedAttachmentMock1Id) + } + coEvery { attachmentsRepository.upload(attachmentMock2, crypto) } answers { + AttachmentsRepository.Result.Success(uploadedAttachmentMock2Id) + } + + uploadAttachments(attachmentIds, message, crypto, false) + + val actualMessage = slot() + 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, false) + + 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, false) + + val actualMessage = slot() + val expectedAttachments = listOf(attachmentMock1) + coVerify(exactly = 2) { messageDetailsRepository.saveMessageLocally(capture(actualMessage)) } + assertEquals(expectedAttachments, actualMessage.captured.Attachments) + } + } + + @Test + fun uploadAttachmentsDoesNotAttachThePublicKeyIfTheMessageIsNotBeingSent() { + runBlockingTest { + val attachmentIds = emptyList() + val messageId = "message-id-123842" + val message = Message(messageId) + val username = "username" + every { userManager.username } returns username + every { pendingActionsDao.findPendingUploadByMessageId(messageId) } returns null + every { userManager.getMailSettings(username)?.getAttachPublicKey() } returns true + coEvery { attachmentsRepository.uploadPublicKey(message, crypto) } returns mockk() + + val result = uploadAttachments(attachmentIds, message, crypto, false) + + coVerify(exactly = 0) { attachmentsRepository.uploadPublicKey(any(), any()) } + assertEquals(UploadAttachments.Result.Success, result) + } + } + + @Test + fun uploadAttachmentsAttachesThePublicKeyOnlyWhenTheMessageIsBeingSent() { + runBlockingTest { + val attachmentIds = emptyList() + val messageId = "message-id-123842" + val message = Message(messageId) + val username = "username" + every { userManager.username } returns username + every { pendingActionsDao.findPendingUploadByMessageId(messageId) } returns null + every { userManager.getMailSettings(username)?.getAttachPublicKey() } returns true + coEvery { attachmentsRepository.uploadPublicKey(message, crypto) } answers { + AttachmentsRepository.Result.Success("82384") + } + + val result = uploadAttachments.blocking(attachmentIds, message, crypto, true) + + coVerify { attachmentsRepository.uploadPublicKey(any(), any()) } + assertEquals(UploadAttachments.Result.Success, result) + } + } } 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..129e457e9 --- /dev/null +++ b/app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt @@ -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.compose + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.work.WorkManager +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 +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.compose.SaveDraftResult +import ch.protonmail.android.usecase.delete.DeleteMessage +import ch.protonmail.android.usecase.fetch.FetchPublicKeys +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 io.mockk.unmockkStatic +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 { + + @get:Rule + val trampolineSchedulerRule = TrampolineScheduler() + + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @RelaxedMockK + private lateinit var stringResourceResolver: StringResourceResolver + + @RelaxedMockK + lateinit var composeMessageRepository: ComposeMessageRepository + + @RelaxedMockK + lateinit var userManager: UserManager + + @RelaxedMockK + lateinit var messageDetailsRepository: MessageDetailsRepository + + @RelaxedMockK + lateinit var saveDraft: SaveDraft + + @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 workManager: WorkManager + + @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 { + // Given + val message = Message() + givenViewModelPropertiesAreInitialised() + + // When + viewModel.saveDraft(message, hasConnectivity = false) + + // Then + val parameters = SaveDraft.SaveDraftParameters( + message, + emptyList(), + "parentId823", + Constants.MessageActionType.FORWARD, + "previousSenderAddressId" + ) + coVerify { saveDraft(parameters) } + } + } + + @Test + fun saveDraftReadsNewlyCreatedDraftFromRepositoryAndPostsItToLiveDataWhenSaveDraftUseCaseSucceeds() { + runBlockingTest { + // Given + val message = Message() + val createdDraftId = "newDraftId" + val createdDraft = Message(messageId = createdDraftId, localId = "local28348") + val savedDraftObserver = viewModel.savingDraftComplete.testObserver() + givenViewModelPropertiesAreInitialised() + coEvery { saveDraft(any()) } returns flowOf(SaveDraftResult.Success(createdDraftId)) + coEvery { messageDetailsRepository.findMessageById(createdDraftId) } returns createdDraft + + // When + viewModel.saveDraft(message, hasConnectivity = false) + + coVerify { messageDetailsRepository.findMessageById(createdDraftId) } + assertEquals(createdDraft, savedDraftObserver.observedValues[0]) + } + } + + @Test + fun saveDraftObservesMessageInComposeRepositoryToGetNotifiedWhenMessageIsSent() { + runBlockingTest { + // Given + 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 + 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) } + } + } + + @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" + coVerify { 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" + coVerify { stringResourceResolver.invoke(errorResId) } + assertEquals(expectedError, saveDraftErrorObserver.observedValues[0]) + } + } + + @Test + fun saveDraftReadsNewlyCreatedDraftFromRepositoryAndPostsItToLiveDataWhenUpdatingDraftAndSaveDraftUseCaseSucceeds() { + runBlockingTest { + // Given + val message = Message() + val updatedDraftId = "updatedDraftId" + val updatedDraft = Message(messageId = updatedDraftId, localId = "local82347") + val savedDraftObserver = viewModel.savingDraftComplete.testObserver() + givenViewModelPropertiesAreInitialised() + viewModel.draftId = "non-empty draftId triggers update draft" + coEvery { saveDraft(any()) } returns flowOf(SaveDraftResult.Success(updatedDraftId)) + coEvery { messageDetailsRepository.findMessageById(updatedDraftId) } returns updatedDraft + + // When + viewModel.saveDraft(message, hasConnectivity = false) + + coVerify { messageDetailsRepository.findMessageById(updatedDraftId) } + assertEquals(updatedDraft, savedDraftObserver.observedValues[0]) + } + } + + @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) + unmockkStatic(UiUtil::class) + } + } + + @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) + unmockkStatic(UiUtil::class) + } + } + + 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" + } + +} diff --git a/app/src/test/java/ch/protonmail/android/contacts/details/ContactDetailsRepositoryTest.kt b/app/src/test/java/ch/protonmail/android/contacts/details/ContactDetailsRepositoryTest.kt index fbb6057a3..23f82f335 100644 --- a/app/src/test/java/ch/protonmail/android/contacts/details/ContactDetailsRepositoryTest.kt +++ b/app/src/test/java/ch/protonmail/android/contacts/details/ContactDetailsRepositoryTest.kt @@ -26,6 +26,7 @@ import ch.protonmail.android.api.models.room.contacts.ContactEmail import ch.protonmail.android.api.models.room.contacts.ContactsDao import com.birbit.android.jobqueue.JobManager import io.mockk.MockKAnnotations +import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.InjectMockKs import io.mockk.impl.annotations.RelaxedMockK @@ -70,7 +71,7 @@ class ContactDetailsRepositoryTest { repository.saveContactEmails(emails) - verify { contactsDao.saveAllContactsEmails(emails) } + coVerify { contactsDao.saveAllContactsEmails(emails) } } } @@ -108,7 +109,7 @@ class ContactDetailsRepositoryTest { verify { contactsDao.findContactEmailsByContactId(contactId) } verify { contactsDao.deleteAllContactsEmails(localContactEmails) } - verify { contactsDao.saveAllContactsEmails(serverEmails) } + verify { contactsDao.saveAllContactsEmailsBlocking(serverEmails) } } } diff --git a/app/src/test/java/ch/protonmail/android/contacts/details/ContactGroupsRepositoryTest.kt b/app/src/test/java/ch/protonmail/android/contacts/details/ContactGroupsRepositoryTest.kt index 9af9e3a66..98c0397c5 100644 --- a/app/src/test/java/ch/protonmail/android/contacts/details/ContactGroupsRepositoryTest.kt +++ b/app/src/test/java/ch/protonmail/android/contacts/details/ContactGroupsRepositoryTest.kt @@ -24,16 +24,16 @@ import ch.protonmail.android.api.models.room.contacts.ContactsDao import ch.protonmail.android.contacts.groups.list.ContactGroupsRepository import ch.protonmail.android.testAndroid.rx.TestSchedulerRule import io.mockk.MockKAnnotations -import io.mockk.every +import io.mockk.coEvery import io.mockk.impl.annotations.InjectMockKs import io.mockk.impl.annotations.RelaxedMockK import io.mockk.verify -import io.reactivex.Flowable -import io.reactivex.Observable +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runBlockingTest +import me.proton.core.test.kotlin.TestDispatcherProvider import org.junit.Assert.assertEquals import org.junit.Rule -import java.io.IOException -import java.util.concurrent.TimeUnit import kotlin.test.BeforeTest import kotlin.test.Test @@ -51,6 +51,8 @@ class ContactGroupsRepositoryTest { @InjectMockKs private lateinit var contactGroupsRepository: ContactGroupsRepository + private val dispatcherProvider = TestDispatcherProvider + private val label1 = ContactLabel("a", "aa") private val label2 = ContactLabel("b", "bb") private val label3 = ContactLabel("c", "cc") @@ -59,45 +61,57 @@ class ContactGroupsRepositoryTest { @BeforeTest fun setUp() { MockKAnnotations.init(this) - every { protonMailApi.fetchContactGroupsAsObservable() } answers { - Observable.just(listOf(label1, label2, label3, label4)).delay(500, TimeUnit.MILLISECONDS) - } - every { contactsDao.findContactGroupsObservable() } answers { Flowable.just(listOf(label1, label2, label3)) } } @Test - fun testDbAndAPIEventsEmitted() { - val testObserver = contactGroupsRepository.getContactGroups().test() - - testSchedulerRule.schedulerTest.advanceTimeBy(1000, TimeUnit.MILLISECONDS) - testObserver.awaitTerminalEvent() - testObserver.assertNoErrors() - testObserver.assertValueCount(2) + fun verifyThatDbAndApiContactsAreEmittedInOrder() { + runBlockingTest { + // given + val dbContactsList = listOf(label1) + val searchTerm = "Rob" + coEvery { contactsDao.findContactGroupsFlow("%$searchTerm%") } returns flowOf(dbContactsList) + coEvery { contactsDao.countContactEmailsByLabelId(any()) } returns 1 + + // when + val result = contactGroupsRepository.observeContactGroups(searchTerm).first() + + // then + assertEquals(dbContactsList, result) + } } @Test - fun testDbEventBeforeAPIEvent() { - val testObserver = contactGroupsRepository.getContactGroups().test() - - testObserver.assertValueCount(1) - testObserver.assertValue(listOf(label1, label2, label3)) - testSchedulerRule.schedulerTest.advanceTimeBy(1000, TimeUnit.MILLISECONDS) - testObserver.awaitCount(2) - testObserver.assertValueCount(2) - assertEquals(listOf(label1, label2, label3, label4), testObserver.values()[1]) + fun verifyThatDbAndApiContactsAreEmittedIn() { + runBlockingTest { + // given + val dbContactsList = listOf(label1) + val searchTerm = "Rob" + coEvery { contactsDao.findContactGroupsFlow("%$searchTerm%") } returns flowOf(dbContactsList) + coEvery { contactsDao.countContactEmailsByLabelId(any()) } returns 1 + + // when + val result = contactGroupsRepository.observeContactGroups(searchTerm).first() + + // then + assertEquals(dbContactsList, result) + } } @Test - fun testApiErrorEvent() { - every { protonMailApi.fetchContactGroupsAsObservable() } answers { Observable.error(IOException(":(")) } - - val testObserver = contactGroupsRepository.getContactGroups().test() - - testSchedulerRule.schedulerTest.triggerActions() - testSchedulerRule.schedulerTest.advanceTimeBy(1000, TimeUnit.MILLISECONDS) - testObserver.awaitTerminalEvent() - testObserver.assertValue(listOf(label1, label2, label3)) - testObserver.assertError(IOException::class.java) + fun verifyThatDbContactsAreEmitted() { + runBlockingTest { + // given + val searchTerm = "search" + val dbContactsList = listOf(label1) + coEvery { contactsDao.findContactGroupsFlow("%$searchTerm%") } returns flowOf(dbContactsList) + coEvery { contactsDao.countContactEmailsByLabelId(any()) } returns 1 + + // when + val result = contactGroupsRepository.observeContactGroups(searchTerm).first() + + // then + assertEquals(dbContactsList, result) + } } @Test diff --git a/app/src/test/java/ch/protonmail/android/contacts/groups/ContactGroupsViewModelTest.kt b/app/src/test/java/ch/protonmail/android/contacts/groups/ContactGroupsViewModelTest.kt index 99ba0094a..175319ccb 100644 --- a/app/src/test/java/ch/protonmail/android/contacts/groups/ContactGroupsViewModelTest.kt +++ b/app/src/test/java/ch/protonmail/android/contacts/groups/ContactGroupsViewModelTest.kt @@ -19,19 +19,21 @@ package ch.protonmail.android.contacts.groups import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import ch.protonmail.android.api.models.room.contacts.ContactEmailContactLabelJoin import ch.protonmail.android.api.models.room.contacts.ContactLabel import ch.protonmail.android.contacts.groups.list.ContactGroupsRepository import ch.protonmail.android.contacts.groups.list.ContactGroupsViewModel 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.delete.DeleteLabel +import ch.protonmail.android.utils.Event import io.mockk.MockKAnnotations -import io.mockk.every +import io.mockk.coEvery import io.mockk.impl.annotations.InjectMockKs import io.mockk.impl.annotations.RelaxedMockK -import io.reactivex.Observable -import io.reactivex.schedulers.Schedulers +import io.mockk.mockk +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 @@ -44,9 +46,6 @@ class ContactGroupsViewModelTest : CoroutinesTest { @get: Rule var instantExecutorRule = InstantTaskExecutorRule() - @get: Rule - val rxSchedulerRule = TrampolineScheduler() - @RelaxedMockK private lateinit var userManager: UserManager @@ -69,27 +68,45 @@ class ContactGroupsViewModelTest : CoroutinesTest { } @Test - fun `fetch contact groups posts contactLabels on contactGroupResult LiveData when repository succeeds`() { + fun verifyThatFetchContactGroupsPostsSucceedsWithDataEmittedInContactGroupsResult() { + // given + val searchTerm = "searchTerm" val resultLiveData = contactGroupsViewModel.contactGroupsResult.testObserver() val contactLabels = listOf(label1, label2, label3) - every { contactGroupsRepository.getContactGroups() } returns Observable.just(contactLabels) + coEvery { contactGroupsRepository.observeContactGroups(searchTerm) } returns flowOf(contactLabels) + val join1 = mockk() + val joins = listOf(join1) + coEvery { contactGroupsRepository.getJoins() } returns flowOf(joins) - contactGroupsViewModel.fetchContactGroups(Schedulers.trampoline()) + // when + contactGroupsViewModel.setSearchPhrase(searchTerm) + contactGroupsViewModel.observeContactGroups() + // then val observedContactLabels = resultLiveData.observedValues[0] assertEquals(contactLabels, observedContactLabels) } @Test - fun `fetch contact groups posts error on contactGroupsError LiveData when repository fails`() { - val resultLiveData = contactGroupsViewModel.contactGroupsError.testObserver() - val exception = Exception("test-exception") - every { contactGroupsRepository.getContactGroups() } returns Observable.error(exception) - - contactGroupsViewModel.fetchContactGroups(Schedulers.trampoline()) - - val observedError = resultLiveData.observedValues[0]?.getContentIfNotHandled() - assertEquals("test-exception", observedError) + fun verifyThatFetchContactGroupsErrorCausesContactGroupsErrorEmission() { + // given + runBlockingTest { + val searchTerm = "searchTerm" + val resultLiveData = contactGroupsViewModel.contactGroupsError.testObserver() + val exception = Exception("test-exception") + coEvery { contactGroupsRepository.observeContactGroups(searchTerm) } throws exception + val join1 = mockk() + val joins = listOf(join1) + coEvery { contactGroupsRepository.getJoins() } returns flowOf(joins) + + // when + contactGroupsViewModel.setSearchPhrase(searchTerm) + contactGroupsViewModel.observeContactGroups() + + // then + val observedError = resultLiveData.observedValues[0] + assertEquals("test-exception", (observedError as Event).getContentIfNotHandled()) + } } } diff --git a/app/src/test/java/ch/protonmail/android/contacts/groups/edit/ContactGroupEditCreateRepositoryTest.kt b/app/src/test/java/ch/protonmail/android/contacts/groups/edit/ContactGroupEditCreateRepositoryTest.kt index 4dcb7ec25..7c5f918a3 100644 --- a/app/src/test/java/ch/protonmail/android/contacts/groups/edit/ContactGroupEditCreateRepositoryTest.kt +++ b/app/src/test/java/ch/protonmail/android/contacts/groups/edit/ContactGroupEditCreateRepositoryTest.kt @@ -103,7 +103,7 @@ class ContactGroupEditCreateRepositoryTest { verifyOrder { contactsDao.fetchJoins(contactGroupId) contactsDao.saveContactGroupLabel(contactLabel) - contactsDao.saveContactEmailContactLabel(emailLabelJoinedList) + contactsDao.saveContactEmailContactLabelBlocking(emailLabelJoinedList) } } diff --git a/app/src/test/java/ch/protonmail/android/contacts/list/viewModel/ContactsListMapperTest.kt b/app/src/test/java/ch/protonmail/android/contacts/list/viewModel/ContactsListMapperTest.kt new file mode 100644 index 000000000..82f101fa6 --- /dev/null +++ b/app/src/test/java/ch/protonmail/android/contacts/list/viewModel/ContactsListMapperTest.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.contacts.list.viewModel + +import ch.protonmail.android.api.models.room.contacts.ContactData +import ch.protonmail.android.api.models.room.contacts.ContactEmail +import ch.protonmail.android.contacts.list.listView.ContactItem +import kotlin.test.Test +import kotlin.test.assertEquals + +class ContactsListMapperTest { + + private val mapper = ContactsListMapper() + private val contactId1 = "contactId1" + private val name1 = "name1" + private val contactId2 = "contactId2" + private val name2 = "name2" + private val email1 = "email1@abc.com" + private val email2 = "email2@abc.com" + private val contactItem1 = ContactItem( + isProtonMailContact = true, + contactId = contactId1, + name = name1, + email = email1, + additionalEmailsCount = 0, + labels = null, + isChecked = false + ) + private val contactItem2 = ContactItem( + isProtonMailContact = true, + contactId = contactId2, + name = name2, + email = email2, + additionalEmailsCount = 0, + labels = null, + isChecked = false + ) + + @Test + fun verifyThatEmptyContactsAreMappedProperly() { + // given + val dataList = listOf() + val emailsList = listOf() + val expected = emptyList() + + // when + val result = mapper.mapToContactItems(dataList, emailsList) + + // then + assertEquals(expected, result) + } + + @Test + fun verifyThatContactsAreMappedProperly() { + // given + + val contact1 = ContactData(contactId = contactId1, name = name1) + + val contact2 = ContactData(contactId = contactId2, name = name2) + val dataList = listOf(contact1, contact2) + val contactEmail1 = ContactEmail( + contactEmailId = "contactEmailId1", email = email1, name = "emailName1", contactId = contactId1 + ) + val contactEmail2 = ContactEmail( + contactEmailId = "contactEmailId2", email = email2, name = "emailName1", contactId = contactId2 + ) + val emailsList = listOf(contactEmail1, contactEmail2) + val expected = listOf( + contactItem1, + contactItem2 + ) + + // when + val result = mapper.mapToContactItems(dataList, emailsList) + + // then + assertEquals(expected, result) + } + + @Test + fun verifyContactsAreMergedProperlyWithoutOverlappingContacts() { + // given + val contactId3 = "contactId3" + val name3 = "name1" + val email3 = "email3@abc.com" + val contactItem3 = ContactItem( + isProtonMailContact = true, + contactId = contactId3, + name = name3, + email = email3, + additionalEmailsCount = 0, + labels = null, + isChecked = false + ) + val headerItem = + ContactItem( + isProtonMailContact = true, + contactId = "-1", + name = null, + email = null, + additionalEmailsCount = 0, + labels = null, + isChecked = false + ) + val protonContacts = listOf(contactItem1, contactItem2) + val androidContacts = listOf(contactItem3) + val expected = listOf( + headerItem, contactItem1, contactItem2, headerItem.copy(isProtonMailContact = false), contactItem3 + ) + + // when + val result = mapper.mergeContactItems(protonContacts, androidContacts) + + // then + assertEquals(expected, result) + } + + @Test + fun verifyContactsAreMergedProperlyWithOneOverlappingContactSkipped() { + // given + val contactId3 = "contactId3" + val name3 = "name3" + val contactItem3 = ContactItem( + isProtonMailContact = true, + contactId = contactId3, + name = name3, + email = email1, + additionalEmailsCount = 0, + labels = null, + isChecked = false + ) + val headerItem = + ContactItem( + isProtonMailContact = true, + contactId = "-1", + name = null, + email = null, + additionalEmailsCount = 0, + labels = null, + isChecked = false + ) + val protonContacts = listOf(contactItem1, contactItem2) + val androidContacts = listOf(contactItem3) + val expected = listOf(headerItem, contactItem1, contactItem2) + + // when + val result = mapper.mergeContactItems(protonContacts, androidContacts) + + // then + assertEquals(expected, result) + } +} 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..b63c8223e --- /dev/null +++ b/app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt @@ -0,0 +1,464 @@ +/* + * 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 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 +import ch.protonmail.android.api.models.room.pendingActions.PendingSend +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 +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 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 +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.mockk +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 +import java.util.UUID +import kotlin.test.BeforeTest +import kotlin.test.Test + +class SaveDraftTest : CoroutinesTest { + + @RelaxedMockK + private lateinit var errorNotifier: ErrorNotifier + + @RelaxedMockK + private lateinit var uploadAttachments: UploadAttachments + + @RelaxedMockK + private lateinit var createDraftScheduler: Enqueuer + + @RelaxedMockK + private lateinit var pendingActionsDao: PendingActionsDao + + @RelaxedMockK + private lateinit var addressCryptoFactory: AddressCrypto.Factory + + @RelaxedMockK + lateinit var messageDetailsRepository: MessageDetailsRepository + + @InjectMockKs + lateinit var saveDraft: SaveDraft + + private val currentUsername = "username" + + @BeforeTest + fun setUp() { + MockKAnnotations.init(this) + } + + @Test + fun saveDraftSavesEncryptedDraftMessageToDb() { + 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 + val expectedMessage = message.copy(messageBody = "encrypted armored content") + expectedMessage.setLabelIDs( + listOf( + ALL_DRAFT.messageLocationTypeValue.toString(), + ALL_MAIL.messageLocationTypeValue.toString(), + DRAFT.messageLocationTypeValue.toString() + ) + ) + coVerify { messageDetailsRepository.saveMessageLocally(expectedMessage) } + } + } + + @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( + SaveDraftParameters(message, emptyList(), "parentId", FORWARD, "previousSenderId1273") + ) + + // Then + 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.findMessageByMessageDbIdBlocking(messageDbId) } returns message + coEvery { messageDetailsRepository.saveMessageLocally(message) } returns messageDbId + every { pendingActionsDao.findPendingSendByDbId(messageDbId) } returns PendingSend("anyMessageId") + + // When + val result = saveDraft.invoke( + SaveDraftParameters(message, emptyList(), "parentId123", FORWARD, "previousSenderId1273") + ) + + // Then + val expectedError = SaveDraftResult.SendingInProgressError + assertEquals(expectedError, result.first()) + verify(exactly = 0) { createDraftScheduler.enqueue(any(), any(), any(), 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 + every { pendingActionsDao.findPendingSendByDbId(9833L) } returns null + + // When + saveDraft.invoke( + SaveDraftParameters(message, emptyList(), "parentId123", REPLY_ALL, "previousSenderId1273") + ) + + // Then + verify { createDraftScheduler.enqueue(message, "parentId123", REPLY_ALL, "previousSenderId1273") } + } + } + + @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 { + // 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.findMessageById(any()) } returns null + coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 9833L + every { pendingActionsDao.findPendingSendByDbId(9833L) } returns null + every { pendingActionsDao.findPendingSendByOfflineMessageId("45623") } answers { + PendingSend( + "234234", localDraftId, "offlineId", false, 834L + ) + } + coEvery { uploadAttachments(any(), any(), any(), false) } returns UploadAttachments.Result.Success + val workOutputData = workDataOf( + KEY_OUTPUT_RESULT_SAVE_DRAFT_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 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 apiDraft = message.copy(messageId = "createdDraftMessageId345") + val workOutputData = workDataOf( + 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 + 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(), false) } 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 + coVerify { uploadAttachments(newAttachmentIds, apiDraft, addressCrypto, false) } + } + } + + @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_RESULT_SAVE_DRAFT_ERROR_ENUM to CreateDraftWorkerErrors.ServerError.name + ) + val workerStatusFlow = buildCreateDraftWorkerResponse(WorkInfo.State.FAILED, workOutputData) + coEvery { messageDetailsRepository.saveMessageLocally(message) } returns 9833L + coEvery { 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(SaveDraftResult.OnlineDraftCreationFailed, result) + } + } + + @Test + fun saveDraftsShowPersistentErrorAndReturnsErrorWhenUploadingNewAttachmentsFails() { + runBlockingTest { + // Given + val localDraftId = "8345" + val message = Message().apply { + dbId = 123L + this.messageId = "45623" + 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(), false) } returns Failure(errorMessage) + every { + createDraftScheduler.enqueue( + message, + "parentId234", + REPLY, + "previousSenderId132423" + ) + } answers { workerStatusFlow } + + // When + val result = saveDraft.invoke( + SaveDraftParameters(message, newAttachmentIds, "parentId234", REPLY, "previousSenderId132423") + ).first() + + // Then + verify { errorNotifier.showPersistentError(errorMessage, "Message Subject") } + assertEquals(SaveDraftResult.UploadDraftAttachmentsFailed, result) + } + } + + @Test + fun saveDraftReturnsSuccessWhenBothDraftCreationAndAttachmentsUploadSucceeds() { + 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_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 + 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(), false) } 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 + val result = saveDraft.invoke( + SaveDraftParameters(message, newAttachmentIds, "parentId234", REPLY_ALL, "previousSenderId132423") + ).first() + + // Then + assertEquals(SaveDraftResult.Success("createdDraftMessageId345"), result) + } + } + + + 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/usecase/delete/DeleteLabelTest.kt b/app/src/test/java/ch/protonmail/android/usecase/delete/DeleteLabelTest.kt index 7c7ccea71..e73d31bd9 100644 --- a/app/src/test/java/ch/protonmail/android/usecase/delete/DeleteLabelTest.kt +++ b/app/src/test/java/ch/protonmail/android/usecase/delete/DeleteLabelTest.kt @@ -28,6 +28,7 @@ import ch.protonmail.android.api.models.room.contacts.ContactsDatabase import ch.protonmail.android.api.models.room.messages.MessagesDatabase import ch.protonmail.android.worker.DeleteLabelWorker import io.mockk.MockKAnnotations +import io.mockk.coEvery import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.mockk @@ -82,8 +83,8 @@ class DeleteLabelTest { workerStatusLiveData.value = workInfo val expected = true - every { contactsDatabase.findContactGroupById(labelId) } returns contactLabel - every { contactsDatabase.deleteContactGroup(contactLabel) } returns Unit + coEvery { contactsDatabase.findContactGroupById(labelId) } returns contactLabel + coEvery { contactsDatabase.deleteContactGroup(contactLabel) } returns Unit every { messagesDatabase.deleteLabelById(labelId) } returns Unit every { workScheduler.enqueue(any()) } returns workerStatusLiveData @@ -117,8 +118,8 @@ class DeleteLabelTest { workerStatusLiveData.value = workInfo val expected = false - every { contactsDatabase.findContactGroupById(labelId) } returns contactLabel - every { contactsDatabase.deleteContactGroup(contactLabel) } returns Unit + coEvery { contactsDatabase.findContactGroupById(labelId) } returns contactLabel + coEvery { contactsDatabase.deleteContactGroup(contactLabel) } returns Unit every { messagesDatabase.deleteLabelById(labelId) } returns Unit every { workScheduler.enqueue(any()) } returns workerStatusLiveData @@ -151,8 +152,8 @@ class DeleteLabelTest { val workerStatusLiveData = MutableLiveData() workerStatusLiveData.value = workInfo - every { contactsDatabase.findContactGroupById(labelId) } returns contactLabel - every { contactsDatabase.deleteContactGroup(contactLabel) } returns Unit + coEvery { contactsDatabase.findContactGroupById(labelId) } returns contactLabel + coEvery { contactsDatabase.deleteContactGroup(contactLabel) } returns Unit every { messagesDatabase.deleteLabelById(labelId) } returns Unit every { workScheduler.enqueue(any()) } returns workerStatusLiveData 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/utils/notifier/AndroidErrorNotifierTest.kt b/app/src/test/java/ch/protonmail/android/utils/notifier/AndroidErrorNotifierTest.kt new file mode 100644 index 000000000..a91699b1f --- /dev/null +++ b/app/src/test/java/ch/protonmail/android/utils/notifier/AndroidErrorNotifierTest.kt @@ -0,0 +1,59 @@ +/* + * 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.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.verify +import kotlin.test.BeforeTest +import kotlin.test.Test + +class AndroidErrorNotifierTest { + + @RelaxedMockK + private lateinit var notificationServer: INotificationServer + + @MockK + private lateinit var userManager: UserManager + + @InjectMockKs + private lateinit var errorNotifier: AndroidErrorNotifier + + @BeforeTest + fun setUp() { + MockKAnnotations.init(this) + } + + @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") } + } +} 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..5c6e832f0 --- /dev/null +++ b/app/src/test/java/ch/protonmail/android/worker/CreateDraftWorkerTest.kt @@ -0,0 +1,829 @@ +/* + * 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.BackoffPolicy +import androidx.work.Data +import androidx.work.ExistingWorkPolicy +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.ProtonMailApiManager +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 +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.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.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.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 +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.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.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 +import java.io.IOException +import kotlin.test.BeforeTest +import kotlin.test.Test + +class CreateDraftWorkerTest : CoroutinesTest { + + @RelaxedMockK + private lateinit var errorNotifier: ErrorNotifier + + @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 workManager: WorkManager + + @RelaxedMockK + private lateinit var userManager: UserManager + + @RelaxedMockK + private lateinit var addressCryptoFactory: AddressCrypto.Factory + + @RelaxedMockK + private lateinit var base64: Base64Encoder + + @RelaxedMockK + private lateinit var apiManager: ProtonMailApiManager + + @RelaxedMockK + private lateinit var pendingActionsDao: PendingActionsDao + + @InjectMockKs + private lateinit var worker: CreateDraftWorker + + @BeforeTest + fun setUp() { + MockKAnnotations.init(this) + coEvery { apiManager.createDraft(any()) } returns mockk(relaxed = true) + } + + @Test + fun workerEnqueuerCreatesOneTimeRequestWorkerWhichIsUniqueForMessageId() { + runBlockingTest { + // Given + val messageParentId = "98234" + val messageId = "2834" + val messageDbId = 534L + val messageActionType = REPLY_ALL + val message = Message(messageId = messageId) + message.dbId = messageDbId + val previousSenderAddressId = "previousSenderId82348" + val requestSlot = slot() + every { + workManager.enqueueUniqueWork( + "saveDraftUniqueWork-$messageId", + ExistingWorkPolicy.REPLACE, + capture(requestSlot) + ) + } answers { mockk() } + + // When + CreateDraftWorker.Enqueuer(workManager).enqueue( + message, + messageParentId, + messageActionType, + previousSenderAddressId + ) + + // Then + val workSpec = requestSlot.captured.workSpec + val constraints = workSpec.constraints + val inputData = workSpec.input + 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.getInt(KEY_INPUT_SAVE_DRAFT_ACTION_TYPE, -1) + val actualPreviousSenderAddress = inputData.getString(KEY_INPUT_SAVE_DRAFT_PREV_SENDER_ADDR_ID) + assertEquals(message.dbId, actualMessageDbId) + assertEquals(message.messageId, actualMessageLocalId) + assertEquals(messageParentId, actualMessageParentId) + assertEquals(messageActionType.messageActionTypeValue, actualMessageActionType) + assertEquals(previousSenderAddressId, actualPreviousSenderAddress) + assertEquals(NetworkType.CONNECTED, constraints.requiredNetworkType) + assertEquals(BackoffPolicy.EXPONENTIAL, workSpec.backoffPolicy) + assertEquals(20000, workSpec.backoffDelayDuration) + verify { workManager.getWorkInfoByIdLiveData(any()) } + } + } + + @Test + fun workerReturnsMessageNotFoundErrorWhenMessageDetailsRepositoryDoesNotReturnAValidMessage() { + runBlockingTest { + // Given + val messageDbId = 345L + givenMessageIdInput(messageDbId) + every { messageDetailsRepository.findMessageByMessageDbIdBlocking(messageDbId) } returns null + + // When + val result = worker.doWork() + + // Then + val error = CreateDraftWorkerErrors.MessageNotFound + val expectedFailure = ListenableWorker.Result.failure( + Data.Builder().putString(KEY_OUTPUT_RESULT_SAVE_DRAFT_ERROR_ENUM, error.name).build() + ) + assertEquals(expectedFailure, result) + } + } + + @Test + fun workerSetsParentIdAndActionTypeOnCreateDraftRequestWhenParentIdIsGiven() { + runBlockingTest { + // Given + val parentId = "89345" + val actionType = FORWARD + val messageDbId = 345L + val message = Message().apply { + dbId = messageDbId + messageId = "17575c27-c3d9-4f3a-9188-02dea1321cc6" + addressID = "addressId" + messageBody = "messageBody" + } + val apiDraftMessage = mockk(relaxed = true) + givenMessageIdInput(messageDbId) + givenParentIdInput(parentId) + givenActionTypeInput(actionType) + every { messageDetailsRepository.findMessageByMessageDbIdBlocking(messageDbId) } returns message + every { messageFactory.createDraftApiRequest(message) } answers { apiDraftMessage } + + // When + worker.doWork() + + // Then + 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 + verify { messageDetailsRepository.findMessageByIdBlocking(parentId) } + } + } + + @Test + fun workerSetsSenderAndMessageBodyOnCreateDraftRequest() { + runBlockingTest { + // Given + val messageDbId = 345L + val addressId = "addressId" + val message = Message().apply { + dbId = messageDbId + messageId = "17575c24-c3d9-4f3a-9188-02dea1321cc6" + addressID = addressId + 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.findMessageByMessageDbIdBlocking(messageDbId) } returns message + every { messageFactory.createDraftApiRequest(message) } answers { apiDraftMessage } + every { userManager.username } returns "username" + every { userManager.getUser("username").loadNew("username") } returns mockk { + every { findAddressById(Id(addressId)) } returns address + } + + // When + worker.doWork() + + // Then + val messageSender = MessageSender("senderName", "sender@email.it") + verify { apiDraftMessage.setSender(messageSender) } + verify { apiDraftMessage.setMessageBody("messageBody") } + } + } + + @Test + fun workerUsesMessageSenderToRequestDraftCreationWhenMessageIsBeingSentByAlias() { + runBlockingTest { + // Given + val messageDbId = 89234L + 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") + } + val apiDraftMessage = mockk(relaxed = true) + givenMessageIdInput(messageDbId) + givenActionTypeInput() + every { messageDetailsRepository.findMessageByMessageDbIdBlocking(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) } + } + } + + @Test + fun workerAddsReEncryptedParentAttachmentsToRequestWhenActionIsForwardAndSenderAddressChanged() { + runBlockingTest { + // Given + val parentId = "89345" + val actionType = FORWARD + val messageDbId = 345L + val message = Message().apply { + dbId = messageDbId + messageId = "17575c26-c3d9-4f3a-9188-02dea1321cc6" + 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.findMessageByMessageDbIdBlocking(messageDbId) } returns message + every { messageFactory.createDraftApiRequest(message) } answers { apiDraftMessage } + 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 + } + 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!!) } + } + } + + @Test + fun workerSkipsNonInlineParentAttachmentsWhenActionIsReplyAllAndSenderAddressChanged() { + runBlockingTest { + // Given + val parentId = "89345" + val messageDbId = 345L + val message = Message().apply { + dbId = messageDbId + messageId = "17575c25-c3d9-4f3a-9188-02dea1321cc6" + 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.findMessageByMessageDbIdBlocking(messageDbId) } returns message + every { messageFactory.createDraftApiRequest(message) } answers { apiDraftMessage } + every { messageDetailsRepository.findMessageByIdBlocking(parentId) } returns parentMessage + every { userManager.username } returns "username93w" + + // When + worker.doWork() + + // Then + verify(exactly = 0) { apiDraftMessage.addAttachmentKeyPacket("attachment2", any()) } + } + } + + @Test + fun workerAddsExistingParentAttachmentsToRequestWhenSenderAddressWasNotChanged() { + runBlockingTest { + // Given + val parentId = "89345" + val messageDbId = 345L + val message = Message().apply { + dbId = messageDbId + messageId = "17575c29-c3d9-4f3a-9188-02dea1321cc6" + 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(FORWARD) + givenPreviousSenderAddress("") + every { messageDetailsRepository.findMessageByMessageDbIdBlocking(messageDbId) } returns message + every { messageFactory.createDraftApiRequest(message) } answers { apiDraftMessage } + every { messageDetailsRepository.findMessageByIdBlocking(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 + messageId = "17575c28-c3d9-4f3a-9188-02dea1321cc6" + 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.findMessageByMessageDbIdBlocking(messageDbId) } returns message + every { messageFactory.createDraftApiRequest(message) } answers { apiDraftMessage } + every { messageDetailsRepository.findMessageByIdBlocking(parentId) } returns parentMessage + + // When + worker.doWork() + + // Then + verify { parentMessage.attachments(messageDetailsRepository.databaseProvider.provideMessagesDao()) } + verifyOrder { + apiDraftMessage.addAttachmentKeyPacket("attachment", "OriginalAttachmentPackets") + apiDraftMessage.addAttachmentKeyPacket("attachment2", "Attachment2KeyPackets") + } + + verify(exactly = 0) { apiDraftMessage.addAttachmentKeyPacket("attachment1", "Attachment1KeyPackets") } + verifyOrder { + apiDraftMessage.addAttachmentKeyPacket("attachment", "OriginalAttachmentPackets") + apiDraftMessage.addAttachmentKeyPacket("attachment2", "Attachment2KeyPackets") + } + } + } + + @Test + fun workerDoesNotAddParentAttachmentsToRequestWhenActionTypeIsOtherThenForwardReplyReplyAll() { + runBlockingTest { + // Given + val parentId = "89345" + val messageDbId = 345L + val message = Message().apply { + dbId = messageDbId + messageId = "17575c31-c3d9-4f3a-9188-02dea1321cc6" + addressID = "addressId835" + messageBody = "messageBody" + } + + val apiDraftMessage = mockk(relaxed = true) + givenMessageIdInput(messageDbId) + givenParentIdInput(parentId) + givenActionTypeInput(NONE) + givenPreviousSenderAddress("") + every { messageDetailsRepository.findMessageByMessageDbIdBlocking(messageDbId) } returns message + every { messageFactory.createDraftApiRequest(message) } answers { apiDraftMessage } + + // When + worker.doWork() + + // Then + verify(exactly = 0) { apiDraftMessage.addAttachmentKeyPacket(any(), any()) } + } + } + + @Test + fun workerPerformsCreateDraftRequestAndBuildsMessageFromResponseWhenRequestSucceeds() { + runBlockingTest { + // Given + val parentId = "89345" + 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") + setLabelIDs(listOf("label", "label1", "label2")) + parsedHeaders = ParsedHeaders("recEncryption", "recAuth") + numAttachments = 2 + Attachments = emptyList() + } + + val apiDraftRequest = mockk(relaxed = true) + val responseMessage = Message(messageId = "created_draft_id").apply { + Attachments = listOf(Attachment("235423"), Attachment("823421")) + } + val apiDraftResponse = mockk { + every { code } returns 1000 + every { messageId } returns "created_draft_id" + every { this@mockk.message } returns responseMessage + } + givenMessageIdInput(messageDbId) + givenParentIdInput(parentId) + givenActionTypeInput(NONE) + givenPreviousSenderAddress("") + every { messageDetailsRepository.findMessageByMessageDbIdBlocking(messageDbId) } returns message + every { messageFactory.createDraftApiRequest(message) } returns apiDraftRequest + coEvery { apiManager.createDraft(apiDraftRequest) } returns apiDraftResponse + + // When + worker.doWork() + + // Then + 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() + 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.Attachments = responseMessage.Attachments + this.localId = message.messageId + } + val actualMessage = slot() + coVerify { messageDetailsRepository.saveMessageLocally(capture(actualMessage)) } + assertEquals(expected, actualMessage.captured) + assertEquals(expected.Attachments, actualMessage.captured.Attachments) + } + } + + @Test + fun workerUpdatesLocalDbDraftWithCreatedDraftAndReturnsSuccessWhenRequestSucceeds() { + runBlockingTest { + // Given + val parentId = "89345" + val messageDbId = 345L + 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")) + parsedHeaders = ParsedHeaders("recEncryption", "recAuth") + numAttachments = 3 + } + + 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 + val apiDraftResponse = mockk { + every { code } returns 1000 + every { messageId } returns "response_message_id" + every { this@mockk.message } returns responseMessage + } + givenMessageIdInput(messageDbId) + givenParentIdInput(parentId) + givenActionTypeInput(NONE) + givenPreviousSenderAddress("") + every { messageDetailsRepository.findMessageByMessageDbIdBlocking(messageDbId) } returns message + every { messageFactory.createDraftApiRequest(message) } returns apiDraftRequest + coEvery { apiManager.createDraft(apiDraftRequest) } returns apiDraftResponse + + // When + val result = worker.doWork() + + // 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() + ) + assertEquals(expected, result) + } + } + + @Test + fun workerReturnsFailureWithoutRetryingWhenApiRequestSucceedsButReturnsNonSuccessResponseCode() { + runBlockingTest { + // Given + val parentId = "89345" + 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 402 + every { error } returns errorMessage + } + givenMessageIdInput(messageDbId) + givenParentIdInput(parentId) + givenActionTypeInput(NONE) + givenPreviousSenderAddress("") + every { messageDetailsRepository.findMessageByMessageDbIdBlocking(messageDbId) } returns message + 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.findMessageByMessageDbIdBlocking(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) + } + } + + @Test + fun workerNotifiesErrorAndReturnsFailureWithErrorWhenAPIRequestFailsAndMaxTriesWereReached() { + 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.findMessageByMessageDbIdBlocking(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 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, + CreateDraftWorkerErrors.ServerError.name + ).build() + ) + assertEquals(expected, result) + } + } + + @Test + fun workerPerformsUpdateDraftRequestWhenMessageIsNotLocalAndStoresResponseMessageInDbWhenRequestSucceeds() { + 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 = 1 + Attachments = listOf(Attachment(attachmentId = "12749")) + } + + val apiDraftRequest = mockk(relaxed = true) + val responseMessage = Message(messageId = "created_draft_id").apply { + Attachments = listOf(Attachment(attachmentId = "82374")) + } + val apiDraftResponse = mockk { + every { code } returns 1000 + every { messageId } returns "created_draft_id" + every { this@mockk.message } returns responseMessage + } + val retrofitTag = RetrofitTag(userManager.username) + givenMessageIdInput(messageDbId) + givenParentIdInput(parentId) + givenActionTypeInput(NONE) + givenPreviousSenderAddress("") + every { messageDetailsRepository.findMessageByMessageDbIdBlocking(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) } + val expectedMessage = Message().apply { + this.dbId = messageDbId + this.messageId = "created_draft_id" + 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.Attachments = responseMessage.Attachments + this.localId = message.messageId + } + val actualMessage = slot() + coVerify { messageDetailsRepository.saveMessageLocally(capture(actualMessage)) } + assertEquals(expectedMessage, actualMessage.captured) + assertEquals(expectedMessage.Attachments, actualMessage.captured.Attachments) + } + } + + private fun givenPreviousSenderAddress(address: String) { + every { parameters.inputData.getString(KEY_INPUT_SAVE_DRAFT_PREV_SENDER_ADDR_ID) } answers { address } + } + + private fun givenActionTypeInput(actionType: Constants.MessageActionType = NONE) { + every { + parameters.inputData.getInt(KEY_INPUT_SAVE_DRAFT_ACTION_TYPE, -1) + } answers { + actionType.messageActionTypeValue + } + } + + private fun givenParentIdInput(parentId: String) { + every { parameters.inputData.getString(KEY_INPUT_SAVE_DRAFT_MSG_PARENT_ID) } answers { parentId } + } + + private fun givenMessageIdInput(messageDbId: Long) { + every { parameters.inputData.getLong(KEY_INPUT_SAVE_DRAFT_MSG_DB_ID, -1) } answers { messageDbId } + } +} 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) + } + } } diff --git a/app/src/test/java/ch/protonmail/android/worker/FetchContactsDataWorkerTest.kt b/app/src/test/java/ch/protonmail/android/worker/FetchContactsDataWorkerTest.kt index 6a03c0480..49cbf5432 100644 --- a/app/src/test/java/ch/protonmail/android/worker/FetchContactsDataWorkerTest.kt +++ b/app/src/test/java/ch/protonmail/android/worker/FetchContactsDataWorkerTest.kt @@ -29,13 +29,12 @@ import ch.protonmail.android.api.models.room.contacts.ContactsDao import ch.protonmail.android.core.Constants import io.mockk.MockKAnnotations import io.mockk.coEvery +import io.mockk.coVerify 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 me.proton.core.util.android.workmanager.toWorkData import kotlin.test.BeforeTest import kotlin.test.Test @@ -60,7 +59,7 @@ class FetchContactsDataWorkerTest { @BeforeTest fun setUp() { MockKAnnotations.init(this) - worker = FetchContactsDataWorker(context, parameters, api, contactsDao, TestDispatcherProvider) + worker = FetchContactsDataWorker(context, parameters, api, contactsDao) } @Test @@ -74,14 +73,14 @@ class FetchContactsDataWorkerTest { every { total } returns contactsList.size } coEvery { api.fetchContacts(0, Constants.CONTACTS_PAGE_SIZE) } returns response - every { contactsDao.saveAllContactsData(contactsList) } returns listOf(1) + coEvery { contactsDao.saveAllContactsData(contactsList) } returns listOf(1) val expected = ListenableWorker.Result.success() // when val operationResult = worker.doWork() // then - verify { contactsDao.saveAllContactsData(contactsList) } + coVerify { contactsDao.saveAllContactsData(contactsList) } assertEquals(expected, operationResult) } diff --git a/app/src/test/java/ch/protonmail/android/worker/FetchContactsEmailsWorkerTest.kt b/app/src/test/java/ch/protonmail/android/worker/FetchContactsEmailsWorkerTest.kt index 9a4995c69..aca618d46 100644 --- a/app/src/test/java/ch/protonmail/android/worker/FetchContactsEmailsWorkerTest.kt +++ b/app/src/test/java/ch/protonmail/android/worker/FetchContactsEmailsWorkerTest.kt @@ -24,7 +24,7 @@ import androidx.work.ListenableWorker import androidx.work.WorkerParameters import ch.protonmail.android.api.segments.contact.ContactEmailsManager import io.mockk.MockKAnnotations -import io.mockk.every +import io.mockk.coEvery import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.RelaxedMockK import kotlinx.coroutines.test.runBlockingTest @@ -56,7 +56,7 @@ class FetchContactsEmailsWorkerTest { fun verityThatInNormalConditionSuccessResultIsReturned() = runBlockingTest { // given - every { contactEmailsManager.refresh() } returns Unit + coEvery { contactEmailsManager.refresh() } returns Unit val expected = ListenableWorker.Result.success() // when @@ -66,14 +66,13 @@ class FetchContactsEmailsWorkerTest { assertEquals(expected, operationResult) } - @Test fun verityThatWhenExceptionIsThrownFalseResultIsReturned() = runBlockingTest { // given val exceptionMessage = "testException" val testException = Exception(exceptionMessage) - every { contactEmailsManager.refresh() } throws testException + coEvery { contactEmailsManager.refresh() } throws testException val expected = ListenableWorker.Result.failure(WorkerError(exceptionMessage).toWorkData()) // when 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/ContactsMatchers.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/contacts/ContactsMatchers.kt index d7f8a3593..c2769e7f0 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/contacts/ContactsMatchers.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/robots/contacts/ContactsMatchers.kt @@ -54,7 +54,7 @@ object ContactsMatchers { override fun matchesSafely(item: RecyclerView.ViewHolder): Boolean { return if (item.itemView is ContactListItemView.ContactView) { val contactItem = item.itemView as ContactListItemView.ContactView - val actualEmail = contactItem.contact_email.text.toString() + val actualEmail = contactItem.contact_subtitle.text.toString() contactsList.add(actualEmail) actualEmail == email } else { @@ -81,7 +81,7 @@ object ContactsMatchers { override fun matchesSafely(item: RecyclerView.ViewHolder): Boolean { return if (item.itemView is ContactListItemView.ContactView) { val contactItem = item.itemView as ContactListItemView.ContactView - contactItem.contact_email.text.toString() == email && + contactItem.contact_subtitle.text.toString() == email && contactItem.contact_name.text.toString() == name } else { false @@ -138,7 +138,7 @@ object ContactsMatchers { .findViewById(R.id.contact_data) .findViewById(R.id.contact_name).text.toString() val groupMembersCount = contactDataParent - .findViewById(R.id.contact_email).text.toString() + .findViewById(R.id.contact_subtitle).text.toString() contactGroupsList.add(groupName) return groupName == name && groupMembersCount == membersCount } 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..0c42d1094 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 @@ -18,14 +18,23 @@ */ package ch.protonmail.android.uitests.robots.mailbox +import android.view.View +import android.widget.LinearLayout +import android.widget.RelativeLayout import android.widget.TextView +import androidx.annotation.IdRes import androidx.recyclerview.widget.RecyclerView +import androidx.test.espresso.matcher.BoundedDiagnosingMatcher 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. @@ -63,6 +72,43 @@ object MailboxMatchers { } } + /** + * Matches the Mailbox message represented by [MessagesListItemView] by message subject and Reply, Reply all or + * Forward flag. Subject must be unique in a list in order to use this matcher. + * + * @param subject - message subject + * @param id - the view id of Reply, Reply all or Forward [TextView]. + */ + fun withMessageSubjectAndFlag(subject: String, @IdRes id: Int): Matcher { + return object : BoundedDiagnosingMatcher(MessagesListViewHolder.MessageViewHolder::class.java) { + + val messagesList = ArrayList() + + override fun matchesSafely(item: MessagesListViewHolder.MessageViewHolder?, mismatchDescription: Description?): Boolean { + val messageSubjectView = item!!.itemView.findViewById(R.id.messageTitleTextView) + val actualSubject = messageSubjectView.text.toString() + val flagView = item.itemView.findViewById(R.id.messageTitleContainerLinearLayout) + .findViewById(R.id.flow_indicators_container) + .findViewById(id) + return if (messageSubjectView != null) { + subject == actualSubject && flagView.visibility == View.VISIBLE + } else { + messagesList.add(actualSubject) + false + } + } + + override fun describeMoreTo(description: Description?) { + description?.apply { + appendText("Message item with subject: \"$subject\"\n") + appendText("Here is the actual list of messages:\n") + } + messagesList.forEach { description?.appendText(" - \"$it\"\n") } + } + } + } + /** * Matches the Mailbox message represented by [MessagesListItemView] by message subject and sender. * Subject must be unique in a list in order to use this matcher. @@ -161,4 +207,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..5690b4b64 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 @@ -27,15 +27,16 @@ import androidx.test.espresso.matcher.ViewMatchers.withParent import ch.protonmail.android.R import ch.protonmail.android.uitests.robots.mailbox.MailboxMatchers.withFirstInstanceMessageSubject import ch.protonmail.android.uitests.robots.mailbox.MailboxMatchers.withMessageSubject +import ch.protonmail.android.uitests.robots.mailbox.MailboxMatchers.withMessageSubjectAndFlag import ch.protonmail.android.uitests.robots.mailbox.MailboxMatchers.withMessageSubjectAndRecipient 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.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 +44,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 +84,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 +100,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 +116,66 @@ 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 + .common.scrollToRecyclerViewMatchedItem(messagesRecyclerViewId, withFirstInstanceMessageSubject(subject)) + } + + fun messageWithSubjectHasRepliedFlag(subject: String) { + UIActions.recyclerView.common.waitForBeingPopulated(messagesRecyclerViewId) UIActions.wait.forViewWithText(subject) UIActions.recyclerView - .scrollToRecyclerViewMatchedItem(messagesRecyclerViewId, withFirstInstanceMessageSubject(subject)) + .common.scrollToRecyclerViewMatchedItem( + messagesRecyclerViewId, + withMessageSubjectAndFlag(subject, R.id.messageReplyTextView) + ) + } + + fun messageWithSubjectHasRepliedAllFlag(subject: String) { + UIActions.recyclerView.common.waitForBeingPopulated(messagesRecyclerViewId) + UIActions.wait.forViewWithText(subject) + UIActions.recyclerView + .common.scrollToRecyclerViewMatchedItem( + messagesRecyclerViewId, + withMessageSubjectAndFlag(subject, R.id.messageReplyAllTextView) + ) + } + + fun messageWithSubjectHasForwardedFlag(subject: String) { + UIActions.recyclerView.common.waitForBeingPopulated(messagesRecyclerViewId) + UIActions.wait.forViewWithText(subject) + UIActions.recyclerView + .common.scrollToRecyclerViewMatchedItem( + messagesRecyclerViewId, + withMessageSubjectAndFlag(subject, R.id.messageForwardTextView) + ) } 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 +218,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/composer/ForwardMessageTests.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/tests/composer/ForwardMessageTests.kt index 365d26f11..690861951 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/tests/composer/ForwardMessageTests.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/tests/composer/ForwardMessageTests.kt @@ -57,7 +57,10 @@ class ForwardMessageTests : BaseTest() { .forward() .forwardMessage(to, body) .navigateUpToSent() - .verify { messageWithSubjectExists(TestData.fwSubject(subject)) } + .verify { + messageWithSubjectExists(fwSubject(subject)) + messageWithSubjectHasForwardedFlag(subject) + } } @TestId("1950") diff --git a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/tests/composer/ReplyToMessageTests.kt b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/tests/composer/ReplyToMessageTests.kt index 75d975587..cf3a454f1 100644 --- a/app/src/uiTest/kotlin/ch/protonmail/android/uitests/tests/composer/ReplyToMessageTests.kt +++ b/app/src/uiTest/kotlin/ch/protonmail/android/uitests/tests/composer/ReplyToMessageTests.kt @@ -59,6 +59,7 @@ class ReplyToMessageTests : BaseTest() { .navigateUpToSent() .verify { messageWithSubjectExists(TestData.reSubject(subject)) + messageWithSubjectHasRepliedFlag(subject) } } @@ -95,6 +96,9 @@ class ReplyToMessageTests : BaseTest() { .replyAll() .editBodyAndReply(body, "Robot ReplyAll ") .navigateUpToSent() - .verify { messageWithSubjectExists(TestData.reSubject(subject)) } + .verify { + messageWithSubjectExists(TestData.reSubject(subject)) + messageWithSubjectHasRepliedAllFlag(subject) + } } } 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) +} diff --git a/buildSrc/src/main/kotlin/ProtonMail.kt b/buildSrc/src/main/kotlin/ProtonMail.kt index edeac4b39..b0971fa7e 100644 --- a/buildSrc/src/main/kotlin/ProtonMail.kt +++ b/buildSrc/src/main/kotlin/ProtonMail.kt @@ -22,8 +22,8 @@ * @author Davide Farella */ object ProtonMail { - const val versionName = "1.13.25" - const val versionCode = 762 + const val versionName = "1.13.28" + const val versionCode = 764 const val targetSdk = 30 const val minSdk = 21 diff --git a/buildSrc/src/main/kotlin/versionsConfig.kt b/buildSrc/src/main/kotlin/versionsConfig.kt index a07c709e3..24a3d6ce7 100644 --- a/buildSrc/src/main/kotlin/versionsConfig.kt +++ b/buildSrc/src/main/kotlin/versionsConfig.kt @@ -43,7 +43,7 @@ fun initVersions() { `lifecycle version` = "2.2.0-rc03" // Released: Dec 05, 2019 `material version` = "1.1.0-beta02" // Released: Nov 10, 2019 `android-paging version` = "2.1.0" // Released: Jan 26, 2019 - `android-room version` = "2.2.1" // Released: Oct 23, 2019 + `android-room version` = "2.2.6" // Released: Dec 16, 2020 `android-work version` = "2.4.0" // Released: Aug 19, 2020 `android-test version` = "1.3.1-alpha02" // Released: Oct 20, 2020 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/) 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) {