diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 1a28947f3..22d885cb4 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -341,9 +341,6 @@
-
-
-
()
+ 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/contact/repository/ContactsRepositoryImplTest.kt b/app/src/test/kotlin/com/android/messaging/data/contact/repository/ContactsRepositoryImplTest.kt
index 7e9e61dc0..0c6706ef2 100644
--- a/app/src/test/kotlin/com/android/messaging/data/contact/repository/ContactsRepositoryImplTest.kt
+++ b/app/src/test/kotlin/com/android/messaging/data/contact/repository/ContactsRepositoryImplTest.kt
@@ -8,6 +8,7 @@ import android.provider.ContactsContract
import com.android.messaging.data.contact.formatter.ContactDestinationFormatterImpl
import com.android.messaging.data.contact.model.Contact
import com.android.messaging.data.contact.model.ContactDestination
+import com.android.messaging.data.phone.formatter.PhoneNumberFormatterImpl
import com.android.messaging.sms.MmsSmsUtils
import com.android.messaging.util.PhoneUtils
import io.mockk.every
@@ -496,7 +497,9 @@ internal class ContactsRepositoryImplTest {
private fun createRepository(): ContactsRepositoryImpl {
return ContactsRepositoryImpl(
- formatter = ContactDestinationFormatterImpl(),
+ formatter = ContactDestinationFormatterImpl(
+ PhoneNumberFormatterImpl(phoneUtilsInstance),
+ ),
contentResolver = contentResolver,
ioDispatcher = UnconfinedTestDispatcher(),
)
diff --git a/app/src/test/kotlin/com/android/messaging/data/conversationlist/store/ConversationListStatusStoreTest.kt b/app/src/test/kotlin/com/android/messaging/data/conversationlist/store/ConversationListStatusStoreTest.kt
index 50439319a..30ace08d3 100644
--- a/app/src/test/kotlin/com/android/messaging/data/conversationlist/store/ConversationListStatusStoreTest.kt
+++ b/app/src/test/kotlin/com/android/messaging/data/conversationlist/store/ConversationListStatusStoreTest.kt
@@ -1,13 +1,11 @@
package com.android.messaging.data.conversationlist.store
+import com.android.messaging.data.secondaryuser.SecondaryUserNotifier
import com.android.messaging.datamodel.DataModel
import com.android.messaging.datamodel.SyncManager
-import com.android.messaging.receiver.SmsReceiver
import io.mockk.every
-import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkStatic
-import io.mockk.runs
import io.mockk.unmockkAll
import io.mockk.verify
import org.junit.After
@@ -19,17 +17,16 @@ internal class ConversationListStatusStoreTest {
private val dataModel = mockk(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/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
new file mode 100644
index 000000000..d17c70278
--- /dev/null
+++ b/app/src/test/kotlin/com/android/messaging/data/secondaryuser/SecondaryUserMessageResolverTest.kt
@@ -0,0 +1,102 @@
+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 io.mockk.verify
+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,
+ )
+ verify(exactly = 0) {
+ phoneNumberFormatter.formatForDisplay(any())
+ }
+ }
+
+ @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(
+ 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/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/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"
+ }
+}
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/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..77630db50
--- /dev/null
+++ b/src/com/android/messaging/data/secondaryuser/SecondaryUserNotifier.kt
@@ -0,0 +1,81 @@
+package com.android.messaging.data.secondaryuser
+
+import android.app.NotificationManager
+import android.content.Context
+import androidx.core.app.NotificationCompat
+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 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,
+ private val notificationManager: NotificationManager,
+ private val generateNotificationId: GenerateNotificationId,
+) : 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 notificationId = generateNotificationId()
+ 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(),
+ notificationId,
+ notification,
+ )
+ } catch (exception: SecurityException) {
+ LogUtil.e(
+ LogUtil.BUGLE_TAG,
+ "Missing permission to post secondary user notification",
+ exception,
+ )
+ }
+ }
+
+ override fun cancel() {
+ val tag = notificationTag()
+ notificationManager.activeNotifications
+ .filter { notification ->
+ notification.tag == tag
+ }
+ .forEach { notification ->
+ notificationManager.cancel(
+ notification.tag,
+ notification.id,
+ )
+ }
+ }
+
+ 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..5cf8e6117
--- /dev/null
+++ b/src/com/android/messaging/data/sms/IncomingSmsDeliverer.kt
@@ -0,0 +1,108 @@
+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.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.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)
+ deliverInternal(
+ context = context,
+ subId = subId,
+ errorCode = errorCode,
+ messages = messages,
+ executeImmediately = true,
+ )
+
+ 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,
+ ) {
+ 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)
+ 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 -> {
+ val action = ReceiveSmsMessageAction(messageValues)
+ when {
+ executeImmediately -> DataModel.executeActionImmediately(action)
+ else -> action.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/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/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..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
@@ -8,6 +9,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
@@ -79,6 +81,15 @@ internal class CoreProvidesModule {
return context.contentResolver
}
+ @Provides
+ @Reusable
+ fun providePackageManager(
+ @ApplicationContext
+ context: Context,
+ ): PackageManager {
+ return context.packageManager
+ }
+
@Provides
@Reusable
fun provideClipboardManager(
@@ -90,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
@@ -129,4 +140,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/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/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/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
+ }
+}
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..5303ab89c
--- /dev/null
+++ b/src/com/android/messaging/receiver/SmsDeliverReceiver.kt
@@ -0,0 +1,56 @@
+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.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) {
+ 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()
+ }
+ }
+ }
+
+ companion object {
+
+ @JvmStatic
+ fun deliverSmsMessages(
+ context: Context,
+ subId: Int,
+ errorCode: Int,
+ messages: Array,
+ ) {
+ entryPoint(context).incomingSmsDeliverer().deliver(
+ context = context,
+ subId = subId,
+ errorCode = errorCode,
+ messages = messages,
+ )
+ }
+
+ private fun entryPoint(context: Context): IncomingSmsEntryPoint {
+ return EntryPointAccessors.fromApplication(
+ context.applicationContext,
+ IncomingSmsEntryPoint::class.java,
+ )
+ }
+ }
+}
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,