diff --git a/mail-contact/dagger/src/main/kotlin/ch/protonmail/android/mailcontact/dagger/MailContactModule.kt b/mail-contact/dagger/src/main/kotlin/ch/protonmail/android/mailcontact/dagger/MailContactModule.kt
index 999e9a5130..1c02fa8eec 100644
--- a/mail-contact/dagger/src/main/kotlin/ch/protonmail/android/mailcontact/dagger/MailContactModule.kt
+++ b/mail-contact/dagger/src/main/kotlin/ch/protonmail/android/mailcontact/dagger/MailContactModule.kt
@@ -20,6 +20,7 @@ package ch.protonmail.android.mailcontact.dagger
import ch.protonmail.android.mailcontact.data.ContactDetailRepositoryImpl
import ch.protonmail.android.mailcontact.data.ContactGroupRepositoryImpl
+import ch.protonmail.android.mailcontact.data.DeviceContactsRepositoryImpl
import ch.protonmail.android.mailcontact.data.local.ContactDetailLocalDataSource
import ch.protonmail.android.mailcontact.data.local.ContactDetailLocalDataSourceImpl
import ch.protonmail.android.mailcontact.data.local.ContactGroupLocalDataSource
@@ -30,6 +31,7 @@ import ch.protonmail.android.mailcontact.data.remote.ContactGroupRemoteDataSourc
import ch.protonmail.android.mailcontact.data.remote.ContactGroupRemoteDataSourceImpl
import ch.protonmail.android.mailcontact.domain.repository.ContactDetailRepository
import ch.protonmail.android.mailcontact.domain.repository.ContactGroupRepository
+import ch.protonmail.android.mailcontact.domain.repository.DeviceContactsRepository
import dagger.Binds
import dagger.Module
import dagger.Reusable
@@ -66,4 +68,8 @@ abstract class MailContactModule {
@Reusable
abstract fun bindContactGroupRepository(impl: ContactGroupRepositoryImpl): ContactGroupRepository
+ @Binds
+ @Reusable
+ abstract fun bindDeviceContactsRepository(impl: DeviceContactsRepositoryImpl): DeviceContactsRepository
+
}
diff --git a/mail-contact/data/src/main/kotlin/ch/protonmail/android/mailcontact/data/DeviceContactsRepositoryImpl.kt b/mail-contact/data/src/main/kotlin/ch/protonmail/android/mailcontact/data/DeviceContactsRepositoryImpl.kt
new file mode 100644
index 0000000000..d9be6093ab
--- /dev/null
+++ b/mail-contact/data/src/main/kotlin/ch/protonmail/android/mailcontact/data/DeviceContactsRepositoryImpl.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2022 Proton Technologies AG
+ * This file is part of Proton Technologies AG and Proton Mail.
+ *
+ * Proton Mail 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.
+ *
+ * Proton Mail 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 Proton Mail. If not, see .
+ */
+
+package ch.protonmail.android.mailcontact.data
+
+import android.content.Context
+import android.provider.ContactsContract
+import arrow.core.Either
+import arrow.core.left
+import arrow.core.right
+import ch.protonmail.android.mailcontact.domain.model.DeviceContact
+import ch.protonmail.android.mailcontact.domain.repository.DeviceContactsRepository
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.withContext
+import me.proton.core.util.kotlin.DispatcherProvider
+import timber.log.Timber
+import javax.inject.Inject
+
+class DeviceContactsRepositoryImpl @Inject constructor(
+ @ApplicationContext private val context: Context,
+ private val dispatcherProvider: DispatcherProvider
+) : DeviceContactsRepository {
+
+ override suspend fun getDeviceContacts(
+ query: String
+ ): Either> {
+
+ val contentResolver = context.contentResolver
+
+ val selectionArgs = arrayOf("%$query%", "%$query%", "%$query%")
+
+ @Suppress("SwallowedException")
+ val contactEmails = try {
+ withContext(dispatcherProvider.Io) {
+ contentResolver.query(
+ ContactsContract.CommonDataKinds.Email.CONTENT_URI,
+ ANDROID_PROJECTION,
+ ANDROID_SELECTION,
+ selectionArgs,
+ ANDROID_ORDER_BY
+ )
+ }
+ } catch (e: SecurityException) {
+ Timber.d("SearchDeviceContacts: contact permission is not granted")
+ null
+ } ?: return DeviceContactsRepository.DeviceContactsErrors.PermissionDenied.left()
+
+ val deviceContacts = mutableListOf()
+
+ val displayNameColumnIndex = contactEmails.getColumnIndex(
+ ContactsContract.CommonDataKinds.Email.DISPLAY_NAME_PRIMARY
+ ).takeIf {
+ it >= 0
+ } ?: 0
+
+ val emailColumnIndex = contactEmails.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS).takeIf {
+ it >= 0
+ } ?: 0
+
+ contactEmails.use { cursor ->
+ for (position in 0 until cursor.count) {
+ cursor.moveToPosition(position)
+ deviceContacts.add(
+ DeviceContact(
+ name = contactEmails.getString(displayNameColumnIndex),
+ email = contactEmails.getString(emailColumnIndex)
+ )
+ )
+ }
+ }
+
+ return deviceContacts.right()
+ }
+
+ companion object {
+
+ private const val ANDROID_ORDER_BY = ContactsContract.CommonDataKinds.Email.DISPLAY_NAME_PRIMARY + " ASC"
+
+ @Suppress("MaxLineLength")
+ private const val ANDROID_SELECTION =
+ "${ContactsContract.CommonDataKinds.Email.DISPLAY_NAME_PRIMARY} LIKE ? OR ${ContactsContract.CommonDataKinds.Email.ADDRESS} LIKE ? OR ${ContactsContract.CommonDataKinds.Email.DATA} LIKE ?"
+
+ private val ANDROID_PROJECTION = arrayOf(
+ ContactsContract.CommonDataKinds.Email.DISPLAY_NAME_PRIMARY,
+ ContactsContract.CommonDataKinds.Email.ADDRESS,
+ ContactsContract.CommonDataKinds.Email.DATA
+ )
+ }
+
+}
diff --git a/mail-contact/domain/src/test/kotlin/ch/protonmail/android/mailcontact/domain/usecase/SearchDeviceContactsTest.kt b/mail-contact/data/src/test/kotlin/ch/protonmail/android/mailcontact/data/DeviceContactsRepositoryImplTest.kt
similarity index 70%
rename from mail-contact/domain/src/test/kotlin/ch/protonmail/android/mailcontact/domain/usecase/SearchDeviceContactsTest.kt
rename to mail-contact/data/src/test/kotlin/ch/protonmail/android/mailcontact/data/DeviceContactsRepositoryImplTest.kt
index 5ed88b8c26..469e382ba9 100644
--- a/mail-contact/domain/src/test/kotlin/ch/protonmail/android/mailcontact/domain/usecase/SearchDeviceContactsTest.kt
+++ b/mail-contact/data/src/test/kotlin/ch/protonmail/android/mailcontact/data/DeviceContactsRepositoryImplTest.kt
@@ -1,29 +1,11 @@
-/*
- * Copyright (c) 2022 Proton Technologies AG
- * This file is part of Proton Technologies AG and Proton Mail.
- *
- * Proton Mail 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.
- *
- * Proton Mail 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 Proton Mail. If not, see .
- */
-
-package ch.protonmail.android.mailcontact.domain.usecase
+package ch.protonmail.android.mailcontact.data
import android.content.ContentResolver
import android.content.Context
import android.database.Cursor
import android.provider.ContactsContract
import arrow.core.left
-import ch.protonmail.android.mailcontact.domain.model.GetContactError
+import ch.protonmail.android.mailcontact.domain.repository.DeviceContactsRepository
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
@@ -31,12 +13,12 @@ import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import me.proton.core.test.kotlin.TestDispatcherProvider
-import org.junit.Assert.assertTrue
-import org.junit.Test
+import org.junit.Assert
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
-class SearchDeviceContactsTest {
+@Suppress("MaxLineLength")
+class DeviceContactsRepositoryImplTest {
private val columnIndexDisplayName = 1
private val columnIndexEmail = 2
@@ -59,7 +41,7 @@ class SearchDeviceContactsTest {
}
private val testDispatcherProvider = TestDispatcherProvider()
- private val searchDeviceContacts = SearchDeviceContacts(
+ private val deviceContactsRepository = DeviceContactsRepositoryImpl(
contextMock,
testDispatcherProvider
)
@@ -80,7 +62,7 @@ class SearchDeviceContactsTest {
every { cursorMock.count } returns count
}
- @Test
+ @org.junit.Test
fun `when there are multiple matching contacts, they are emitted`() = runTest(testDispatcherProvider.Main) {
// Given
val query = "cont"
@@ -89,16 +71,16 @@ class SearchDeviceContactsTest {
expectContactsCount(2)
// When
- val actual = searchDeviceContacts(query).getOrNull()
+ val actual = deviceContactsRepository.getDeviceContacts(query).getOrNull()
// Then
assertNotNull(actual)
- assertTrue(actual.size == 2)
+ Assert.assertTrue(actual.size == 2)
verify(exactly = 2) { cursorMock.getString(columnIndexDisplayName) }
verify(exactly = 2) { cursorMock.getString(columnIndexEmail) }
}
- @Test
+ @org.junit.Test
fun `when there are no matching contacts, empty list is emitted`() = runTest(testDispatcherProvider.Main) {
// Given
val query = "cont"
@@ -107,16 +89,16 @@ class SearchDeviceContactsTest {
expectContactsCount(0)
// When
- val actual = searchDeviceContacts(query).getOrNull()
+ val actual = deviceContactsRepository.getDeviceContacts(query).getOrNull()
// Then
assertNotNull(actual)
- assertTrue(actual.size == 0)
+ Assert.assertTrue(actual.size == 0)
verify(exactly = 0) { cursorMock.getString(columnIndexDisplayName) }
verify(exactly = 0) { cursorMock.getString(columnIndexEmail) }
}
- @Test
+ @org.junit.Test
fun `when content resolver throws SecurityException, left is emitted`() = runTest(testDispatcherProvider.Main) {
// Given
val query = "cont"
@@ -125,11 +107,12 @@ class SearchDeviceContactsTest {
expectContactsCount(0)
// When
- val actual = searchDeviceContacts(query)
+ val actual = deviceContactsRepository.getDeviceContacts(query)
// Then
- assertEquals(GetContactError.left(), actual)
+ assertEquals(DeviceContactsRepository.DeviceContactsErrors.PermissionDenied.left(), actual)
verify(exactly = 0) { cursorMock.getString(columnIndexDisplayName) }
verify(exactly = 0) { cursorMock.getString(columnIndexEmail) }
}
+
}
diff --git a/mail-contact/domain/src/main/kotlin/ch/protonmail/android/mailcontact/domain/repository/DeviceContactsRepository.kt b/mail-contact/domain/src/main/kotlin/ch/protonmail/android/mailcontact/domain/repository/DeviceContactsRepository.kt
new file mode 100644
index 0000000000..fc2977d3db
--- /dev/null
+++ b/mail-contact/domain/src/main/kotlin/ch/protonmail/android/mailcontact/domain/repository/DeviceContactsRepository.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2022 Proton Technologies AG
+ * This file is part of Proton Technologies AG and Proton Mail.
+ *
+ * Proton Mail 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.
+ *
+ * Proton Mail 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 Proton Mail. If not, see .
+ */
+
+package ch.protonmail.android.mailcontact.domain.repository
+
+import arrow.core.Either
+import ch.protonmail.android.mailcontact.domain.model.DeviceContact
+
+interface DeviceContactsRepository {
+
+ suspend fun getDeviceContacts(query: String): Either>
+
+ sealed class DeviceContactsErrors {
+ data object PermissionDenied : DeviceContactsErrors()
+ }
+
+}
diff --git a/mail-contact/domain/src/main/kotlin/ch/protonmail/android/mailcontact/domain/usecase/SearchDeviceContacts.kt b/mail-contact/domain/src/main/kotlin/ch/protonmail/android/mailcontact/domain/usecase/SearchDeviceContacts.kt
index 6990bbdbd0..6cdc2dbd27 100644
--- a/mail-contact/domain/src/main/kotlin/ch/protonmail/android/mailcontact/domain/usecase/SearchDeviceContacts.kt
+++ b/mail-contact/domain/src/main/kotlin/ch/protonmail/android/mailcontact/domain/usecase/SearchDeviceContacts.kt
@@ -18,84 +18,20 @@
package ch.protonmail.android.mailcontact.domain.usecase
-import android.content.Context
-import android.provider.ContactsContract
import arrow.core.Either
-import arrow.core.left
-import arrow.core.right
import ch.protonmail.android.mailcontact.domain.model.DeviceContact
import ch.protonmail.android.mailcontact.domain.model.GetContactError
-import dagger.hilt.android.qualifiers.ApplicationContext
-import kotlinx.coroutines.withContext
-import me.proton.core.util.kotlin.DispatcherProvider
-import timber.log.Timber
+import ch.protonmail.android.mailcontact.domain.repository.DeviceContactsRepository
import javax.inject.Inject
class SearchDeviceContacts @Inject constructor(
- @ApplicationContext private val context: Context,
- private val dispatcherProvider: DispatcherProvider
+ private val deviceContactsRepository: DeviceContactsRepository
) {
suspend operator fun invoke(query: String): Either> {
-
- val contentResolver = context.contentResolver
-
- val selectionArgs = arrayOf("%$query%", "%$query%", "%$query%")
-
- @Suppress("SwallowedException")
- val contactEmails = try {
- withContext(dispatcherProvider.Io) {
- contentResolver.query(
- ContactsContract.CommonDataKinds.Email.CONTENT_URI,
- ANDROID_PROJECTION,
- ANDROID_SELECTION,
- selectionArgs,
- ANDROID_ORDER_BY
- )
- }
- } catch (e: SecurityException) {
- Timber.d("SearchDeviceContacts: contact permission is not granted")
- null
- } ?: return GetContactError.left()
-
- val deviceContacts = mutableListOf()
-
- val displayNameColumnIndex = contactEmails.getColumnIndex(
- ContactsContract.CommonDataKinds.Email.DISPLAY_NAME_PRIMARY
- ).takeIf {
- it >= 0
- } ?: 0
-
- val emailColumnIndex = contactEmails.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS).takeIf {
- it >= 0
- } ?: 0
-
- contactEmails.use { cursor ->
- for (position in 0 until cursor.count) {
- cursor.moveToPosition(position)
- deviceContacts.add(
- DeviceContact(
- name = contactEmails.getString(displayNameColumnIndex),
- email = contactEmails.getString(emailColumnIndex)
- )
- )
- }
+ return deviceContactsRepository.getDeviceContacts(query).mapLeft {
+ GetContactError
}
-
- return deviceContacts.right()
}
- companion object {
-
- private const val ANDROID_ORDER_BY = ContactsContract.CommonDataKinds.Email.DISPLAY_NAME_PRIMARY + " ASC"
-
- @Suppress("MaxLineLength")
- private const val ANDROID_SELECTION = "${ContactsContract.CommonDataKinds.Email.DISPLAY_NAME_PRIMARY} LIKE ? OR ${ContactsContract.CommonDataKinds.Email.ADDRESS} LIKE ? OR ${ContactsContract.CommonDataKinds.Email.DATA} LIKE ?"
-
- private val ANDROID_PROJECTION = arrayOf(
- ContactsContract.CommonDataKinds.Email.DISPLAY_NAME_PRIMARY,
- ContactsContract.CommonDataKinds.Email.ADDRESS,
- ContactsContract.CommonDataKinds.Email.DATA
- )
- }
}