Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,25 @@ class ContactResolver @Inject constructor(
private val deviceContactLookup: DeviceContactLookup,
private val phoneUtils: PhoneUtils,
) {
suspend fun resolveName(e164: String, fallback: String = e164): String =
contactDataSource.getDisplayName(e164)
?: deviceContactLookup.lookupDisplayName(e164)
suspend fun resolveName(e164: String, fallback: String = e164): String {
val cached = contactDataSource.getDisplayName(e164)
val device = deviceContactLookup.lookupDisplayName(e164)

if (device != null) {
if (device != cached) contactDataSource.updateDisplayName(e164, device)
return device
}

return cached
?: runCatching { phoneUtils.formatNumber(e164) }.getOrDefault(fallback)
}

suspend fun resolvePhotoUri(e164: String): String? {
val cached = contactDataSource.getPhotoUri(e164)
if (cached != null) return cached

suspend fun resolvePhotoUri(e164: String): String? =
contactDataSource.getPhotoUri(e164)
val device = deviceContactLookup.lookupPhotoUri(e164) ?: return null
contactDataSource.updatePhotoUri(e164, device)
return device
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ package com.flipcash.app.contacts.device

interface DeviceContactLookup {
fun lookupDisplayName(e164: String): String?
fun lookupPhotoUri(e164: String): String?
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@ internal class AndroidDeviceContactLookup @Inject constructor(
@param:ApplicationContext private val context: Context,
) : DeviceContactLookup {

override fun lookupDisplayName(e164: String): String? {
override fun lookupDisplayName(e164: String): String? =
lookupField(e164, ContactsContract.PhoneLookup.DISPLAY_NAME)

override fun lookupPhotoUri(e164: String): String? =
lookupField(e164, ContactsContract.PhoneLookup.PHOTO_URI)

private fun lookupField(e164: String, column: String): String? {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED
) return null
Expand All @@ -28,7 +34,7 @@ internal class AndroidDeviceContactLookup @Inject constructor(
return try {
context.contentResolver.query(
uri,
arrayOf(ContactsContract.PhoneLookup.DISPLAY_NAME),
arrayOf(column),
null, null, null
)?.use { cursor ->
if (cursor.moveToFirst()) cursor.getString(0)?.takeIf { it.isNotEmpty() } else null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,17 @@ class ContactResolverTest {
formatted: String? = null,
formatThrows: Boolean = false,
photoUri: String? = null,
devicePhotoUri: String? = null,
): ContactResolver {
val dataSource = mockk<ContactDataSource> {
coEvery { getDisplayName(e164) } returns dbName
coEvery { getPhotoUri(e164) } returns photoUri
coEvery { updateDisplayName(any(), any()) } returns Unit
coEvery { updatePhotoUri(any(), any()) } returns Unit
}
val deviceLookup = mockk<DeviceContactLookup> {
every { lookupDisplayName(e164) } returns deviceName
every { lookupPhotoUri(e164) } returns devicePhotoUri
}
val phoneUtils = mockk<PhoneUtils> {
if (formatThrows) {
Expand All @@ -42,19 +46,19 @@ class ContactResolverTest {
// region resolveName

@Test
fun `DB display name is returned when present`() = runTest {
val result = resolver(dbName = "Alice").resolveName(e164)
assertEquals("Alice", result)
fun `device name is returned when present`() = runTest {
val result = resolver(deviceName = "Alice Device").resolveName(e164)
assertEquals("Alice Device", result)
}

@Test
fun `device lookup is used when DB returns null`() = runTest {
val result = resolver(deviceName = "Bob Device").resolveName(e164)
assertEquals("Bob Device", result)
fun `DB name is used when device returns null`() = runTest {
val result = resolver(dbName = "Alice DB").resolveName(e164)
assertEquals("Alice DB", result)
}

@Test
fun `formatted number is used when DB and device both return null`() = runTest {
fun `formatted number is used when device and DB both return null`() = runTest {
val result = resolver(formatted = "+1 (555) 123-4567").resolveName(e164)
assertEquals("+1 (555) 123-4567", result)
}
Expand All @@ -72,9 +76,9 @@ class ContactResolverTest {
}

@Test
fun `DB takes priority over device lookup`() = runTest {
fun `device takes priority over DB name`() = runTest {
val result = resolver(dbName = "Alice DB", deviceName = "Alice Device").resolveName(e164)
assertEquals("Alice DB", result)
assertEquals("Alice Device", result)
}

// endregion
Expand All @@ -88,10 +92,23 @@ class ContactResolverTest {
}

@Test
fun `null photo URI when data source has none`() = runTest {
fun `device photo URI used when data source has none`() = runTest {
val result = resolver(devicePhotoUri = "content://photo/2").resolvePhotoUri(e164)
assertEquals("content://photo/2", result)
}

@Test
fun `null photo URI when both sources have none`() = runTest {
val result = resolver().resolvePhotoUri(e164)
assertNull(result)
}

@Test
fun `data source photo URI takes priority over device`() = runTest {
val result = resolver(photoUri = "content://photo/1", devicePhotoUri = "content://photo/2")
.resolvePhotoUri(e164)
assertEquals("content://photo/1", result)
}

// endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,15 @@ interface ContactDao {
@Query("SELECT displayName FROM contact_mapping WHERE e164 = :e164 LIMIT 1")
suspend fun getDisplayName(e164: String): String?

@Query("UPDATE contact_mapping SET displayName = :displayName WHERE e164 = :e164")
suspend fun updateDisplayName(e164: String, displayName: String)

@Query("SELECT photoUri FROM contact_mapping WHERE e164 = :e164 LIMIT 1")
suspend fun getPhotoUri(e164: String): String?

@Query("UPDATE contact_mapping SET photoUri = :photoUri WHERE e164 = :e164")
suspend fun updatePhotoUri(e164: String, photoUri: String)

@Query("DELETE FROM contact_mapping WHERE e164 IN (:e164s)")
suspend fun deleteMappings(e164s: List<String>)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,16 @@ class ContactDataSource @Inject constructor(
suspend fun getDisplayName(e164: String): String? =
db?.contactDao()?.getDisplayName(e164)

suspend fun updateDisplayName(e164: String, displayName: String) {
db?.contactDao()?.updateDisplayName(e164, displayName)
}

suspend fun getPhotoUri(e164: String): String? =
db?.contactDao()?.getPhotoUri(e164)

suspend fun updatePhotoUri(e164: String, photoUri: String) {
db?.contactDao()?.updatePhotoUri(e164, photoUri)
}

// endregion
}
Loading