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 @@ -33,4 +33,7 @@ class ContactResolver @Inject constructor(
contactDataSource.updatePhotoUri(e164, device)
return device
}

fun resolvePhotoBytes(e164: String): ByteArray? =
deviceContactLookup.lookupPhotoBytes(e164)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ package com.flipcash.app.contacts.device
interface DeviceContactLookup {
fun lookupDisplayName(e164: String): String?
fun lookupPhotoUri(e164: String): String?
fun lookupPhotoBytes(e164: String): ByteArray?
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.flipcash.app.contacts.device.internal

import android.Manifest
import android.content.ContentUris
import android.content.Context
import android.content.pm.PackageManager
import android.provider.ContactsContract
Expand All @@ -21,6 +22,23 @@ internal class AndroidDeviceContactLookup @Inject constructor(
override fun lookupPhotoUri(e164: String): String? =
lookupField(e164, ContactsContract.PhoneLookup.PHOTO_URI)

override fun lookupPhotoBytes(e164: String): ByteArray? {
val contactId = lookupContactId(e164) ?: return null
val contactUri = ContentUris.withAppendedId(
ContactsContract.Contacts.CONTENT_URI, contactId
)
return try {
ContactsContract.Contacts.openContactPhotoInputStream(
context.contentResolver, contactUri, true
)?.use { it.readBytes() }
} catch (e: Exception) {
null
}
}

private fun lookupContactId(e164: String): Long? =
lookupField(e164, ContactsContract.PhoneLookup._ID)?.toLongOrNull()

private fun lookupField(e164: String, column: String): String? {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED
Expand All @@ -37,7 +55,11 @@ internal class AndroidDeviceContactLookup @Inject constructor(
arrayOf(column),
null, null, null
)?.use { cursor ->
if (cursor.moveToFirst()) cursor.getString(0)?.takeIf { it.isNotEmpty() } else null
while (cursor.moveToNext()) {
val value = cursor.getString(0)?.takeIf { it.isNotEmpty() }
if (value != null) return@use value
}
null
}
} catch (e: Exception) {
null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class ContactResolverTest {
val deviceLookup = mockk<DeviceContactLookup> {
every { lookupDisplayName(e164) } returns deviceName
every { lookupPhotoUri(e164) } returns devicePhotoUri
every { lookupPhotoBytes(e164) } returns null
}
val phoneUtils = mockk<PhoneUtils> {
if (formatThrows) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.ImageDecoder
import android.graphics.Paint
Expand Down Expand Up @@ -219,16 +220,21 @@ class NotificationService : FirebaseMessagingService(),
}

private suspend fun resolveContactPhoto(e164: String): Bitmap? {
val uriString = contactResolver.resolvePhotoUri(e164) ?: return null
return try {
val source = ImageDecoder.createSource(contentResolver, uriString.toUri())
ImageDecoder.decodeBitmap(source) { decoder, _, _ ->
decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
val uriString = contactResolver.resolvePhotoUri(e164)
if (uriString != null) {
try {
val source = ImageDecoder.createSource(contentResolver, uriString.toUri())
return ImageDecoder.decodeBitmap(source) { decoder, _, _ ->
decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
}
} catch (e: Exception) {
trace(tag = "NotificationService", message = "Failed to decode contact photo: ${e.message}", type = TraceType.Log)
}
} catch (e: Exception) {
trace(tag = "NotificationService", message = "Failed to decode contact photo: ${e.message}", type = TraceType.Log)
null
}

// URI unavailable or decode failed — read directly from contacts provider
val bytes = contactResolver.resolvePhotoBytes(e164) ?: return null
return BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
}

private suspend fun resolveSubstitution(substitution: Substitution): String {
Expand Down
Loading