From c88b3cb2dc5c5af24b83c2cc50b0447637d6cbcc Mon Sep 17 00:00:00 2001 From: Matvei Plokhov Date: Sun, 12 Jul 2026 23:00:27 +0200 Subject: [PATCH 1/4] Fix secondary user SMS notification hiding sender and message --- AndroidManifest.xml | 3 - .../repository/ContactsRepositoryImplTest.kt | 5 +- .../store/ConversationListStatusStoreTest.kt | 13 +- .../SecondaryUserMessageResolverTest.kt | 72 +++++ .../mapper/TargetUiStateMapperImplTest.kt | 13 +- .../RecipientPickerDelegateImplTest.kt | 6 +- src/com/android/messaging/BugleApplication.kt | 8 +- .../formatter/ContactDestinationFormatter.kt | 24 +- .../store/ConversationListStatusStore.kt | 8 +- .../phone/formatter/PhoneNumberFormatter.kt | 45 +++ .../SecondaryUserMessageResolver.kt | 41 +++ .../secondaryuser/SecondaryUserNotifier.kt | 75 +++++ .../secondaryuser/SmsContactNameLookup.kt | 27 ++ .../model/SecondaryUserMessageInfo.kt | 6 + .../data/sms/IncomingSmsDeliverer.kt | 84 ++++++ .../messaging/data/sms/IncomingSmsParser.kt | 75 +++++ .../messaging/data/sms/SmsReceiverToggle.kt | 44 +++ .../datamodel/data/ConversationListData.java | 24 -- .../messaging/di/core/CoreProvidesModule.kt | 7 + .../messaging/di/phone/PhoneBindsModule.kt | 20 ++ .../di/receiver/IncomingSmsEntryPoint.kt | 30 ++ .../secondaryuser/SecondaryUserBindsModule.kt | 36 +++ .../messaging/di/sms/SmsBindsModule.kt | 36 +++ .../BootAndPackageReplacedReceiver.java | 49 ---- .../BootAndPackageReplacedReceiver.kt | 37 +++ .../receiver/SmsDeliverReceiver.java | 34 --- .../messaging/receiver/SmsDeliverReceiver.kt | 44 +++ .../messaging/receiver/SmsReceiver.java | 261 ------------------ .../android/messaging/receiver/SmsReceiver.kt | 71 +++++ .../mapper/TargetUiStateMapper.kt | 5 +- .../delegate/RecipientPickerDelegate.kt | 16 +- .../android/messaging/util/DebugUtils.java | 4 +- 32 files changed, 801 insertions(+), 422 deletions(-) create mode 100644 app/src/test/kotlin/com/android/messaging/data/secondaryuser/SecondaryUserMessageResolverTest.kt create mode 100644 src/com/android/messaging/data/phone/formatter/PhoneNumberFormatter.kt create mode 100644 src/com/android/messaging/data/secondaryuser/SecondaryUserMessageResolver.kt create mode 100644 src/com/android/messaging/data/secondaryuser/SecondaryUserNotifier.kt create mode 100644 src/com/android/messaging/data/secondaryuser/SmsContactNameLookup.kt create mode 100644 src/com/android/messaging/data/secondaryuser/model/SecondaryUserMessageInfo.kt create mode 100644 src/com/android/messaging/data/sms/IncomingSmsDeliverer.kt create mode 100644 src/com/android/messaging/data/sms/IncomingSmsParser.kt create mode 100644 src/com/android/messaging/data/sms/SmsReceiverToggle.kt create mode 100644 src/com/android/messaging/di/phone/PhoneBindsModule.kt create mode 100644 src/com/android/messaging/di/receiver/IncomingSmsEntryPoint.kt create mode 100644 src/com/android/messaging/di/secondaryuser/SecondaryUserBindsModule.kt create mode 100644 src/com/android/messaging/di/sms/SmsBindsModule.kt delete mode 100644 src/com/android/messaging/receiver/BootAndPackageReplacedReceiver.java create mode 100644 src/com/android/messaging/receiver/BootAndPackageReplacedReceiver.kt delete mode 100644 src/com/android/messaging/receiver/SmsDeliverReceiver.java create mode 100644 src/com/android/messaging/receiver/SmsDeliverReceiver.kt delete mode 100644 src/com/android/messaging/receiver/SmsReceiver.java create mode 100644 src/com/android/messaging/receiver/SmsReceiver.kt diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 1a28947f3..22d885cb4 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -341,9 +341,6 @@ - - - (relaxed = true) private val syncManager = mockk() + private val secondaryUserNotifier = mockk(relaxed = true) - private val store = ConversationListStatusStoreImpl() + private val store = ConversationListStatusStoreImpl(secondaryUserNotifier) @Before fun setUp() { mockkStatic(DataModel::class) - mockkStatic(SmsReceiver::class) every { DataModel.get() } returns dataModel every { dataModel.syncManager } returns syncManager - every { SmsReceiver.cancelSecondaryUserNotification() } just runs } @After @@ -49,7 +46,7 @@ internal class ConversationListStatusStoreTest { store.setNewestConversationVisible(isVisible = true) verify { dataModel.isConversationListScrolledToNewestConversation = true } - verify { SmsReceiver.cancelSecondaryUserNotification() } + verify { secondaryUserNotifier.cancel() } } @Test @@ -60,7 +57,7 @@ internal class ConversationListStatusStoreTest { dataModel.isConversationListScrolledToNewestConversation = false } verify(exactly = 0) { - SmsReceiver.cancelSecondaryUserNotification() + secondaryUserNotifier.cancel() } } } diff --git a/app/src/test/kotlin/com/android/messaging/data/secondaryuser/SecondaryUserMessageResolverTest.kt b/app/src/test/kotlin/com/android/messaging/data/secondaryuser/SecondaryUserMessageResolverTest.kt new file mode 100644 index 000000000..fb23e191f --- /dev/null +++ b/app/src/test/kotlin/com/android/messaging/data/secondaryuser/SecondaryUserMessageResolverTest.kt @@ -0,0 +1,72 @@ +package com.android.messaging.data.secondaryuser + +import com.android.messaging.data.phone.formatter.PhoneNumberFormatter +import com.android.messaging.data.secondaryuser.model.SecondaryUserMessageInfo +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +internal class SecondaryUserMessageResolverTest { + + private val contactNameLookup = mockk() + private val phoneNumberFormatter = mockk() + + private val resolver = SecondaryUserMessageResolverImpl( + contactNameLookup = contactNameLookup, + phoneNumberFormatter = phoneNumberFormatter, + ) + + @Test + fun resolve_contactFound_usesContactName() { + every { contactNameLookup.lookup("+15551234") } returns "Alice" + + val info = resolver.resolve(address = "+15551234", body = "Hello there") + assertEquals( + SecondaryUserMessageInfo(sender = "Alice", body = "Hello there"), + info, + ) + } + + @Test + fun resolve_noMatchingContact_fallsBackToFormattedNumber() { + every { contactNameLookup.lookup("+15551234") } returns null + every { phoneNumberFormatter.formatForDisplay("+15551234") } returns "+1 555-1234" + + val info = resolver.resolve(address = "+15551234", body = "Hello there") + assertEquals( + SecondaryUserMessageInfo(sender = "+1 555-1234", body = "Hello there"), + info, + ) + } + + @Test + fun resolve_blankContactName_fallsBackToFormattedNumber() { + every { contactNameLookup.lookup("+15551234") } returns "" + every { phoneNumberFormatter.formatForDisplay("+15551234") } returns "+1 555-1234" + + val info = resolver.resolve(address = "+15551234", body = "Hello there") + assertEquals("+1 555-1234", info?.sender) + } + + @Test + fun resolve_nullBody_returnsNull() { + assertNull(resolver.resolve(address = "+15551234", body = null)) + } + + @Test + fun resolve_emptyBody_returnsNull() { + assertNull(resolver.resolve(address = "+15551234", body = "")) + } + + @Test + fun resolve_nullAddress_returnsNull() { + assertNull(resolver.resolve(address = null, body = "Hello there")) + } + + @Test + fun resolve_emptyAddress_returnsNull() { + assertNull(resolver.resolve(address = "", body = "Hello there")) + } +} diff --git a/app/src/test/kotlin/com/android/messaging/ui/conversationpicker/mapper/TargetUiStateMapperImplTest.kt b/app/src/test/kotlin/com/android/messaging/ui/conversationpicker/mapper/TargetUiStateMapperImplTest.kt index 2e75934d4..79e7f60b4 100644 --- a/app/src/test/kotlin/com/android/messaging/ui/conversationpicker/mapper/TargetUiStateMapperImplTest.kt +++ b/app/src/test/kotlin/com/android/messaging/ui/conversationpicker/mapper/TargetUiStateMapperImplTest.kt @@ -2,12 +2,11 @@ package com.android.messaging.ui.conversationpicker.mapper import com.android.messaging.data.contact.formatter.ContactDestinationFormatter import com.android.messaging.data.conversationpicker.model.TargetConversation +import com.android.messaging.data.phone.formatter.PhoneNumberFormatter import com.android.messaging.domain.conversation.usecase.avatar.ResolveAvatarUri import com.android.messaging.ui.conversationpicker.model.TargetUiState -import com.android.messaging.util.PhoneUtils import io.mockk.every import io.mockk.mockk -import io.mockk.mockkStatic import io.mockk.unmockkAll import kotlinx.collections.immutable.persistentListOf import org.junit.After @@ -23,7 +22,9 @@ import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) internal class TargetUiStateMapperImplTest { - private val phoneUtilsInstance = mockk(relaxed = true) + private val phoneNumberFormatter = mockk { + every { formatForDisplay(any()) } answers { "formatted:${firstArg()}" } + } private val contactDestinationFormatter = mockk { every { canonicalize(any()) } answers { "canonical:${firstArg()}" } @@ -33,16 +34,12 @@ internal class TargetUiStateMapperImplTest { private val mapper = TargetUiStateMapperImpl( contactDestinationFormatter = contactDestinationFormatter, + phoneNumberFormatter = phoneNumberFormatter, resolveAvatarUri = resolveAvatarUri, ) @Before fun setUp() { - mockkStatic(PhoneUtils::class) - every { PhoneUtils.getDefault() } returns phoneUtilsInstance - every { phoneUtilsInstance.formatForDisplay(any()) } answers { - "formatted:${firstArg()}" - } every { resolveAvatarUri(any()) } returns null } diff --git a/app/src/test/kotlin/com/android/messaging/ui/recipientselection/delegate/RecipientPickerDelegateImplTest.kt b/app/src/test/kotlin/com/android/messaging/ui/recipientselection/delegate/RecipientPickerDelegateImplTest.kt index 4cf704a56..3d57577f6 100644 --- a/app/src/test/kotlin/com/android/messaging/ui/recipientselection/delegate/RecipientPickerDelegateImplTest.kt +++ b/app/src/test/kotlin/com/android/messaging/ui/recipientselection/delegate/RecipientPickerDelegateImplTest.kt @@ -6,6 +6,7 @@ import com.android.messaging.data.contact.model.Contact import com.android.messaging.data.contact.model.ContactDestination import com.android.messaging.data.contact.model.ContactsPage import com.android.messaging.data.contact.repository.ContactsRepository +import com.android.messaging.data.phone.formatter.PhoneNumberFormatterImpl import com.android.messaging.domain.contacts.usecase.IsReadContactsPermissionGranted import com.android.messaging.sms.MmsSmsUtils import com.android.messaging.testutil.MainDispatcherRule @@ -438,7 +439,10 @@ class RecipientPickerDelegateImplTest { isPermissionGranted: Boolean = true, ): RecipientPickerDelegateImpl { return RecipientPickerDelegateImpl( - contactDestinationFormatter = ContactDestinationFormatterImpl(), + contactDestinationFormatter = ContactDestinationFormatterImpl( + PhoneNumberFormatterImpl(phoneUtilsInstance), + ), + phoneNumberFormatter = PhoneNumberFormatterImpl(phoneUtilsInstance), contactUiModelMapper = ContactUiModelMapperImpl(), contactsRepository = mockContactsRepository(pages = pages), isReadContactsPermissionGranted = mockIsReadContactsPermissionGranted( diff --git a/src/com/android/messaging/BugleApplication.kt b/src/com/android/messaging/BugleApplication.kt index 4a0105213..bb4b4460b 100644 --- a/src/com/android/messaging/BugleApplication.kt +++ b/src/com/android/messaging/BugleApplication.kt @@ -29,8 +29,8 @@ import android.telephony.CarrierConfigManager import android.util.Log import androidx.appcompat.mms.CarrierConfigValuesLoader import androidx.appcompat.mms.MmsManager +import com.android.messaging.di.receiver.IncomingSmsEntryPoint import com.android.messaging.domain.notification.usecase.MigrateConversationNotificationChannels -import com.android.messaging.receiver.SmsReceiver import com.android.messaging.sms.BugleUserAgentInfoLoader import com.android.messaging.sms.MmsConfig import com.android.messaging.ui.ConversationDrawables @@ -41,6 +41,7 @@ import com.android.messaging.util.NotificationChannelUtil import com.android.messaging.util.PhoneUtils import com.android.messaging.util.Trace import com.google.common.annotations.VisibleForTesting +import dagger.hilt.android.EntryPointAccessors import dagger.hilt.android.HiltAndroidApp import java.io.File import javax.inject.Inject @@ -268,10 +269,11 @@ open class BugleApplication : return runningTests } - @JvmStatic fun updateAppConfig(context: Context) { // Make sure we set the correct state for the SMS/MMS receivers. - SmsReceiver.updateSmsReceiveHandler(context) + EntryPointAccessors.fromApplication(context, IncomingSmsEntryPoint::class.java) + .smsReceiverToggle() + .update(context) } } } diff --git a/src/com/android/messaging/data/contact/formatter/ContactDestinationFormatter.kt b/src/com/android/messaging/data/contact/formatter/ContactDestinationFormatter.kt index 08b08b0d6..069dccc2b 100644 --- a/src/com/android/messaging/data/contact/formatter/ContactDestinationFormatter.kt +++ b/src/com/android/messaging/data/contact/formatter/ContactDestinationFormatter.kt @@ -1,7 +1,7 @@ package com.android.messaging.data.contact.formatter +import com.android.messaging.data.phone.formatter.PhoneNumberFormatter import com.android.messaging.sms.MmsSmsUtils -import com.android.messaging.util.PhoneUtils import java.util.Locale import javax.inject.Inject @@ -16,13 +16,13 @@ internal interface ContactDestinationFormatter { fun countryCandidates(): List } -internal class ContactDestinationFormatterImpl @Inject constructor() : ContactDestinationFormatter { +internal class ContactDestinationFormatterImpl @Inject constructor( + private val phoneNumberFormatter: PhoneNumberFormatter, +) : ContactDestinationFormatter { override fun canonicalize(value: String): String { return canonicalize(value = value) { trimmed -> - PhoneUtils - .getDefault() - .getCanonicalForEnteredPhoneNumber(trimmed) + phoneNumberFormatter.getCanonicalForEnteredNumber(trimmed) } } @@ -31,24 +31,16 @@ internal class ContactDestinationFormatterImpl @Inject constructor() : ContactDe countryCandidates: List, ): String { return canonicalize(value = value) { trimmed -> - PhoneUtils - .getDefault() - .getCanonicalForEnteredPhoneNumber(trimmed, countryCandidates) + phoneNumberFormatter.getCanonicalForEnteredNumber(trimmed, countryCandidates) } } override fun formatPhoneForDisplay(value: String): String { - return PhoneUtils.getDefault().formatForDisplay(value) + return phoneNumberFormatter.formatForDisplay(value) } override fun countryCandidates(): List { - val phoneUtils = PhoneUtils - .getDefault() - .apply { - warmUp() - } - - return phoneUtils.countryCandidatesForEnteredPhoneNumber + return phoneNumberFormatter.countryCandidates() } private inline fun canonicalize( diff --git a/src/com/android/messaging/data/conversationlist/store/ConversationListStatusStore.kt b/src/com/android/messaging/data/conversationlist/store/ConversationListStatusStore.kt index 4dfe7efca..72f6095ff 100644 --- a/src/com/android/messaging/data/conversationlist/store/ConversationListStatusStore.kt +++ b/src/com/android/messaging/data/conversationlist/store/ConversationListStatusStore.kt @@ -1,7 +1,7 @@ package com.android.messaging.data.conversationlist.store +import com.android.messaging.data.secondaryuser.SecondaryUserNotifier import com.android.messaging.datamodel.DataModel -import com.android.messaging.receiver.SmsReceiver import javax.inject.Inject internal interface ConversationListStatusStore { @@ -9,7 +9,9 @@ internal interface ConversationListStatusStore { fun setNewestConversationVisible(isVisible: Boolean) } -internal class ConversationListStatusStoreImpl @Inject constructor() : ConversationListStatusStore { +internal class ConversationListStatusStoreImpl @Inject constructor( + private val secondaryUserNotifier: SecondaryUserNotifier, +) : ConversationListStatusStore { override fun hasFirstSyncCompleted(): Boolean { val dataModel = DataModel.get() @@ -21,7 +23,7 @@ internal class ConversationListStatusStoreImpl @Inject constructor() : Conversat dataModel.isConversationListScrolledToNewestConversation = isVisible if (isVisible) { - SmsReceiver.cancelSecondaryUserNotification() + secondaryUserNotifier.cancel() } } } diff --git a/src/com/android/messaging/data/phone/formatter/PhoneNumberFormatter.kt b/src/com/android/messaging/data/phone/formatter/PhoneNumberFormatter.kt new file mode 100644 index 000000000..5ba19adf0 --- /dev/null +++ b/src/com/android/messaging/data/phone/formatter/PhoneNumberFormatter.kt @@ -0,0 +1,45 @@ +package com.android.messaging.data.phone.formatter + +import com.android.messaging.util.PhoneUtils +import javax.inject.Inject + +internal interface PhoneNumberFormatter { + fun formatForDisplay(number: String): String + fun formatForDisplayUsingSimCountry(number: String): String + fun formatNormalizedUsingSimCountry(number: String): String + fun getCanonicalForEnteredNumber(number: String): String + fun getCanonicalForEnteredNumber(number: String, countryCandidates: List): String + fun countryCandidates(): List +} + +internal class PhoneNumberFormatterImpl @Inject constructor( + private val phoneUtils: PhoneUtils, +) : PhoneNumberFormatter { + + override fun formatForDisplay(number: String): String { + return phoneUtils.formatForDisplay(number) + } + + override fun formatForDisplayUsingSimCountry(number: String): String { + return phoneUtils.formatForDisplayUsingSimCountry(number).orEmpty() + } + + override fun formatNormalizedUsingSimCountry(number: String): String { + return phoneUtils.formatNormalizedDestinationUsingSimCountry(number).orEmpty() + } + + override fun getCanonicalForEnteredNumber(number: String): String { + return phoneUtils.getCanonicalForEnteredPhoneNumber(number) + } + + override fun getCanonicalForEnteredNumber( + number: String, + countryCandidates: List, + ): String { + return phoneUtils.getCanonicalForEnteredPhoneNumber(number, countryCandidates) + } + + override fun countryCandidates(): List { + return phoneUtils.apply { warmUp() }.countryCandidatesForEnteredPhoneNumber + } +} diff --git a/src/com/android/messaging/data/secondaryuser/SecondaryUserMessageResolver.kt b/src/com/android/messaging/data/secondaryuser/SecondaryUserMessageResolver.kt new file mode 100644 index 000000000..b6e8bd618 --- /dev/null +++ b/src/com/android/messaging/data/secondaryuser/SecondaryUserMessageResolver.kt @@ -0,0 +1,41 @@ +package com.android.messaging.data.secondaryuser + +import com.android.messaging.data.phone.formatter.PhoneNumberFormatter +import com.android.messaging.data.secondaryuser.model.SecondaryUserMessageInfo +import javax.inject.Inject + +internal interface SecondaryUserMessageResolver { + fun resolve(address: String?, body: String?): SecondaryUserMessageInfo? +} + +internal class SecondaryUserMessageResolverImpl @Inject constructor( + private val contactNameLookup: SmsContactNameLookup, + private val phoneNumberFormatter: PhoneNumberFormatter, +) : SecondaryUserMessageResolver { + + override fun resolve( + address: String?, + body: String?, + ): SecondaryUserMessageInfo? { + if (body.isNullOrEmpty()) { + return null + } + + return resolveSenderDisplayName(address)?.let { sender -> + SecondaryUserMessageInfo( + sender = sender, + body = body, + ) + } + } + + private fun resolveSenderDisplayName(address: String?): String? { + if (address.isNullOrEmpty()) { + return null + } + + return contactNameLookup.lookup(address) + ?.takeIf(String::isNotEmpty) + ?: phoneNumberFormatter.formatForDisplay(address) + } +} diff --git a/src/com/android/messaging/data/secondaryuser/SecondaryUserNotifier.kt b/src/com/android/messaging/data/secondaryuser/SecondaryUserNotifier.kt new file mode 100644 index 000000000..34fe05e83 --- /dev/null +++ b/src/com/android/messaging/data/secondaryuser/SecondaryUserNotifier.kt @@ -0,0 +1,75 @@ +package com.android.messaging.data.secondaryuser + +import android.content.Context +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import com.android.messaging.R +import com.android.messaging.data.secondaryuser.model.SecondaryUserMessageInfo +import com.android.messaging.ui.UIIntents +import com.android.messaging.util.LogUtil +import com.android.messaging.util.NotificationChannelUtil +import com.android.messaging.util.PendingIntentConstants +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +internal interface SecondaryUserNotifier { + fun notifyIncomingMessage(info: SecondaryUserMessageInfo?) + fun cancel() +} + +internal class SecondaryUserNotifierImpl @Inject constructor( + @param:ApplicationContext + private val context: Context, +) : SecondaryUserNotifier { + + override fun notifyIncomingMessage(info: SecondaryUserMessageInfo?) { + val fallback = context.getString(R.string.secondary_user_new_message_title) + val title = info?.sender ?: fallback + val text = info?.body ?: fallback + val style = NotificationCompat.BigTextStyle().bigText(text) + val pendingIntent = UIIntents.get() + .getPendingIntentForSecondaryUserNewMessageNotification(context) + + val notification = NotificationCompat.Builder( + context, + NotificationChannelUtil.INCOMING_MESSAGES, + ) + .setContentTitle(title) + .setContentText(text) + .setTicker(context.getString(R.string.secondary_user_new_message_ticker)) + .setSmallIcon(R.drawable.ic_sms_light) + .setVisibility(NotificationCompat.VISIBILITY_PRIVATE) + .setContentIntent(pendingIntent) + .setStyle(style) + .build() + + try { + notificationManager().notify( + notificationTag(), + PendingIntentConstants.SMS_SECONDARY_USER_NOTIFICATION_ID, + notification, + ) + } catch (exception: SecurityException) { + LogUtil.e( + LogUtil.BUGLE_TAG, + "Missing permission to post secondary user notification", + exception, + ) + } + } + + override fun cancel() { + notificationManager().cancel( + notificationTag(), + PendingIntentConstants.SMS_SECONDARY_USER_NOTIFICATION_ID, + ) + } + + private fun notificationManager(): NotificationManagerCompat { + return NotificationManagerCompat.from(context) + } + + private fun notificationTag(): String { + return context.packageName + ":secondaryuser" + } +} diff --git a/src/com/android/messaging/data/secondaryuser/SmsContactNameLookup.kt b/src/com/android/messaging/data/secondaryuser/SmsContactNameLookup.kt new file mode 100644 index 000000000..b2db3f065 --- /dev/null +++ b/src/com/android/messaging/data/secondaryuser/SmsContactNameLookup.kt @@ -0,0 +1,27 @@ +package com.android.messaging.data.secondaryuser + +import android.content.Context +import com.android.messaging.util.ContactUtil +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +internal interface SmsContactNameLookup { + fun lookup(address: String): String? +} + +internal class SmsContactNameLookupImpl @Inject constructor( + @param:ApplicationContext + private val context: Context, +) : SmsContactNameLookup { + + override fun lookup(address: String): String? { + return ContactUtil.lookupDestination(context, address) + .performSynchronousQuery() + ?.use { cursor -> + when { + cursor.moveToFirst() -> cursor.getString(ContactUtil.INDEX_DISPLAY_NAME) + else -> null + } + } + } +} diff --git a/src/com/android/messaging/data/secondaryuser/model/SecondaryUserMessageInfo.kt b/src/com/android/messaging/data/secondaryuser/model/SecondaryUserMessageInfo.kt new file mode 100644 index 000000000..259ae44f7 --- /dev/null +++ b/src/com/android/messaging/data/secondaryuser/model/SecondaryUserMessageInfo.kt @@ -0,0 +1,6 @@ +package com.android.messaging.data.secondaryuser.model + +internal data class SecondaryUserMessageInfo( + val sender: String, + val body: String, +) diff --git a/src/com/android/messaging/data/sms/IncomingSmsDeliverer.kt b/src/com/android/messaging/data/sms/IncomingSmsDeliverer.kt new file mode 100644 index 000000000..5e2a5f44e --- /dev/null +++ b/src/com/android/messaging/data/sms/IncomingSmsDeliverer.kt @@ -0,0 +1,84 @@ +package com.android.messaging.data.sms + +import android.content.Context +import android.content.Intent +import android.provider.Telephony.Sms +import android.telephony.SmsMessage +import com.android.messaging.Factory +import com.android.messaging.datamodel.action.ReceiveSmsMessageAction +import com.android.messaging.sms.MmsUtils +import com.android.messaging.util.DebugUtils +import com.android.messaging.util.LogUtil +import com.android.messaging.util.PhoneUtils +import javax.inject.Inject + +internal interface IncomingSmsDeliverer { + fun deliverFromIntent(context: Context, intent: Intent) + fun deliver(context: Context, subId: Int, errorCode: Int, messages: Array) +} + +internal class IncomingSmsDelivererImpl @Inject constructor( + private val parser: IncomingSmsParser, +) : IncomingSmsDeliverer { + + override fun deliverFromIntent( + context: Context, + intent: Intent, + ) { + val messages = parser.parse(intent) + if (messages.isNullOrEmpty()) { + LogUtil.e(LogUtil.BUGLE_TAG, "processReceivedSms: null or zero or ignored message") + return + } + + val errorCode = intent.getIntExtra(EXTRA_ERROR_CODE, NO_ERROR_CODE) + val subId = PhoneUtils.getDefault() + .getEffectiveIncomingSubIdFromSystem(intent, EXTRA_SUB_ID) + deliver( + context = context, + subId = subId, + errorCode = errorCode, + messages = messages, + ) + + if (MmsUtils.isDumpSmsEnabled()) { + val format = intent.getStringExtra(EXTRA_FORMAT) + DebugUtils.dumpSms(messages.first().timestampMillis, messages, format) + } + } + + override fun deliver( + context: Context, + subId: Int, + errorCode: Int, + messages: Array, + ) { + val firstMessage = messages.first() + val messageValues = MmsUtils.parseReceivedSmsMessage(context, messages, errorCode) + val receivedTimestampMs = MmsUtils.getMessageDate(firstMessage, System.currentTimeMillis()) + + // Default to unread and unseen for us but ReceiveSmsMessageAction will override + // seen for the telephony db. + messageValues.put(Sms.Inbox.READ, 0) + messageValues.put(Sms.Inbox.SEEN, 0) + messageValues.put(Sms.Inbox.DATE, receivedTimestampMs) + messageValues.put(Sms.SUBSCRIPTION_ID, subId) + + val isClassZero = firstMessage.messageClass == SmsMessage.MessageClass.CLASS_0 || + DebugUtils.debugClassZeroSmsEnabled() + + when { + isClassZero -> Factory.get().getUIIntents() + .launchClassZeroActivity(context, messageValues) + + else -> ReceiveSmsMessageAction(messageValues).start() + } + } + + private companion object { + private const val EXTRA_ERROR_CODE = "errorCode" + private const val EXTRA_SUB_ID = "subscription" + private const val EXTRA_FORMAT = "format" + private const val NO_ERROR_CODE = -1 + } +} diff --git a/src/com/android/messaging/data/sms/IncomingSmsParser.kt b/src/com/android/messaging/data/sms/IncomingSmsParser.kt new file mode 100644 index 000000000..b5a5ec4d8 --- /dev/null +++ b/src/com/android/messaging/data/sms/IncomingSmsParser.kt @@ -0,0 +1,75 @@ +package com.android.messaging.data.sms + +import android.content.Intent +import android.provider.Telephony.Sms +import android.telephony.SmsMessage +import com.android.messaging.util.BugleGservices +import com.android.messaging.util.BugleGservicesKeys +import com.android.messaging.util.LogUtil +import java.util.regex.Pattern +import java.util.regex.PatternSyntaxException +import javax.inject.Inject + +internal interface IncomingSmsParser { + fun parse(intent: Intent): Array? +} + +internal class IncomingSmsParserImpl @Inject constructor() : IncomingSmsParser { + + @Suppress("TooGenericExceptionCaught") + override fun parse(intent: Intent): Array? { + val messages = Sms.Intents.getMessagesFromIntent(intent) + if (messages.isNullOrEmpty()) { + return null + } + + // Sometimes, SmsMessage.mWrappedSmsMessage is null causing NPE when we access + // the methods on it although the SmsMessage itself is not null. So do this check + // before we do anything on the parsed SmsMessages. + return try { + when { + isIgnored(messages.first().displayMessageBody) -> null + else -> messages + } + } catch (exception: NullPointerException) { + LogUtil.e( + LogUtil.BUGLE_TAG, + "shouldIgnoreMessage: NPE inside SmsMessage", + exception, + ) + null + } + } + + private fun isIgnored(messageBody: String?): Boolean { + if (messageBody == null) { + return false + } + + return ignorePatterns().any { pattern -> + pattern.matcher(messageBody).matches() + } + } + + private fun ignorePatterns(): List { + val smsIgnoreRegex = BugleGservices.get().getString( + BugleGservicesKeys.SMS_IGNORE_MESSAGE_REGEX, + BugleGservicesKeys.SMS_IGNORE_MESSAGE_REGEX_DEFAULT, + ) ?: return emptyList() + + val patterns = mutableListOf() + for (expression in smsIgnoreRegex.split("\n")) { + try { + patterns.add(Pattern.compile(expression)) + } catch (exception: PatternSyntaxException) { + LogUtil.e( + LogUtil.BUGLE_TAG, + "compileIgnoreSmsPatterns: Skipping bad expression: $expression", + exception, + ) + } + } + + return patterns + } +} diff --git a/src/com/android/messaging/data/sms/SmsReceiverToggle.kt b/src/com/android/messaging/data/sms/SmsReceiverToggle.kt new file mode 100644 index 000000000..0485ebca4 --- /dev/null +++ b/src/com/android/messaging/data/sms/SmsReceiverToggle.kt @@ -0,0 +1,44 @@ +package com.android.messaging.data.sms + +import android.content.ComponentName +import android.content.Context +import android.content.pm.PackageManager +import com.android.messaging.util.LogUtil +import com.android.messaging.util.OsUtil +import javax.inject.Inject + +internal interface SmsReceiverToggle { + fun update(context: Context) +} + +internal class SmsReceiverToggleImpl @Inject constructor() : SmsReceiverToggle { + + override fun update(context: Context) { + // When we're running as the secondary user, we don't get the new SMS_DELIVER intent, + // only the primary user receives that. As secondary, we need to go old-school and + // listen for the SMS_RECEIVED intent. For the secondary user, use this SmsReceiver + // for both sms and mms notification. For the primary user we don't use the SmsReceiver. + val enabled = OsUtil.isSecondaryUser() + LogUtil.v( + LogUtil.BUGLE_TAG, + when { + enabled -> "Enabling SMS message receiving" + else -> "Disabling SMS message receiving" + }, + ) + + val state = when { + enabled -> PackageManager.COMPONENT_ENABLED_STATE_ENABLED + else -> PackageManager.COMPONENT_ENABLED_STATE_DISABLED + } + context.packageManager.setComponentEnabledSetting( + ComponentName(context.packageName, SMS_RECEIVER_CLASS), + state, + PackageManager.DONT_KILL_APP, + ) + } + + private companion object { + private const val SMS_RECEIVER_CLASS = "com.android.messaging.receiver.SmsReceiver" + } +} diff --git a/src/com/android/messaging/datamodel/data/ConversationListData.java b/src/com/android/messaging/datamodel/data/ConversationListData.java index 8afcb3a0b..b83be7ab0 100644 --- a/src/com/android/messaging/datamodel/data/ConversationListData.java +++ b/src/com/android/messaging/datamodel/data/ConversationListData.java @@ -24,15 +24,11 @@ import androidx.loader.content.Loader; import com.android.messaging.datamodel.BoundCursorLoader; -import com.android.messaging.datamodel.BugleNotifications; -import com.android.messaging.datamodel.DataModel; import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns; import com.android.messaging.datamodel.MessagingContentProvider; -import com.android.messaging.datamodel.SyncManager; import com.android.messaging.datamodel.binding.BindableData; import com.android.messaging.datamodel.binding.BindingBase; import com.android.messaging.datamodel.data.ConversationListItemData.ConversationListViewColumns; -import com.android.messaging.receiver.SmsReceiver; import com.android.messaging.util.Assert; import com.android.messaging.util.LogUtil; @@ -178,10 +174,6 @@ public void init(final LoaderManager loaderManager, mLoaderManager.initLoader(BLOCKED_PARTICIPANTS_AVAILABLE_LOADER, mArgs, loaderCallbacks); } - public void handleSecondaryUserMessagesSeen() { - SmsReceiver.cancelSecondaryUserNotification(); - } - @Override protected void unregisterListeners() { mListener = null; @@ -194,20 +186,4 @@ protected void unregisterListeners() { } } - public boolean getHasFirstSyncCompleted() { - final SyncManager syncManager = DataModel.get().getSyncManager(); - return syncManager.getHasFirstSyncCompleted(); - } - - public void setScrolledToNewestConversation(boolean scrolledToNewestConversation) { - DataModel.get().setConversationListScrolledToNewestConversation( - scrolledToNewestConversation); - if (scrolledToNewestConversation) { - handleSecondaryUserMessagesSeen(); - } - } - - public HashSet getBlockedParticipants() { - return mBlockedParticipants; - } } diff --git a/src/com/android/messaging/di/core/CoreProvidesModule.kt b/src/com/android/messaging/di/core/CoreProvidesModule.kt index 4c49744d4..e8a3110c7 100644 --- a/src/com/android/messaging/di/core/CoreProvidesModule.kt +++ b/src/com/android/messaging/di/core/CoreProvidesModule.kt @@ -8,6 +8,7 @@ import android.content.pm.PackageManager import android.os.SystemClock import android.telephony.SubscriptionManager import android.telephony.TelephonyManager +import com.android.messaging.util.PhoneUtils import com.android.messaging.util.core.ElapsedRealtimeProvider import dagger.Module import dagger.Provides @@ -129,4 +130,10 @@ internal class CoreProvidesModule { fun provideElapsedRealtimeProvider(): ElapsedRealtimeProvider { return ElapsedRealtimeProvider { SystemClock.elapsedRealtime() } } + + @Provides + @Reusable + fun providePhoneUtils(): PhoneUtils { + return PhoneUtils.getDefault() + } } diff --git a/src/com/android/messaging/di/phone/PhoneBindsModule.kt b/src/com/android/messaging/di/phone/PhoneBindsModule.kt new file mode 100644 index 000000000..9997b9735 --- /dev/null +++ b/src/com/android/messaging/di/phone/PhoneBindsModule.kt @@ -0,0 +1,20 @@ +package com.android.messaging.di.phone + +import com.android.messaging.data.phone.formatter.PhoneNumberFormatter +import com.android.messaging.data.phone.formatter.PhoneNumberFormatterImpl +import dagger.Binds +import dagger.Module +import dagger.Reusable +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal abstract class PhoneBindsModule { + + @Binds + @Reusable + abstract fun bindPhoneNumberFormatter( + impl: PhoneNumberFormatterImpl, + ): PhoneNumberFormatter +} diff --git a/src/com/android/messaging/di/receiver/IncomingSmsEntryPoint.kt b/src/com/android/messaging/di/receiver/IncomingSmsEntryPoint.kt new file mode 100644 index 000000000..5a2f46cda --- /dev/null +++ b/src/com/android/messaging/di/receiver/IncomingSmsEntryPoint.kt @@ -0,0 +1,30 @@ +package com.android.messaging.di.receiver + +import com.android.messaging.data.secondaryuser.SecondaryUserMessageResolver +import com.android.messaging.data.secondaryuser.SecondaryUserNotifier +import com.android.messaging.data.sms.IncomingSmsDeliverer +import com.android.messaging.data.sms.IncomingSmsParser +import com.android.messaging.data.sms.SmsReceiverToggle +import com.android.messaging.di.core.ApplicationCoroutineScope +import com.android.messaging.di.core.IoDispatcher +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope + +@EntryPoint +@InstallIn(SingletonComponent::class) +internal interface IncomingSmsEntryPoint { + fun incomingSmsParser(): IncomingSmsParser + fun incomingSmsDeliverer(): IncomingSmsDeliverer + fun secondaryUserMessageResolver(): SecondaryUserMessageResolver + fun secondaryUserNotifier(): SecondaryUserNotifier + fun smsReceiverToggle(): SmsReceiverToggle + + @ApplicationCoroutineScope + fun applicationScope(): CoroutineScope + + @IoDispatcher + fun ioDispatcher(): CoroutineDispatcher +} diff --git a/src/com/android/messaging/di/secondaryuser/SecondaryUserBindsModule.kt b/src/com/android/messaging/di/secondaryuser/SecondaryUserBindsModule.kt new file mode 100644 index 000000000..5fe979200 --- /dev/null +++ b/src/com/android/messaging/di/secondaryuser/SecondaryUserBindsModule.kt @@ -0,0 +1,36 @@ +package com.android.messaging.di.secondaryuser + +import com.android.messaging.data.secondaryuser.SecondaryUserMessageResolver +import com.android.messaging.data.secondaryuser.SecondaryUserMessageResolverImpl +import com.android.messaging.data.secondaryuser.SecondaryUserNotifier +import com.android.messaging.data.secondaryuser.SecondaryUserNotifierImpl +import com.android.messaging.data.secondaryuser.SmsContactNameLookup +import com.android.messaging.data.secondaryuser.SmsContactNameLookupImpl +import dagger.Binds +import dagger.Module +import dagger.Reusable +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal abstract class SecondaryUserBindsModule { + + @Binds + @Reusable + abstract fun bindSecondaryUserMessageResolver( + impl: SecondaryUserMessageResolverImpl, + ): SecondaryUserMessageResolver + + @Binds + @Reusable + abstract fun bindSecondaryUserNotifier( + impl: SecondaryUserNotifierImpl, + ): SecondaryUserNotifier + + @Binds + @Reusable + abstract fun bindSmsContactNameLookup( + impl: SmsContactNameLookupImpl, + ): SmsContactNameLookup +} diff --git a/src/com/android/messaging/di/sms/SmsBindsModule.kt b/src/com/android/messaging/di/sms/SmsBindsModule.kt new file mode 100644 index 000000000..d27b9eb43 --- /dev/null +++ b/src/com/android/messaging/di/sms/SmsBindsModule.kt @@ -0,0 +1,36 @@ +package com.android.messaging.di.sms + +import com.android.messaging.data.sms.IncomingSmsDeliverer +import com.android.messaging.data.sms.IncomingSmsDelivererImpl +import com.android.messaging.data.sms.IncomingSmsParser +import com.android.messaging.data.sms.IncomingSmsParserImpl +import com.android.messaging.data.sms.SmsReceiverToggle +import com.android.messaging.data.sms.SmsReceiverToggleImpl +import dagger.Binds +import dagger.Module +import dagger.Reusable +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal abstract class SmsBindsModule { + + @Binds + @Reusable + abstract fun bindIncomingSmsParser( + impl: IncomingSmsParserImpl, + ): IncomingSmsParser + + @Binds + @Reusable + abstract fun bindIncomingSmsDeliverer( + impl: IncomingSmsDelivererImpl, + ): IncomingSmsDeliverer + + @Binds + @Reusable + abstract fun bindSmsReceiverToggle( + impl: SmsReceiverToggleImpl, + ): SmsReceiverToggle +} diff --git a/src/com/android/messaging/receiver/BootAndPackageReplacedReceiver.java b/src/com/android/messaging/receiver/BootAndPackageReplacedReceiver.java deleted file mode 100644 index be0d2968a..000000000 --- a/src/com/android/messaging/receiver/BootAndPackageReplacedReceiver.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.messaging.receiver; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -import com.android.messaging.BugleApplication; -import com.android.messaging.Factory; -import com.android.messaging.datamodel.action.UpdateMessageNotificationAction; -import com.android.messaging.util.BuglePrefsKeys; -import com.android.messaging.util.LogUtil; - -/** - * Receives notification of boot completion and package replacement - */ -public class BootAndPackageReplacedReceiver extends BroadcastReceiver { - @Override - public void onReceive(final Context context, final Intent intent) { - if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction()) - || Intent.ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction())) { - // Repost unseen notifications - Factory.get().getApplicationPrefs().putLong( - BuglePrefsKeys.LATEST_NOTIFICATION_MESSAGE_TIMESTAMP, Long.MIN_VALUE); - UpdateMessageNotificationAction.updateMessageNotification(); - - BugleApplication.updateAppConfig(context); - } else { - LogUtil.i(LogUtil.BUGLE_TAG, "BootAndPackageReplacedReceiver got unexpected action: " - + intent.getAction()); - } - } -} - diff --git a/src/com/android/messaging/receiver/BootAndPackageReplacedReceiver.kt b/src/com/android/messaging/receiver/BootAndPackageReplacedReceiver.kt new file mode 100644 index 000000000..b974e58b7 --- /dev/null +++ b/src/com/android/messaging/receiver/BootAndPackageReplacedReceiver.kt @@ -0,0 +1,37 @@ +package com.android.messaging.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import com.android.messaging.BugleApplication +import com.android.messaging.Factory +import com.android.messaging.datamodel.action.UpdateMessageNotificationAction +import com.android.messaging.util.BuglePrefsKeys +import com.android.messaging.util.LogUtil + +class BootAndPackageReplacedReceiver : BroadcastReceiver() { + + override fun onReceive( + context: Context, + intent: Intent, + ) { + if (intent.action != Intent.ACTION_BOOT_COMPLETED && + intent.action != Intent.ACTION_MY_PACKAGE_REPLACED + ) { + LogUtil.i( + LogUtil.BUGLE_TAG, + "BootAndPackageReplacedReceiver got unexpected action: ${intent.action}", + ) + return + } + + // Repost unseen notifications + Factory.get().applicationPrefs.putLong( + BuglePrefsKeys.LATEST_NOTIFICATION_MESSAGE_TIMESTAMP, + Long.MIN_VALUE, + ) + + UpdateMessageNotificationAction.updateMessageNotification() + BugleApplication.updateAppConfig(context) + } +} diff --git a/src/com/android/messaging/receiver/SmsDeliverReceiver.java b/src/com/android/messaging/receiver/SmsDeliverReceiver.java deleted file mode 100644 index 7410a2a0c..000000000 --- a/src/com/android/messaging/receiver/SmsDeliverReceiver.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.messaging.receiver; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.provider.Telephony; - -/** - * Class that receives incoming SMS messages. - */ -public final class SmsDeliverReceiver extends BroadcastReceiver { - @Override - public void onReceive(final Context context, final Intent intent) { - if (Telephony.Sms.Intents.SMS_DELIVER_ACTION.equals(intent.getAction())) { - SmsReceiver.deliverSmsIntent(context, intent); - } - } -} diff --git a/src/com/android/messaging/receiver/SmsDeliverReceiver.kt b/src/com/android/messaging/receiver/SmsDeliverReceiver.kt new file mode 100644 index 000000000..c1aab8190 --- /dev/null +++ b/src/com/android/messaging/receiver/SmsDeliverReceiver.kt @@ -0,0 +1,44 @@ +package com.android.messaging.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.provider.Telephony.Sms +import android.telephony.SmsMessage +import com.android.messaging.data.sms.IncomingSmsDeliverer +import com.android.messaging.di.receiver.IncomingSmsEntryPoint +import dagger.hilt.android.EntryPointAccessors + +class SmsDeliverReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == Sms.Intents.SMS_DELIVER_ACTION) { + delivererFrom(context).deliverFromIntent(context, intent) + } + } + + companion object { + + @JvmStatic + fun deliverSmsMessages( + context: Context, + subId: Int, + errorCode: Int, + messages: Array, + ) { + delivererFrom(context).deliver( + context = context, + subId = subId, + errorCode = errorCode, + messages = messages + ) + } + + private fun delivererFrom(context: Context): IncomingSmsDeliverer { + return EntryPointAccessors.fromApplication( + context.applicationContext, + IncomingSmsEntryPoint::class.java, + ).incomingSmsDeliverer() + } + } +} diff --git a/src/com/android/messaging/receiver/SmsReceiver.java b/src/com/android/messaging/receiver/SmsReceiver.java deleted file mode 100644 index d96407d9b..000000000 --- a/src/com/android/messaging/receiver/SmsReceiver.java +++ /dev/null @@ -1,261 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.messaging.receiver; - -import android.app.Notification; -import android.app.PendingIntent; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.res.Resources; -import android.provider.Telephony; -import android.provider.Telephony.Sms; - -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; - -import com.android.messaging.Factory; -import com.android.messaging.R; -import com.android.messaging.datamodel.action.ReceiveSmsMessageAction; -import com.android.messaging.sms.MmsUtils; -import com.android.messaging.ui.UIIntents; -import com.android.messaging.util.BugleGservices; -import com.android.messaging.util.BugleGservicesKeys; -import com.android.messaging.util.DebugUtils; -import com.android.messaging.util.LogUtil; -import com.android.messaging.util.NotificationChannelUtil; -import com.android.messaging.util.OsUtil; -import com.android.messaging.util.PendingIntentConstants; -import com.android.messaging.util.PhoneUtils; - -import java.util.ArrayList; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; - -/** - * Class that receives incoming SMS messages through android.provider.Telephony.SMS_RECEIVED - * - * This class processes phone verification SMS messages - */ -public final class SmsReceiver extends BroadcastReceiver { - private static final String TAG = LogUtil.BUGLE_TAG; - - private static ArrayList sIgnoreSmsPatterns; - - /** - * Enable or disable the SmsReceiver as appropriate. This receiver is not used when running as the - * primary user and the SmsDeliverReceiver is used for receiving incoming SMS messages. - * When running as a secondary user, this receiver is still used to trigger the incoming - * notification. - */ - public static void updateSmsReceiveHandler(final Context context) { - - // When we're running as the secondary user, we don't get the new SMS_DELIVER intent, - // only the primary user receives that. As secondary, we need to go old-school and - // listen for the SMS_RECEIVED intent. For the secondary user, use this SmsReceiver - // for both sms and mms notification. For the primary user we don't use the SmsReceiver. - boolean smsReceiverEnabled = OsUtil.isSecondaryUser(); - - final PackageManager packageManager = context.getPackageManager(); - final boolean logv = LogUtil.isLoggable(TAG, LogUtil.VERBOSE); - if (smsReceiverEnabled) { - if (logv) { - LogUtil.v(TAG, "Enabling SMS message receiving"); - } - packageManager.setComponentEnabledSetting( - new ComponentName(context, SmsReceiver.class), - PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP); - - } else { - if (logv) { - LogUtil.v(TAG, "Disabling SMS message receiving"); - } - packageManager.setComponentEnabledSetting( - new ComponentName(context, SmsReceiver.class), - PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); - } - } - - private static final String EXTRA_ERROR_CODE = "errorCode"; - private static final String EXTRA_SUB_ID = "subscription"; - - public static void deliverSmsIntent(final Context context, final Intent intent) { - final android.telephony.SmsMessage[] messages = getMessagesFromIntent(intent); - - // Check messages for validity - if (messages == null || messages.length < 1) { - LogUtil.e(TAG, "processReceivedSms: null or zero or ignored message"); - return; - } - - final int errorCode = - intent.getIntExtra(EXTRA_ERROR_CODE, SendStatusReceiver.NO_ERROR_CODE); - // Always convert negative subIds into -1 - int subId = PhoneUtils.getDefault().getEffectiveIncomingSubIdFromSystem( - intent, EXTRA_SUB_ID); - deliverSmsMessages(context, subId, errorCode, messages); - if (MmsUtils.isDumpSmsEnabled()) { - final String format = intent.getStringExtra("format"); - DebugUtils.dumpSms(messages[0].getTimestampMillis(), messages, format); - } - } - - public static void deliverSmsMessages(final Context context, final int subId, - final int errorCode, final android.telephony.SmsMessage[] messages) { - final ContentValues messageValues = - MmsUtils.parseReceivedSmsMessage(context, messages, errorCode); - - LogUtil.v(TAG, "SmsReceiver.deliverSmsMessages"); - - final long nowInMillis = System.currentTimeMillis(); - final long receivedTimestampMs = MmsUtils.getMessageDate(messages[0], nowInMillis); - - messageValues.put(Sms.Inbox.DATE, receivedTimestampMs); - // Default to unread and unseen for us but ReceiveSmsMessageAction will override - // seen for the telephony db. - messageValues.put(Sms.Inbox.READ, 0); - messageValues.put(Sms.Inbox.SEEN, 0); - messageValues.put(Sms.SUBSCRIPTION_ID, subId); - - if (messages[0].getMessageClass() == android.telephony.SmsMessage.MessageClass.CLASS_0 || - DebugUtils.debugClassZeroSmsEnabled()) { - Factory.get().getUIIntents().launchClassZeroActivity(context, messageValues); - } else { - final ReceiveSmsMessageAction action = new ReceiveSmsMessageAction(messageValues); - action.start(); - } - } - - @Override - public void onReceive(final Context context, final Intent intent) { - LogUtil.v(TAG, "SmsReceiver.onReceive " + intent); - // We only take delivery of SMS messages in SmsDeliverReceiver. - if (PhoneUtils.getDefault().isSmsEnabled()) { - final String action = intent.getAction(); - if (OsUtil.isSecondaryUser() && - (Telephony.Sms.Intents.SMS_RECEIVED_ACTION.equals(action) || - // TODO: update this with the actual constant from Telephony - "android.provider.Telephony.MMS_DOWNLOADED".equals(action))) { - postNewMessageSecondaryUserNotification(); - } - } - } - - public static void postNewMessageSecondaryUserNotification() { - final Context context = Factory.get().getApplicationContext(); - final Resources resources = context.getResources(); - final PendingIntent pendingIntent = UIIntents.get() - .getPendingIntentForSecondaryUserNewMessageNotification(context); - - final NotificationCompat.Builder builder = - new NotificationCompat.Builder(context, NotificationChannelUtil.INCOMING_MESSAGES); - builder.setContentTitle(resources.getString(R.string.secondary_user_new_message_title)) - .setTicker(resources.getString(R.string.secondary_user_new_message_ticker)) - .setSmallIcon(R.drawable.ic_sms_light) - // Returning PRIORITY_HIGH causes L to put up a HUD notification. Without it, the ticker - // isn't displayed. - .setPriority(Notification.PRIORITY_HIGH) - .setContentIntent(pendingIntent); - - final NotificationCompat.BigTextStyle bigTextStyle = - new NotificationCompat.BigTextStyle(builder); - bigTextStyle.bigText(resources.getString(R.string.secondary_user_new_message_title)); - final Notification notification = bigTextStyle.build(); - - final NotificationManagerCompat notificationManager = - NotificationManagerCompat.from(Factory.get().getApplicationContext()); - - notificationManager.notify(getNotificationTag(), - PendingIntentConstants.SMS_SECONDARY_USER_NOTIFICATION_ID, notification); - } - - private static String getNotificationTag() { - return Factory.get().getApplicationContext().getPackageName() + ":secondaryuser"; - } - - /** - * Cancel the notification - */ - public static void cancelSecondaryUserNotification() { - final NotificationManagerCompat notificationManager = - NotificationManagerCompat.from(Factory.get().getApplicationContext()); - notificationManager.cancel(getNotificationTag(), - PendingIntentConstants.SMS_SECONDARY_USER_NOTIFICATION_ID); - } - - /** - * Compile all of the patterns we check for to ignore system SMS messages. - */ - private static void compileIgnoreSmsPatterns() { - // Get the pattern set from GServices - final String smsIgnoreRegex = BugleGservices.get().getString( - BugleGservicesKeys.SMS_IGNORE_MESSAGE_REGEX, - BugleGservicesKeys.SMS_IGNORE_MESSAGE_REGEX_DEFAULT); - if (smsIgnoreRegex != null) { - final String[] ignoreSmsExpressions = smsIgnoreRegex.split("\n"); - if (ignoreSmsExpressions.length != 0) { - sIgnoreSmsPatterns = new ArrayList(); - for (int i = 0; i < ignoreSmsExpressions.length; i++) { - try { - sIgnoreSmsPatterns.add(Pattern.compile(ignoreSmsExpressions[i])); - } catch (PatternSyntaxException e) { - LogUtil.e(TAG, "compileIgnoreSmsPatterns: Skipping bad expression: " + - ignoreSmsExpressions[i]); - } - } - } - } - } - - /** - * Get the SMS messages from the specified SMS intent. - * @return the messages. If there is an error or the message should be ignored, return null. - */ - public static android.telephony.SmsMessage[] getMessagesFromIntent(Intent intent) { - final android.telephony.SmsMessage[] messages = Sms.Intents.getMessagesFromIntent(intent); - - // Check messages for validity - if (messages == null || messages.length < 1) { - return null; - } - // Sometimes, SmsMessage.mWrappedSmsMessage is null causing NPE when we access - // the methods on it although the SmsMessage itself is not null. So do this check - // before we do anything on the parsed SmsMessages. - try { - final String messageBody = messages[0].getDisplayMessageBody(); - if (messageBody != null) { - // Compile patterns if necessary - if (sIgnoreSmsPatterns == null) { - compileIgnoreSmsPatterns(); - } - // Check against filters - for (final Pattern pattern : sIgnoreSmsPatterns) { - if (pattern.matcher(messageBody).matches()) { - return null; - } - } - } - } catch (final NullPointerException e) { - LogUtil.e(TAG, "shouldIgnoreMessage: NPE inside SmsMessage"); - return null; - } - return messages; - } -} diff --git a/src/com/android/messaging/receiver/SmsReceiver.kt b/src/com/android/messaging/receiver/SmsReceiver.kt new file mode 100644 index 000000000..3aaf2f6ee --- /dev/null +++ b/src/com/android/messaging/receiver/SmsReceiver.kt @@ -0,0 +1,71 @@ +package com.android.messaging.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.provider.Telephony.Sms +import com.android.messaging.data.secondaryuser.model.SecondaryUserMessageInfo +import com.android.messaging.di.receiver.IncomingSmsEntryPoint +import com.android.messaging.sms.MmsUtils +import com.android.messaging.util.LogUtil +import com.android.messaging.util.OsUtil +import com.android.messaging.util.PhoneUtils +import dagger.hilt.android.EntryPointAccessors +import kotlinx.coroutines.launch + +class SmsReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + LogUtil.v(LogUtil.BUGLE_TAG, "SmsReceiver.onReceive $intent") + + if (!PhoneUtils.getDefault().isSmsEnabled || !OsUtil.isSecondaryUser()) { + return + } + + if (intent.action == Sms.Intents.SMS_RECEIVED_ACTION) { + handleSecondaryUserSmsReceived(context, intent, entryPoint(context)) + } + } + + private fun handleSecondaryUserSmsReceived( + context: Context, + intent: Intent, + entryPoint: IncomingSmsEntryPoint, + ) { + // The contact lookup performs a synchronous query, so resolve off the main thread. + val pendingResult = goAsync() + entryPoint.applicationScope().launch(entryPoint.ioDispatcher()) { + try { + val info = resolveMessage(context, intent, entryPoint) + entryPoint.secondaryUserNotifier().notifyIncomingMessage(info) + } finally { + pendingResult.finish() + } + } + } + + private fun resolveMessage( + context: Context, + intent: Intent, + entryPoint: IncomingSmsEntryPoint, + ): SecondaryUserMessageInfo? { + val messages = entryPoint.incomingSmsParser().parse(intent) ?: return null + val values = MmsUtils.parseReceivedSmsMessage( + context, + messages, + SendStatusReceiver.NO_ERROR_CODE, + ) + + return entryPoint.secondaryUserMessageResolver().resolve( + address = values.getAsString(Sms.ADDRESS), + body = values.getAsString(Sms.BODY), + ) + } + + private fun entryPoint(context: Context): IncomingSmsEntryPoint { + return EntryPointAccessors.fromApplication( + context.applicationContext, + IncomingSmsEntryPoint::class.java, + ) + } +} diff --git a/src/com/android/messaging/ui/conversationpicker/mapper/TargetUiStateMapper.kt b/src/com/android/messaging/ui/conversationpicker/mapper/TargetUiStateMapper.kt index 11386b1fa..7af7884e6 100644 --- a/src/com/android/messaging/ui/conversationpicker/mapper/TargetUiStateMapper.kt +++ b/src/com/android/messaging/ui/conversationpicker/mapper/TargetUiStateMapper.kt @@ -2,10 +2,10 @@ package com.android.messaging.ui.conversationpicker.mapper import com.android.messaging.data.contact.formatter.ContactDestinationFormatter import com.android.messaging.data.conversationpicker.model.TargetConversation +import com.android.messaging.data.phone.formatter.PhoneNumberFormatter import com.android.messaging.domain.conversation.usecase.avatar.ResolveAvatarUri import com.android.messaging.ui.conversationpicker.formatter.targetDetailsTextOrNull import com.android.messaging.ui.conversationpicker.model.TargetUiState -import com.android.messaging.util.PhoneUtils import javax.inject.Inject import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -18,6 +18,7 @@ internal interface TargetUiStateMapper { internal class TargetUiStateMapperImpl @Inject constructor( private val contactDestinationFormatter: ContactDestinationFormatter, + private val phoneNumberFormatter: PhoneNumberFormatter, private val resolveAvatarUri: ResolveAvatarUri, ) : TargetUiStateMapper { @@ -38,7 +39,7 @@ internal class TargetUiStateMapperImpl @Inject constructor( ?.takeUnless { conversation.isGroup } val formattedDestination = otherParticipantDestination - ?.let { PhoneUtils.getDefault().formatForDisplay(it) } + ?.let(phoneNumberFormatter::formatForDisplay) val canonicalDestination = otherParticipantDestination ?.let(contactDestinationFormatter::canonicalize) diff --git a/src/com/android/messaging/ui/recipientselection/delegate/RecipientPickerDelegate.kt b/src/com/android/messaging/ui/recipientselection/delegate/RecipientPickerDelegate.kt index ca50821dd..235dbe89c 100644 --- a/src/com/android/messaging/ui/recipientselection/delegate/RecipientPickerDelegate.kt +++ b/src/com/android/messaging/ui/recipientselection/delegate/RecipientPickerDelegate.kt @@ -5,6 +5,7 @@ import com.android.messaging.data.contact.formatter.ContactDestinationFormatter import com.android.messaging.data.contact.model.Contact import com.android.messaging.data.contact.model.ContactsPage import com.android.messaging.data.contact.repository.ContactsRepository +import com.android.messaging.data.phone.formatter.PhoneNumberFormatter import com.android.messaging.di.core.DefaultDispatcher import com.android.messaging.domain.contacts.usecase.IsReadContactsPermissionGranted import com.android.messaging.ui.contact.mapper.ContactUiModelMapper @@ -53,6 +54,7 @@ internal interface RecipientPickerDelegate { internal class RecipientPickerDelegateImpl @Inject constructor( private val contactDestinationFormatter: ContactDestinationFormatter, + private val phoneNumberFormatter: PhoneNumberFormatter, private val contactUiModelMapper: ContactUiModelMapper, private val contactsRepository: ContactsRepository, private val isReadContactsPermissionGranted: IsReadContactsPermissionGranted, @@ -470,7 +472,7 @@ internal class RecipientPickerDelegateImpl @Inject constructor( return when { candidate.isExcludedBy(excludedDestinations) -> null isAlreadyAContactDestination -> null - else -> candidate.toListItem() + else -> candidate.toListItem(phoneNumberFormatter) } } @@ -564,11 +566,11 @@ internal class RecipientPickerDelegateImpl @Inject constructor( return destinationIdentity.isExcludedBy(excludedDestinations = excludedDestinations) } - fun toListItem(): RecipientPickerListItem.SyntheticPhone { - val phoneUtils = PhoneUtils.getDefault() - val displayName = phoneUtils.formatForDisplayUsingSimCountry(rawQuery).orEmpty() - val normalizedForDisplay = - phoneUtils.formatNormalizedDestinationUsingSimCountry(rawQuery).orEmpty() + fun toListItem( + phoneNumberFormatter: PhoneNumberFormatter, + ): RecipientPickerListItem.SyntheticPhone { + val displayName = phoneNumberFormatter.formatForDisplayUsingSimCountry(rawQuery) + val secondaryText = phoneNumberFormatter.formatNormalizedUsingSimCountry(rawQuery) return RecipientPickerListItem.SyntheticPhone( id = "$SYNTHETIC_RECIPIENT_ID_PREFIX$rawQuery", @@ -576,7 +578,7 @@ internal class RecipientPickerDelegateImpl @Inject constructor( destination = rawQuery, normalizedDestination = destinationIdentity.normalizedDestination, displayName = displayName, - secondaryText = normalizedForDisplay, + secondaryText = secondaryText, ) } } diff --git a/src/com/android/messaging/util/DebugUtils.java b/src/com/android/messaging/util/DebugUtils.java index 8c643fc67..373060989 100644 --- a/src/com/android/messaging/util/DebugUtils.java +++ b/src/com/android/messaging/util/DebugUtils.java @@ -43,7 +43,7 @@ import com.android.messaging.debug.DebugSimEmulationMode; import com.android.messaging.debug.DebugSimEmulationStore; import com.android.messaging.debug.TestDataSeeder; -import com.android.messaging.receiver.SmsReceiver; +import com.android.messaging.receiver.SmsDeliverReceiver; import com.android.messaging.sms.MmsUtils; import com.android.messaging.ui.UIIntents; @@ -320,7 +320,7 @@ private void receiveFromDumpFile(final String dumpFileName) { if (dumpFileName.startsWith(MmsUtils.SMS_DUMP_PREFIX)) { final SmsMessage[] messages = DebugUtils.retrieveSmsFromDumpFile(dumpFileName); if (messages != null) { - SmsReceiver.deliverSmsMessages(mHost, ParticipantData.DEFAULT_SELF_SUB_ID, + SmsDeliverReceiver.deliverSmsMessages(mHost, ParticipantData.DEFAULT_SELF_SUB_ID, 0, messages); } else { LogUtil.e(LogUtil.BUGLE_TAG, From 4845c7e313f1f0319a6395d9000497c69571a641 Mon Sep 17 00:00:00 2001 From: Matvei Plokhov Date: Mon, 13 Jul 2026 22:35:22 +0200 Subject: [PATCH 2/4] Generate unique secondary user notification IDs --- .../usecase/GenerateNotificationIdImplTest.kt | 21 ++++++++++ .../secondaryuser/SecondaryUserNotifier.kt | 30 +++++++++------ .../messaging/di/core/CoreProvidesModule.kt | 16 ++++++-- .../notification/NotificationBindsModule.kt | 9 +++++ .../usecase/GenerateNotificationId.kt | 38 +++++++++++++++++++ 5 files changed, 99 insertions(+), 15 deletions(-) create mode 100644 app/src/test/kotlin/com/android/messaging/domain/notification/usecase/GenerateNotificationIdImplTest.kt create mode 100644 src/com/android/messaging/domain/notification/usecase/GenerateNotificationId.kt diff --git a/app/src/test/kotlin/com/android/messaging/domain/notification/usecase/GenerateNotificationIdImplTest.kt b/app/src/test/kotlin/com/android/messaging/domain/notification/usecase/GenerateNotificationIdImplTest.kt new file mode 100644 index 000000000..a7bf54315 --- /dev/null +++ b/app/src/test/kotlin/com/android/messaging/domain/notification/usecase/GenerateNotificationIdImplTest.kt @@ -0,0 +1,21 @@ +package com.android.messaging.domain.notification.usecase + +import org.junit.Assert.assertNotEquals +import org.junit.Test + +internal class GenerateNotificationIdImplTest { + + @Test + fun invoke_sameElapsedRealtime_returnsDifferentIds() { + val generateNotificationId = GenerateNotificationIdImpl { ELAPSED_REALTIME_MILLIS } + + val firstId = generateNotificationId() + val secondId = generateNotificationId() + + assertNotEquals(firstId, secondId) + } + + private companion object { + private const val ELAPSED_REALTIME_MILLIS = 123_456L + } +} diff --git a/src/com/android/messaging/data/secondaryuser/SecondaryUserNotifier.kt b/src/com/android/messaging/data/secondaryuser/SecondaryUserNotifier.kt index 34fe05e83..77630db50 100644 --- a/src/com/android/messaging/data/secondaryuser/SecondaryUserNotifier.kt +++ b/src/com/android/messaging/data/secondaryuser/SecondaryUserNotifier.kt @@ -1,14 +1,14 @@ package com.android.messaging.data.secondaryuser +import android.app.NotificationManager import android.content.Context import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat import com.android.messaging.R import com.android.messaging.data.secondaryuser.model.SecondaryUserMessageInfo +import com.android.messaging.domain.notification.usecase.GenerateNotificationId import com.android.messaging.ui.UIIntents import com.android.messaging.util.LogUtil import com.android.messaging.util.NotificationChannelUtil -import com.android.messaging.util.PendingIntentConstants import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject @@ -20,6 +20,8 @@ internal interface SecondaryUserNotifier { internal class SecondaryUserNotifierImpl @Inject constructor( @param:ApplicationContext private val context: Context, + private val notificationManager: NotificationManager, + private val generateNotificationId: GenerateNotificationId, ) : SecondaryUserNotifier { override fun notifyIncomingMessage(info: SecondaryUserMessageInfo?) { @@ -30,6 +32,7 @@ internal class SecondaryUserNotifierImpl @Inject constructor( val pendingIntent = UIIntents.get() .getPendingIntentForSecondaryUserNewMessageNotification(context) + val notificationId = generateNotificationId() val notification = NotificationCompat.Builder( context, NotificationChannelUtil.INCOMING_MESSAGES, @@ -44,9 +47,9 @@ internal class SecondaryUserNotifierImpl @Inject constructor( .build() try { - notificationManager().notify( + notificationManager.notify( notificationTag(), - PendingIntentConstants.SMS_SECONDARY_USER_NOTIFICATION_ID, + notificationId, notification, ) } catch (exception: SecurityException) { @@ -59,14 +62,17 @@ internal class SecondaryUserNotifierImpl @Inject constructor( } override fun cancel() { - notificationManager().cancel( - notificationTag(), - PendingIntentConstants.SMS_SECONDARY_USER_NOTIFICATION_ID, - ) - } - - private fun notificationManager(): NotificationManagerCompat { - return NotificationManagerCompat.from(context) + val tag = notificationTag() + notificationManager.activeNotifications + .filter { notification -> + notification.tag == tag + } + .forEach { notification -> + notificationManager.cancel( + notification.tag, + notification.id, + ) + } } private fun notificationTag(): String { diff --git a/src/com/android/messaging/di/core/CoreProvidesModule.kt b/src/com/android/messaging/di/core/CoreProvidesModule.kt index e8a3110c7..a9ef9edbc 100644 --- a/src/com/android/messaging/di/core/CoreProvidesModule.kt +++ b/src/com/android/messaging/di/core/CoreProvidesModule.kt @@ -1,5 +1,6 @@ package com.android.messaging.di.core +import android.app.NotificationManager import android.app.role.RoleManager import android.content.ClipboardManager import android.content.ContentResolver @@ -80,6 +81,15 @@ internal class CoreProvidesModule { return context.contentResolver } + @Provides + @Reusable + fun providePackageManager( + @ApplicationContext + context: Context, + ): PackageManager { + return context.packageManager + } + @Provides @Reusable fun provideClipboardManager( @@ -91,11 +101,11 @@ internal class CoreProvidesModule { @Provides @Reusable - fun providePackageManager( + fun provideNotificationManager( @ApplicationContext context: Context, - ): PackageManager { - return context.packageManager + ): NotificationManager { + return context.getSystemService(NotificationManager::class.java) } @Provides diff --git a/src/com/android/messaging/di/notification/NotificationBindsModule.kt b/src/com/android/messaging/di/notification/NotificationBindsModule.kt index 6fba1ae9b..d32faab98 100644 --- a/src/com/android/messaging/di/notification/NotificationBindsModule.kt +++ b/src/com/android/messaging/di/notification/NotificationBindsModule.kt @@ -1,5 +1,7 @@ package com.android.messaging.di.notification +import com.android.messaging.domain.notification.usecase.GenerateNotificationId +import com.android.messaging.domain.notification.usecase.GenerateNotificationIdImpl import com.android.messaging.domain.notification.usecase.MigrateConversationNotificationChannels import com.android.messaging.domain.notification.usecase.MigrateConversationNotificationChannelsImpl import dagger.Binds @@ -7,11 +9,18 @@ import dagger.Module import dagger.Reusable import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) internal abstract class NotificationBindsModule { + @Binds + @Singleton + abstract fun bindGenerateNotificationId( + impl: GenerateNotificationIdImpl, + ): GenerateNotificationId + @Binds @Reusable abstract fun bindMigrateConversationNotificationChannels( diff --git a/src/com/android/messaging/domain/notification/usecase/GenerateNotificationId.kt b/src/com/android/messaging/domain/notification/usecase/GenerateNotificationId.kt new file mode 100644 index 000000000..fe52663ad --- /dev/null +++ b/src/com/android/messaging/domain/notification/usecase/GenerateNotificationId.kt @@ -0,0 +1,38 @@ +package com.android.messaging.domain.notification.usecase + +import com.android.messaging.util.core.ElapsedRealtimeProvider +import java.util.concurrent.atomic.AtomicInteger +import javax.inject.Inject + +internal fun interface GenerateNotificationId { + operator fun invoke(): Int +} + +internal class GenerateNotificationIdImpl @Inject constructor( + private val elapsedRealtimeProvider: ElapsedRealtimeProvider, +) : GenerateNotificationId { + + private val sequence = AtomicInteger() + + override operator fun invoke(): Int { + val elapsedRealtimeMillis = elapsedRealtimeProvider.elapsedRealtimeMillis() + val sequenceValue = sequence.incrementAndGet() + + return hashNotificationSeed( + elapsedRealtimeMillis = elapsedRealtimeMillis, + sequenceValue = sequenceValue, + ) + } + + private fun hashNotificationSeed( + elapsedRealtimeMillis: Long, + sequenceValue: Int, + ): Int { + val elapsedRealtimeHash = elapsedRealtimeMillis.hashCode() + return HASH_MULTIPLIER * elapsedRealtimeHash + sequenceValue + } + + private companion object { + private const val HASH_MULTIPLIER = 31 + } +} From f9f3d8c3fe2bab8d8912c93e01bb6ddf8c855831 Mon Sep 17 00:00:00 2001 From: Matvei Plokhov Date: Mon, 13 Jul 2026 22:54:50 +0200 Subject: [PATCH 3/4] Add receiver and formatter tests --- .../ContactDestinationFormatterImplTest.kt | 72 ++++++++++++++++++ .../formatter/PhoneNumberFormatterImplTest.kt | 75 +++++++++++++++++++ .../SecondaryUserMessageResolverTest.kt | 32 +++++++- .../data/sms/SmsReceiverToggleImplTest.kt | 74 ++++++++++++++++++ 4 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 app/src/test/kotlin/com/android/messaging/data/contact/formatter/ContactDestinationFormatterImplTest.kt create mode 100644 app/src/test/kotlin/com/android/messaging/data/phone/formatter/PhoneNumberFormatterImplTest.kt create mode 100644 app/src/test/kotlin/com/android/messaging/data/sms/SmsReceiverToggleImplTest.kt diff --git a/app/src/test/kotlin/com/android/messaging/data/contact/formatter/ContactDestinationFormatterImplTest.kt b/app/src/test/kotlin/com/android/messaging/data/contact/formatter/ContactDestinationFormatterImplTest.kt new file mode 100644 index 000000000..deaa7b7e2 --- /dev/null +++ b/app/src/test/kotlin/com/android/messaging/data/contact/formatter/ContactDestinationFormatterImplTest.kt @@ -0,0 +1,72 @@ +package com.android.messaging.data.contact.formatter + +import com.android.messaging.data.phone.formatter.PhoneNumberFormatter +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +internal class ContactDestinationFormatterImplTest { + + private val phoneNumberFormatter = mockk() + private val formatter = ContactDestinationFormatterImpl( + phoneNumberFormatter = phoneNumberFormatter, + ) + + @Test + fun canonicalize_whenValueIsBlank_returnsTrimmedBlank() { + assertEquals("", formatter.canonicalize(" ")) + verify(exactly = 0) { + phoneNumberFormatter.getCanonicalForEnteredNumber(any()) + } + } + + @Test + fun canonicalize_whenValueIsEmail_trimsAndLowercasesEmail() { + assertEquals("alice@example.com", formatter.canonicalize(" Alice@Example.COM ")) + verify(exactly = 0) { + phoneNumberFormatter.getCanonicalForEnteredNumber(any()) + } + } + + @Test + fun canonicalize_whenValueIsPhone_trimsCanonicalizesAndStripsSeparators() { + every { + phoneNumberFormatter.getCanonicalForEnteredNumber("+1 (555) 123-4567") + } returns "+1 (555) 123-4567" + + assertEquals("+15551234567", formatter.canonicalize(" +1 (555) 123-4567 ")) + } + + @Test + fun canonicalize_withCountryCandidates_passesCandidatesToPhoneNumberFormatter() { + val countryCandidates = listOf("US", "CA") + every { + phoneNumberFormatter.getCanonicalForEnteredNumber("555.123/4567", countryCandidates) + } returns "555.123/4567" + + assertEquals( + "5551234567", + formatter.canonicalize("555.123/4567", countryCandidates), + ) + } + + @Test + fun formatPhoneForDisplay_delegatesToPhoneNumberFormatter() { + every { phoneNumberFormatter.formatForDisplay("+15551234") } returns "+1 555-1234" + + assertEquals("+1 555-1234", formatter.formatPhoneForDisplay("+15551234")) + } + + @Test + fun countryCandidates_delegatesToPhoneNumberFormatter() { + val countryCandidates = listOf("US", "CA") + every { phoneNumberFormatter.countryCandidates() } returns countryCandidates + + assertEquals(countryCandidates, formatter.countryCandidates()) + } +} diff --git a/app/src/test/kotlin/com/android/messaging/data/phone/formatter/PhoneNumberFormatterImplTest.kt b/app/src/test/kotlin/com/android/messaging/data/phone/formatter/PhoneNumberFormatterImplTest.kt new file mode 100644 index 000000000..ee720cd7b --- /dev/null +++ b/app/src/test/kotlin/com/android/messaging/data/phone/formatter/PhoneNumberFormatterImplTest.kt @@ -0,0 +1,75 @@ +package com.android.messaging.data.phone.formatter + +import com.android.messaging.util.PhoneUtils +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Test + +internal class PhoneNumberFormatterImplTest { + + private val phoneUtils = mockk() + private val formatter = PhoneNumberFormatterImpl(phoneUtils = phoneUtils) + + @Test + fun formatForDisplay_delegatesToPhoneUtils() { + every { phoneUtils.formatForDisplay(PHONE_NUMBER) } returns FORMATTED_PHONE_NUMBER + + assertEquals(FORMATTED_PHONE_NUMBER, formatter.formatForDisplay(PHONE_NUMBER)) + } + + @Test + fun formatForDisplayUsingSimCountry_whenPhoneUtilsReturnsNull_returnsEmptyString() { + every { phoneUtils.formatForDisplayUsingSimCountry(PHONE_NUMBER) } returns null + + assertEquals("", formatter.formatForDisplayUsingSimCountry(PHONE_NUMBER)) + } + + @Test + fun formatNormalizedUsingSimCountry_whenPhoneUtilsReturnsNull_returnsEmptyString() { + every { phoneUtils.formatNormalizedDestinationUsingSimCountry(PHONE_NUMBER) } returns null + + assertEquals("", formatter.formatNormalizedUsingSimCountry(PHONE_NUMBER)) + } + + @Test + fun getCanonicalForEnteredNumber_delegatesToPhoneUtils() { + every { phoneUtils.getCanonicalForEnteredPhoneNumber(PHONE_NUMBER) } returns + CANONICAL_NUMBER + + assertEquals(CANONICAL_NUMBER, formatter.getCanonicalForEnteredNumber(PHONE_NUMBER)) + } + + @Test + fun getCanonicalForEnteredNumber_withCountryCandidates_delegatesToPhoneUtils() { + val countryCandidates = listOf("US", "CA") + every { + phoneUtils.getCanonicalForEnteredPhoneNumber(PHONE_NUMBER, countryCandidates) + } returns CANONICAL_NUMBER + + assertEquals( + CANONICAL_NUMBER, + formatter.getCanonicalForEnteredNumber(PHONE_NUMBER, countryCandidates), + ) + } + + @Test + fun countryCandidates_warmsUpPhoneUtilsBeforeReadingCandidates() { + val countryCandidates = listOf("US", "CA") + every { phoneUtils.warmUp() } returns Unit + every { phoneUtils.countryCandidatesForEnteredPhoneNumber } returns countryCandidates + + assertEquals(countryCandidates, formatter.countryCandidates()) + verify { + phoneUtils.warmUp() + phoneUtils.countryCandidatesForEnteredPhoneNumber + } + } + + private companion object { + private const val PHONE_NUMBER = "+15551234" + private const val FORMATTED_PHONE_NUMBER = "+1 555-1234" + private const val CANONICAL_NUMBER = "+15551234" + } +} diff --git a/app/src/test/kotlin/com/android/messaging/data/secondaryuser/SecondaryUserMessageResolverTest.kt b/app/src/test/kotlin/com/android/messaging/data/secondaryuser/SecondaryUserMessageResolverTest.kt index fb23e191f..d17c70278 100644 --- a/app/src/test/kotlin/com/android/messaging/data/secondaryuser/SecondaryUserMessageResolverTest.kt +++ b/app/src/test/kotlin/com/android/messaging/data/secondaryuser/SecondaryUserMessageResolverTest.kt @@ -4,6 +4,7 @@ import com.android.messaging.data.phone.formatter.PhoneNumberFormatter import com.android.messaging.data.secondaryuser.model.SecondaryUserMessageInfo import io.mockk.every import io.mockk.mockk +import io.mockk.verify import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Test @@ -27,6 +28,9 @@ internal class SecondaryUserMessageResolverTest { SecondaryUserMessageInfo(sender = "Alice", body = "Hello there"), info, ) + verify(exactly = 0) { + phoneNumberFormatter.formatForDisplay(any()) + } } @Test @@ -47,26 +51,52 @@ internal class SecondaryUserMessageResolverTest { every { phoneNumberFormatter.formatForDisplay("+15551234") } returns "+1 555-1234" val info = resolver.resolve(address = "+15551234", body = "Hello there") - assertEquals("+1 555-1234", info?.sender) + assertEquals( + SecondaryUserMessageInfo(sender = "+1 555-1234", body = "Hello there"), + info, + ) + } + + @Test + fun resolve_shortAddress_usesFormattedAddress() { + every { contactNameLookup.lookup("123") } returns null + every { phoneNumberFormatter.formatForDisplay("123") } returns "123" + + val info = resolver.resolve(address = "123", body = "Hello there") + assertEquals( + SecondaryUserMessageInfo(sender = "123", body = "Hello there"), + info, + ) } @Test fun resolve_nullBody_returnsNull() { assertNull(resolver.resolve(address = "+15551234", body = null)) + verifyNoSenderResolution() } @Test fun resolve_emptyBody_returnsNull() { assertNull(resolver.resolve(address = "+15551234", body = "")) + verifyNoSenderResolution() } @Test fun resolve_nullAddress_returnsNull() { assertNull(resolver.resolve(address = null, body = "Hello there")) + verifyNoSenderResolution() } @Test fun resolve_emptyAddress_returnsNull() { assertNull(resolver.resolve(address = "", body = "Hello there")) + verifyNoSenderResolution() + } + + private fun verifyNoSenderResolution() { + verify(exactly = 0) { + contactNameLookup.lookup(any()) + phoneNumberFormatter.formatForDisplay(any()) + } } } diff --git a/app/src/test/kotlin/com/android/messaging/data/sms/SmsReceiverToggleImplTest.kt b/app/src/test/kotlin/com/android/messaging/data/sms/SmsReceiverToggleImplTest.kt new file mode 100644 index 000000000..548aab9ed --- /dev/null +++ b/app/src/test/kotlin/com/android/messaging/data/sms/SmsReceiverToggleImplTest.kt @@ -0,0 +1,74 @@ +package com.android.messaging.data.sms + +import android.content.Context +import android.content.pm.PackageManager +import com.android.messaging.util.OsUtil +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import io.mockk.verify +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +internal class SmsReceiverToggleImplTest { + + private lateinit var packageManager: PackageManager + private lateinit var context: Context + + private val toggle = SmsReceiverToggleImpl() + + @Before + fun setUp() { + packageManager = mockk(relaxed = true) + context = mockk(relaxed = true) + every { context.packageName } returns PACKAGE_NAME + every { context.packageManager } returns packageManager + mockkStatic(OsUtil::class) + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun update_whenSecondaryUser_enablesSmsReceiver() { + every { OsUtil.isSecondaryUser() } returns true + + toggle.update(context) + + verifyReceiverState(PackageManager.COMPONENT_ENABLED_STATE_ENABLED) + } + + @Test + fun update_whenPrimaryUser_disablesSmsReceiver() { + every { OsUtil.isSecondaryUser() } returns false + + toggle.update(context) + + verifyReceiverState(PackageManager.COMPONENT_ENABLED_STATE_DISABLED) + } + + private fun verifyReceiverState(expectedState: Int) { + verify { + packageManager.setComponentEnabledSetting( + match { componentName -> + componentName.packageName == PACKAGE_NAME && + componentName.className == SMS_RECEIVER_CLASS + }, + expectedState, + PackageManager.DONT_KILL_APP, + ) + } + } + + private companion object { + private const val PACKAGE_NAME = "com.android.messaging" + private const val SMS_RECEIVER_CLASS = "com.android.messaging.receiver.SmsReceiver" + } +} From d420827de0a84d0aedabf672dd4b3d66c98f0876 Mon Sep 17 00:00:00 2001 From: Matvei Plokhov Date: Tue, 14 Jul 2026 22:26:06 +0200 Subject: [PATCH 4/4] Fix incoming SMS not imported until the app is opened --- .../data/sms/IncomingSmsDelivererImplTest.kt | 88 +++++++++++++++++++ .../data/sms/IncomingSmsDeliverer.kt | 28 +++++- .../messaging/datamodel/DataModel.java | 5 ++ .../datamodel/action/ActionService.java | 7 ++ .../datamodel/action/ActionServiceImpl.java | 20 +++++ .../messaging/receiver/SmsDeliverReceiver.kt | 26 ++++-- 6 files changed, 165 insertions(+), 9 deletions(-) create mode 100644 app/src/test/kotlin/com/android/messaging/data/sms/IncomingSmsDelivererImplTest.kt diff --git a/app/src/test/kotlin/com/android/messaging/data/sms/IncomingSmsDelivererImplTest.kt b/app/src/test/kotlin/com/android/messaging/data/sms/IncomingSmsDelivererImplTest.kt new file mode 100644 index 000000000..b07cf98f0 --- /dev/null +++ b/app/src/test/kotlin/com/android/messaging/data/sms/IncomingSmsDelivererImplTest.kt @@ -0,0 +1,88 @@ +package com.android.messaging.data.sms + +import android.content.ContentValues +import android.content.Context +import android.content.Intent +import android.telephony.SmsMessage +import com.android.messaging.datamodel.DataModel +import com.android.messaging.datamodel.action.ReceiveSmsMessageAction +import com.android.messaging.sms.MmsUtils +import com.android.messaging.util.DebugUtils +import com.android.messaging.util.PhoneUtils +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import io.mockk.verify +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +internal class IncomingSmsDelivererImplTest { + + private val context: Context = mockk(relaxed = true) + private val intent: Intent = mockk(relaxed = true) + private val parser: IncomingSmsParser = mockk() + + // DCS 0 → message class UNKNOWN (not class 0), so it routes to ReceiveSmsMessageAction. + private val message: SmsMessage = SmsMessage.createFromPdu(SMS_DELIVER_PDU, SMS_FORMAT_3GPP) + private val deliverer = IncomingSmsDelivererImpl(parser) + + @Before + fun setUp() { + mockkStatic(MmsUtils::class, DebugUtils::class, PhoneUtils::class, DataModel::class) + every { MmsUtils.parseReceivedSmsMessage(any(), any(), any()) } returns ContentValues() + every { MmsUtils.getMessageDate(any(), any()) } returns RECEIVED_AT + every { MmsUtils.isDumpSmsEnabled() } returns false + every { DebugUtils.debugClassZeroSmsEnabled() } returns false + every { DataModel.executeActionImmediately(any()) } just Runs + every { DataModel.startActionService(any()) } just Runs + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun deliverFromIntent_executesReceiveActionImmediately() { + every { parser.parse(intent) } returns arrayOf(message) + every { PhoneUtils.getDefault() } returns mockk { + every { getEffectiveIncomingSubIdFromSystem(any(), any()) } returns SUB_ID + } + + deliverer.deliverFromIntent(context, intent) + + verify { DataModel.executeActionImmediately(any()) } + verify(exactly = 0) { DataModel.startActionService(any()) } + } + + @Test + fun deliver_startsReceiveActionAsynchronously() { + deliverer.deliver(context, SUB_ID, NO_ERROR_CODE, arrayOf(message)) + + verify { DataModel.startActionService(any()) } + verify(exactly = 0) { DataModel.executeActionImmediately(any()) } + } + + private companion object { + private const val SUB_ID = 1 + private const val NO_ERROR_CODE = -1 + private const val RECEIVED_AT = 1_000L + private const val SMS_FORMAT_3GPP = "3gpp" + + // A valid GSM SMS-DELIVER PDU ("hellohello"). + private const val SMS_DELIVER_PDU_HEX = + "07911326040000F0040B911346610089F60000208062917314080CC8F71D14969741F977FD07" + + private val SMS_DELIVER_PDU: ByteArray = + ByteArray(SMS_DELIVER_PDU_HEX.length / 2) { index -> + SMS_DELIVER_PDU_HEX.substring(index * 2, index * 2 + 2).toInt(16).toByte() + } + } +} diff --git a/src/com/android/messaging/data/sms/IncomingSmsDeliverer.kt b/src/com/android/messaging/data/sms/IncomingSmsDeliverer.kt index 5e2a5f44e..5cf8e6117 100644 --- a/src/com/android/messaging/data/sms/IncomingSmsDeliverer.kt +++ b/src/com/android/messaging/data/sms/IncomingSmsDeliverer.kt @@ -5,6 +5,7 @@ import android.content.Intent import android.provider.Telephony.Sms import android.telephony.SmsMessage import com.android.messaging.Factory +import com.android.messaging.datamodel.DataModel import com.android.messaging.datamodel.action.ReceiveSmsMessageAction import com.android.messaging.sms.MmsUtils import com.android.messaging.util.DebugUtils @@ -34,11 +35,12 @@ internal class IncomingSmsDelivererImpl @Inject constructor( val errorCode = intent.getIntExtra(EXTRA_ERROR_CODE, NO_ERROR_CODE) val subId = PhoneUtils.getDefault() .getEffectiveIncomingSubIdFromSystem(intent, EXTRA_SUB_ID) - deliver( + deliverInternal( context = context, subId = subId, errorCode = errorCode, messages = messages, + executeImmediately = true, ) if (MmsUtils.isDumpSmsEnabled()) { @@ -52,6 +54,22 @@ internal class IncomingSmsDelivererImpl @Inject constructor( subId: Int, errorCode: Int, messages: Array, + ) { + deliverInternal( + context = context, + subId = subId, + errorCode = errorCode, + messages = messages, + executeImmediately = false, + ) + } + + private fun deliverInternal( + context: Context, + subId: Int, + errorCode: Int, + messages: Array, + executeImmediately: Boolean, ) { val firstMessage = messages.first() val messageValues = MmsUtils.parseReceivedSmsMessage(context, messages, errorCode) @@ -71,7 +89,13 @@ internal class IncomingSmsDelivererImpl @Inject constructor( isClassZero -> Factory.get().getUIIntents() .launchClassZeroActivity(context, messageValues) - else -> ReceiveSmsMessageAction(messageValues).start() + else -> { + val action = ReceiveSmsMessageAction(messageValues) + when { + executeImmediately -> DataModel.executeActionImmediately(action) + else -> action.start() + } + } } } diff --git a/src/com/android/messaging/datamodel/DataModel.java b/src/com/android/messaging/datamodel/DataModel.java index 38f4a9168..2f459531d 100644 --- a/src/com/android/messaging/datamodel/DataModel.java +++ b/src/com/android/messaging/datamodel/DataModel.java @@ -52,6 +52,11 @@ public static final void startActionService(final Action action) { get().getActionService().startAction(action); } + @DoesNotRunOnMainThread + public static final void executeActionImmediately(final Action action) { + get().getActionService().executeActionImmediately(action); + } + public static final void scheduleAction(final Action action, final int code, final long delayMs) { get().getActionService().scheduleAction(action, code, delayMs); diff --git a/src/com/android/messaging/datamodel/action/ActionService.java b/src/com/android/messaging/datamodel/action/ActionService.java index 29225fa70..827bbb46c 100644 --- a/src/com/android/messaging/datamodel/action/ActionService.java +++ b/src/com/android/messaging/datamodel/action/ActionService.java @@ -37,6 +37,13 @@ public void startAction(final Action action) { ActionServiceImpl.startAction(action); } + /** + * Execute an action synchronously instead of posting it over the ActionService + */ + public void executeActionImmediately(final Action action) { + ActionServiceImpl.executeActionImmediately(action); + } + /** * Schedule a delayed action by posting it over the the ActionService */ diff --git a/src/com/android/messaging/datamodel/action/ActionServiceImpl.java b/src/com/android/messaging/datamodel/action/ActionServiceImpl.java index 8c8aad108..e06b6182f 100644 --- a/src/com/android/messaging/datamodel/action/ActionServiceImpl.java +++ b/src/com/android/messaging/datamodel/action/ActionServiceImpl.java @@ -77,6 +77,26 @@ protected static void scheduleAction(final Action action, final int requestCode, PendingActionReceiver.scheduleAlarm(intent, requestCode, delayMs); } + /** + * Execute action synchronously on the calling thread instead of queueing it on the service + * @param action - action to execute + */ + public static void executeActionImmediately(final Action action) { + final LoggingTimer timer = + createLoggingTimer(action, "#executeActionImmediately"); + final BackgroundWorker worker = + DataModel.get().getBackgroundWorkerForActionService(); + action.markStart(); + action.markBeginExecute(); + + timer.start(); + final Object result = action.executeAction(); + timer.stopAndLog(); + + action.markEndExecute(result); + action.sendBackgroundActions(worker); + } + /** * Handle response returned by BackgroundWorker * @param request - request generating response diff --git a/src/com/android/messaging/receiver/SmsDeliverReceiver.kt b/src/com/android/messaging/receiver/SmsDeliverReceiver.kt index c1aab8190..5303ab89c 100644 --- a/src/com/android/messaging/receiver/SmsDeliverReceiver.kt +++ b/src/com/android/messaging/receiver/SmsDeliverReceiver.kt @@ -5,15 +5,27 @@ import android.content.Context import android.content.Intent import android.provider.Telephony.Sms import android.telephony.SmsMessage -import com.android.messaging.data.sms.IncomingSmsDeliverer import com.android.messaging.di.receiver.IncomingSmsEntryPoint import dagger.hilt.android.EntryPointAccessors +import kotlinx.coroutines.launch class SmsDeliverReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { - if (intent.action == Sms.Intents.SMS_DELIVER_ACTION) { - delivererFrom(context).deliverFromIntent(context, intent) + if (intent.action != Sms.Intents.SMS_DELIVER_ACTION) { + return + } + + // Import within the broadcast window, not via the job queue that JobScheduler can defer + val pendingResult = goAsync() + val appContext = context.applicationContext + val entryPoint = entryPoint(appContext) + entryPoint.applicationScope().launch(entryPoint.ioDispatcher()) { + try { + entryPoint.incomingSmsDeliverer().deliverFromIntent(appContext, intent) + } finally { + pendingResult.finish() + } } } @@ -26,19 +38,19 @@ class SmsDeliverReceiver : BroadcastReceiver() { errorCode: Int, messages: Array, ) { - delivererFrom(context).deliver( + entryPoint(context).incomingSmsDeliverer().deliver( context = context, subId = subId, errorCode = errorCode, - messages = messages + messages = messages, ) } - private fun delivererFrom(context: Context): IncomingSmsDeliverer { + private fun entryPoint(context: Context): IncomingSmsEntryPoint { return EntryPointAccessors.fromApplication( context.applicationContext, IncomingSmsEntryPoint::class.java, - ).incomingSmsDeliverer() + ) } } }