Skip to content

Commit 37a7dfd

Browse files
authored
fix(notifications): direct photo stream fallback when PHOTO_URI unavailable (#846)
PhoneLookup.PHOTO_URI can be null when the contacts provider hasn't finished indexing a newly added contact's photo. Fall back to openContactPhotoInputStream which reads the raw photo blob by contact ID and doesn't depend on PHOTO_URI being populated. Also iterate all PhoneLookup rows instead of only the first, since linked contacts can return multiple rows where only one has a photo. Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 1a48040 commit 37a7dfd

5 files changed

Lines changed: 42 additions & 9 deletions

File tree

apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/ContactResolver.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,7 @@ class ContactResolver @Inject constructor(
3333
contactDataSource.updatePhotoUri(e164, device)
3434
return device
3535
}
36+
37+
fun resolvePhotoBytes(e164: String): ByteArray? =
38+
deviceContactLookup.lookupPhotoBytes(e164)
3639
}

apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/device/DeviceContactLookup.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ package com.flipcash.app.contacts.device
33
interface DeviceContactLookup {
44
fun lookupDisplayName(e164: String): String?
55
fun lookupPhotoUri(e164: String): String?
6+
fun lookupPhotoBytes(e164: String): ByteArray?
67
}

apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/device/internal/AndroidDeviceContactLookup.kt

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.flipcash.app.contacts.device.internal
22

33
import android.Manifest
4+
import android.content.ContentUris
45
import android.content.Context
56
import android.content.pm.PackageManager
67
import android.provider.ContactsContract
@@ -21,6 +22,23 @@ internal class AndroidDeviceContactLookup @Inject constructor(
2122
override fun lookupPhotoUri(e164: String): String? =
2223
lookupField(e164, ContactsContract.PhoneLookup.PHOTO_URI)
2324

25+
override fun lookupPhotoBytes(e164: String): ByteArray? {
26+
val contactId = lookupContactId(e164) ?: return null
27+
val contactUri = ContentUris.withAppendedId(
28+
ContactsContract.Contacts.CONTENT_URI, contactId
29+
)
30+
return try {
31+
ContactsContract.Contacts.openContactPhotoInputStream(
32+
context.contentResolver, contactUri, true
33+
)?.use { it.readBytes() }
34+
} catch (e: Exception) {
35+
null
36+
}
37+
}
38+
39+
private fun lookupContactId(e164: String): Long? =
40+
lookupField(e164, ContactsContract.PhoneLookup._ID)?.toLongOrNull()
41+
2442
private fun lookupField(e164: String, column: String): String? {
2543
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS)
2644
!= PackageManager.PERMISSION_GRANTED
@@ -37,7 +55,11 @@ internal class AndroidDeviceContactLookup @Inject constructor(
3755
arrayOf(column),
3856
null, null, null
3957
)?.use { cursor ->
40-
if (cursor.moveToFirst()) cursor.getString(0)?.takeIf { it.isNotEmpty() } else null
58+
while (cursor.moveToNext()) {
59+
val value = cursor.getString(0)?.takeIf { it.isNotEmpty() }
60+
if (value != null) return@use value
61+
}
62+
null
4163
}
4264
} catch (e: Exception) {
4365
null

apps/flipcash/shared/contacts/src/test/kotlin/com/flipcash/app/contacts/ContactResolverTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class ContactResolverTest {
3232
val deviceLookup = mockk<DeviceContactLookup> {
3333
every { lookupDisplayName(e164) } returns deviceName
3434
every { lookupPhotoUri(e164) } returns devicePhotoUri
35+
every { lookupPhotoBytes(e164) } returns null
3536
}
3637
val phoneUtils = mockk<PhoneUtils> {
3738
if (formatThrows) {

apps/flipcash/shared/notifications/src/main/kotlin/com/flipcash/app/notifications/NotificationService.kt

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import android.content.Intent
77
import android.content.pm.PackageManager
88
import android.os.Build
99
import android.graphics.Bitmap
10+
import android.graphics.BitmapFactory
1011
import android.graphics.Canvas
1112
import android.graphics.ImageDecoder
1213
import android.graphics.Paint
@@ -219,16 +220,21 @@ class NotificationService : FirebaseMessagingService(),
219220
}
220221

221222
private suspend fun resolveContactPhoto(e164: String): Bitmap? {
222-
val uriString = contactResolver.resolvePhotoUri(e164) ?: return null
223-
return try {
224-
val source = ImageDecoder.createSource(contentResolver, uriString.toUri())
225-
ImageDecoder.decodeBitmap(source) { decoder, _, _ ->
226-
decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
223+
val uriString = contactResolver.resolvePhotoUri(e164)
224+
if (uriString != null) {
225+
try {
226+
val source = ImageDecoder.createSource(contentResolver, uriString.toUri())
227+
return ImageDecoder.decodeBitmap(source) { decoder, _, _ ->
228+
decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
229+
}
230+
} catch (e: Exception) {
231+
trace(tag = "NotificationService", message = "Failed to decode contact photo: ${e.message}", type = TraceType.Log)
227232
}
228-
} catch (e: Exception) {
229-
trace(tag = "NotificationService", message = "Failed to decode contact photo: ${e.message}", type = TraceType.Log)
230-
null
231233
}
234+
235+
// URI unavailable or decode failed — read directly from contacts provider
236+
val bytes = contactResolver.resolvePhotoBytes(e164) ?: return null
237+
return BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
232238
}
233239

234240
private suspend fun resolveSubstitution(substitution: Substitution): String {

0 commit comments

Comments
 (0)