Skip to content

Commit

Permalink
Move logic from SearchDeviceContacts into repository
Browse files Browse the repository at this point in the history
  • Loading branch information
Adam Jodlowski committed May 23, 2024
1 parent 6efb5a9 commit b018889
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 101 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -66,4 +68,8 @@ abstract class MailContactModule {
@Reusable
abstract fun bindContactGroupRepository(impl: ContactGroupRepositoryImpl): ContactGroupRepository

@Binds
@Reusable
abstract fun bindDeviceContactsRepository(impl: DeviceContactsRepositoryImpl): DeviceContactsRepository

}
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/

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<DeviceContactsRepository.DeviceContactsErrors, List<DeviceContact>> {

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<DeviceContact>()

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
)
}

}
Original file line number Diff line number Diff line change
@@ -1,42 +1,24 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/

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
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
Expand All @@ -59,7 +41,7 @@ class SearchDeviceContactsTest {
}
private val testDispatcherProvider = TestDispatcherProvider()

private val searchDeviceContacts = SearchDeviceContacts(
private val deviceContactsRepository = DeviceContactsRepositoryImpl(
contextMock,
testDispatcherProvider
)
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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) }
}

}
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/

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<DeviceContactsErrors, List<DeviceContact>>

sealed class DeviceContactsErrors {
data object PermissionDenied : DeviceContactsErrors()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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<GetContactError, List<DeviceContact>> {

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<DeviceContact>()

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
)
}
}

0 comments on commit b018889

Please sign in to comment.