Skip to content

[receiver] basic MMS support in webhooks#258

Merged
capcom6 merged 2 commits intomasterfrom
issue/240-base-mms-support
Sep 3, 2025
Merged

[receiver] basic MMS support in webhooks#258
capcom6 merged 2 commits intomasterfrom
issue/240-base-mms-support

Conversation

@capcom6
Copy link
Copy Markdown
Owner

@capcom6 capcom6 commented Aug 12, 2025

Summary by CodeRabbit

  • New Features

    • Incoming MMS support: app detects MMS notifications, extracts metadata, and emits a new "mms:received" webhook with messageId, transactionId, subject, size, sender, SIM number and timestamp.
  • Chores

    • Requests runtime RECEIVE_MMS and RECEIVE_WAP_PUSH permissions and registers a high-priority MMS receiver.
  • Refactor

    • Shared webhook payload base and centralized SIM/subscription extraction.
  • Tests

    • Added unit tests for MMS notification parsing.
  • Documentation

    • README updated to document MMS support, webhook payloads, and permissions.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Aug 12, 2025

Walkthrough

Adds MMS receive support: manifest permissions and a WAP‑push BroadcastReceiver; implements MMS PDU parsing, new InboxMessage.Mms model, subscription extraction helper, webhook event/payload for MMS, ReceiverService emission of MMS events, UI permission request, and parser unit tests.

Changes

Cohort / File(s) Summary of changes
Manifest: MMS receive plumbing
app/src/main/AndroidManifest.xml
Adds RECEIVE_MMS and RECEIVE_WAP_PUSH; registers exported .modules.receiver.MmsReceiver with android:permission="android.permission.BROADCAST_WAP_PUSH" and intent-filter for android.provider.Telephony.WAP_PUSH_RECEIVED with MIME application/vnd.wap.mms-message (priority 999).
WAP‑push receiver implementation
app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt
New MmsReceiver : BroadcastReceiver(), KoinComponent. Validates WAP‑push MMS intents, extracts PDU/metadata, logs header/pdu/extras/uri, parses M-Notification-Ind via MMSParser.parseMNotificationInd, builds InboxMessage.Mms (includes messageId, transactionId, contentClass) and forwards to ReceiverService.process.
Receiver service: MMS handling & sim refactor
app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt
Centralizes sim slot computation into a single simNumber. Adds handling for InboxMessage.Mms that emits WebHookEvent.MmsReceived with MmsReceivedPayload.
Inbox message model
app/src/main/java/me/capcom/smsgateway/modules/receiver/data/InboxMessage.kt
Adds nested InboxMessage.Mms(messageId: String?, transactionId: String, subject?, size, contentClass?, address, date, subscriptionId?). Implements equals/hashCode based on transactionId. Also adjusts Data.hashCode() to safely handle nullable ByteArray.
MMS PDU parser
app/src/main/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParser.kt
New object MMSParser with parseMNotificationInd(pdu: ByteArray): MNotificationInd. Exposes MNotificationInd(messageId?, transactionId, date?, from, subject?, messageSize, contentClass?), ContentClass enum, and internal parsing helpers for WSP/WAP fields.
WebHook event enum
app/src/main/java/me/capcom/smsgateway/modules/webhooks/domain/WebHookEvent.kt
Adds enum constant MmsReceived ("mms:received") annotated @SerializedName("mms:received").
Webhooks payload types & hierarchy
app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/...
Adds base MessageEventPayload(messageId, phoneNumber, simNumber). Adds MmsReceivedPayload(messageId, phoneNumber, simNumber, transactionId, subject, size, contentClass, receivedAt) extending it. Updates SmsEventPayload to inherit from MessageEventPayload.
Subscriptions helper
app/src/main/java/me/capcom/smsgateway/helpers/SubscriptionsHelper.kt
Adds extractSubscriptionId(context, intent): Int? to derive subscriptionId from intent extras (EXTRA_SUBSCRIPTION_INDEX, "subscription", EXTRA_SLOT_INDEX), returning null if absent.
MessagesReceiver cleanup
app/src/main/java/me/capcom/smsgateway/modules/receiver/MessagesReceiver.kt
Replaces local subscription extraction with SubscriptionsHelper.extractSubscriptionId(...) and removes the unused private helper.
UI permissions
app/src/main/java/me/capcom/smsgateway/ui/HomeFragment.kt
Adds Manifest.permission.RECEIVE_MMS to the list of permissions requested/checked before starting app functionality.
Tests
app/src/test/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParserTest.kt
Adds unit tests validating MMSParser.parseMNotificationInd extracts the from field from sample M‑Notification‑Ind payloads.
Docs
README.md
Updates wording to include MMS in real-time notifications and webhooks, declares optional RECEIVE_MMS/RECEIVE_WAP_PUSH permissions, and expands example payload fields (messageId, simNumber).

Sequence Diagram(s)

sequenceDiagram
    participant Telephony as Android Telephony
    participant MmsRx as MmsReceiver
    participant Parser as MMSParser
    participant Logs as LogsService
    participant RcvSvc as ReceiverService
    participant Hooks as WebHooksService
    participant Endpoint as Remote Webhook

    Telephony->>MmsRx: WAP_PUSH_RECEIVED (application/vnd.wap.mms-message)
    MmsRx->>MmsRx: validate intent, extract extras/pdu
    MmsRx->>Parser: parseMNotificationInd(pdu)
    Parser-->>MmsRx: MNotificationInd
    MmsRx->>Logs: insert(metadata: uri, extras, header hex, pdu hex, subscription)
    MmsRx->>RcvSvc: process(InboxMessage.Mms)
    RcvSvc->>RcvSvc: compute simNumber from subscriptionId (once)
    RcvSvc->>Hooks: emit(WebHookEvent.MmsReceived, MmsReceivedPayload)
    Hooks-->>Endpoint: POST mms:received payload
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch issue/240-base-mms-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (4)
app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/MmsReceivedPayload.kt (1)

11-13: Document units and value expectations for attachmentCount and size.

Minor: Clarify via KDoc whether size is in bytes and whether negative values are ever expected (shouldn’t be). Helps downstream consumers.

Apply this diff to add brief docs:

 class MmsReceivedPayload(
     messageId: String,
     phoneNumber: String,
     simNumber: Int?,
     val transactionId: String,
     val subject: String?,
-    val attachmentCount: Int,
-    val size: Long,
+    // Count of MMS parts (attachments + smil), non-negative
+    val attachmentCount: Int,
+    // Total MMS size in bytes, non-negative
+    val size: Long,
     val receivedAt: Date
 ) : MessageEventPayload(messageId, phoneNumber, simNumber)
app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt (3)

102-112: Transaction ID extraction is a placeholder; real parsing is needed for reliability.

Assuming “first 8 bytes” is fragile. Properly parse the WAP PDU (M-Notification.ind) to read the Transaction-Id field.

I can wire a minimal PDU parser to extract Transaction-Id and Subject from M-Notification.ind without hidden APIs. Do you want a utility added under modules/receiver/mms/ for this?


68-81: Consider guarding sensitive debug logs.

Hex-dumping the PDU can leak PII (addresses, subject). Keep it under a debug flag or redact after capture.

Apply this diff if BuildConfig is available:

+import me.capcom.smsgateway.BuildConfig
@@
-            logsService.insert(
+            if (BuildConfig.DEBUG) logsService.insert(
                 LogEntry.Priority.DEBUG,
                 MODULE_NAME,
                 "MMS received from $subscriptionId",
                 mapOf(
                     "data" to intent.dataString,
                     "extras" to bundle.keySet().joinToString(", ") { it },
                     "uri" to intent.extras?.getString("uri"),
                     "pduHex" to pdu.joinToString("") { "%02x".format(it) },
                 )
             )
 
-            Log.d(TAG, "MMS received from $subscriptionId")
+            Log.d(TAG, "MMS received from $subscriptionId")

82-96: Next step: wire the MMS message into ReceiverService when ready.

The processing call is intentionally commented. Once PDU parsing is in place, construct InboxMessage.Mms and call receiverSvc.process(...) asynchronously (per goAsync).

When you’re ready, I can help implement the minimal parsing to populate transactionId, subject (if present), size, and a sensible address and date.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4ae8b02 and 2a3569a.

📒 Files selected for processing (8)
  • app/src/main/AndroidManifest.xml (2 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt (2 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/data/InboxMessage.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/domain/WebHookEvent.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/MessageEventPayload.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/MmsReceivedPayload.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/SmsEventPayload.kt (1 hunks)
🔇 Additional comments (8)
app/src/main/java/me/capcom/smsgateway/modules/webhooks/domain/WebHookEvent.kt (1)

24-25: Add MMS webhook event — LGTM

Enum constant and @SerializedName value are consistent with existing SMS events and fit the established naming scheme.

app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/MessageEventPayload.kt (1)

1-7: Base payload abstraction — LGTM

Simple and clear base for message events; trailing comma is fine in modern Kotlin.

app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt (2)

86-95: MMS payload emission — LGTM

Using transactionId as messageId and including MMS-specific fields is appropriate and consistent with the new webhook event. Once SIM handling fix is applied, this path looks solid.


62-68: No need to guard against –1 in getSimSlotIndex
The helper’s signature returns Int? and yields null whenever the subscription isn’t found (or on missing permissions), not -1. The original chain

val simNumber = message.subscriptionId
    ?.let { SubscriptionsHelper.getSimSlotIndex(context, it) }
    ?.let { it + 1 }

already produces null for any “not found” case and only adds 1 to a valid index. No change required.

Likely an incorrect or invalid review comment.

app/src/main/AndroidManifest.xml (1)

57-65: Receiver permissioning is good; confirm need for very high priority

Requiring android.permission.BROADCAST_WAP_PUSH properly restricts senders to the system. The android:priority="999" is very high; on newer Android versions, ordered broadcast priority may have limited effect. Confirm that this priority is intentional and necessary.

If this was copied from older examples, consider whether default priority suffices. No code change required if you’ve validated device behavior.

app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/MmsReceivedPayload.kt (1)

5-14: Payload shape looks good and consistent with existing SMS payloads.

Extends MessageEventPayload cleanly and adds MMS-specific fields. This will serialize well alongside existing SMS events.

app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/SmsEventPayload.kt (2)

5-9: Good refactor to centralize shared fields in MessageEventPayload.

This reduces duplication and aligns SMS/MMS payloads behind a common base. No functional issues spotted.


5-9: Verify JSON compatibility of superclass fields after the refactor.

Moving messageId, phoneNumber, and simNumber to the superclass should still serialize them (Gson serializes superclass fields), but this is a behavioral change worth validating against webhook consumers.

Consider running an integration test or a quick JSON round-trip to ensure field presence and names are unchanged in emitted webhooks. If needed, I can provide a small harness to serialize a couple of payload instances and compare with snapshots.

Comment thread app/src/main/AndroidManifest.xml
Comment thread app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt Outdated
Comment thread app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt (3)

49-56: Be OEM-robust when fetching PDU: check both intent extras and bundle

Some devices put the WAP PDU under the intent extra “pdu” instead of “data”. Prefer intent.getByteArrayExtra first, then fall back to the bundle.

Apply this diff:

-            val pdu = bundle.getByteArray("data") ?: bundle.getByteArray("pdu")
+            val pdu = intent.getByteArrayExtra("data")
+                ?: intent.getByteArrayExtra("pdu")
+                ?: bundle.getByteArray("data")
+                ?: bundle.getByteArray("pdu")

69-79: Build break: MODULE_NAME is undefined

logsService.insert uses MODULE_NAME, but it’s not declared. Define it in the companion object.

Apply this diff to define the constant:

     companion object {
         private const val TAG = "MmsReceiver"
+        private const val MODULE_NAME = "receiver:mms"
     }

165-178: Use SubscriptionManager constants and map slot index to subscriptionId

Return a true subscriptionId. Use SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, and when only a slot index is present, convert via SubscriptionsHelper.

Apply this diff:

-    private fun extractSubscriptionId(context: Context, intent: Intent): Int? {
-        return when {
-            intent.extras?.containsKey("android.telephony.extra.SUBSCRIPTION_INDEX") == true -> intent.extras?.getInt(
-                "android.telephony.extra.SUBSCRIPTION_INDEX"
-            )
-
-            intent.extras?.containsKey("subscription") == true -> intent.extras?.getInt("subscription")
-            intent.extras?.containsKey("android.telephony.extra.SLOT_INDEX") == true -> intent.extras?.getInt(
-                "android.telephony.extra.SLOT_INDEX"
-            )?.let { SubscriptionsHelper.getSubscriptionId(context, it) }
-
-            else -> null
-        }
-    }
+    private fun extractSubscriptionId(context: Context, intent: Intent): Int? {
+        val subId = when {
+            intent.hasExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX) ->
+                intent.getIntExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, -1)
+            // Some OEMs/legacy
+            intent.hasExtra("subscription") ->
+                intent.getIntExtra("subscription", -1)
+            intent.hasExtra(SubscriptionManager.EXTRA_SLOT_INDEX) -> {
+                val slot = intent.getIntExtra(SubscriptionManager.EXTRA_SLOT_INDEX, -1)
+                if (slot >= 0) SubscriptionsHelper.getSubscriptionId(context, slot) ?: -1 else -1
+            }
+            else -> -1
+        }
+        return subId.takeIf { it >= 0 }
+    }
🧹 Nitpick comments (4)
app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt (4)

73-79: Reduce PII risk and log size: limit PDU hex output and label clearly

Dumping full PDU hex can leak content and bloat logs. Log a short prefix and the length instead.

Apply this diff:

-                    "pdu" to pdu.joinToString("") { "%02x".format(it) },
+                    "pduHex" to pdu.copyOfRange(0, minOf(64, pdu.size)).joinToString("") { "%02x".format(it) },
+                    "pduLength" to pdu.size,

81-81: Clarify log wording: “on subscriptionId” rather than “from”

“from $subscriptionId” can be misread as the sender; this value is the subscription ID.

Apply this diff:

-            Log.d(TAG, "MMS received from $subscriptionId")
+            Log.d(TAG, "MMS received on subscriptionId=$subscriptionId")

3-15: Import platform constants for SubscriptionManager

Use SubscriptionManager constants instead of hard-coded strings.

Apply this diff:

 import android.provider.Telephony
 import android.util.Log
+import android.telephony.SubscriptionManager

115-147: Sender extraction is best-effort; consider PDU-based ‘From’ header when available

Intent extras/URI often don’t carry the MSISDN; MMS ‘From’ comes from the PDU. If you plan to surface sender, parse it from the PDU.

I can provide a lightweight parser for common PDU headers (X-Mms-Message-Type, X-Mms-Transaction-Id, From, Subject) if you want to extend debugging fidelity.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2a3569a and 575248c.

📒 Files selected for processing (3)
  • app/src/main/AndroidManifest.xml (2 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/data/InboxMessage.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/main/AndroidManifest.xml
🔇 Additional comments (2)
app/src/main/java/me/capcom/smsgateway/modules/receiver/data/InboxMessage.kt (1)

33-49: MMS identity via transactionId only — good change

Equality/hashCode based solely on transactionId is aligned with stable MMS identity and decouples from base fields. This addresses prior feedback and avoids spurious inequality caused by date/subscription changes.

app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt (1)

103-113: Transaction ID extraction is placeholder

Current approach slices first 8 bytes, which may not align with WAP PDU header structure. Keep for debugging, but plan a real PDU parser if you rely on it downstream.

Would you like me to sketch a minimal M-Notification.ind/M-Retrieve.conf parser to robustly extract Transaction-ID and From headers?

Comment on lines +20 to +31
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != Telephony.Sms.Intents.WAP_PUSH_RECEIVED_ACTION) {
return
}

val contentType = intent.type
if (contentType != "application/vnd.wap.mms-message") {
return
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid I/O on broadcast main thread: use goAsync()

logsService.insert likely performs I/O. Offload processing from onReceive’s main thread to avoid ANRs.

Apply this diff to wrap processing with goAsync and a background thread:

-        try {
+        val pendingResult = goAsync()
+        Thread {
+            try {
@@
-        } catch (e: Exception) {
-            Log.e(TAG, "Error processing MMS", e)
-        }
+            } catch (e: Exception) {
+                Log.e(TAG, "Error processing MMS", e)
+            } finally {
+                pendingResult.finish()
+            }
+        }.start()

Also applies to: 30-31, 98-101

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🔭 Outside diff range comments (1)
app/src/main/java/me/capcom/smsgateway/ui/HomeFragment.kt (1)

333-341: Include RECEIVE_WAP_PUSH in runtime permission requests

The manifest already declares RECEIVE_WAP_PUSH and your MmsReceiver is correctly registered for WAP_PUSH_RECEIVED, but HomeFragment’s permissionsRequired list doesn’t request it at runtime. Add it alongside your other telephony permissions.

• File: app/src/main/java/me/capcom/smsgateway/ui/HomeFragment.kt (around lines 333–341)
Apply this diff:

     Manifest.permission.READ_SMS,
     Manifest.permission.RECEIVE_SMS,
     Manifest.permission.SEND_SMS,
+    Manifest.permission.RECEIVE_WAP_PUSH,
     Manifest.permission.RECEIVE_MMS,
🧹 Nitpick comments (1)
app/src/main/java/me/capcom/smsgateway/ui/HomeFragment.kt (1)

369-386: Avoid starting the service when critical SMS/MMS permissions are denied.

For MMS debugging, starting after a partially denied grant set can silently fail. Consider blocking start and resetting the toggle if essential permissions aren’t granted (at minimum RECEIVE_MMS and RECEIVE_WAP_PUSH).

Example adjustment:

   ) { result ->
-        if (result.values.all { it }) {
-            // Permission is granted. Continue the action or workflow in your
-            // app.
-            Log.d(javaClass.name, "Permissions granted")
-        } else {
-            Toast.makeText(
-                requireContext(),
-                "Not all permissions granted, some features may not work",
-                Toast.LENGTH_SHORT
-            )
-                .show()
-        }
-
-        start()
+        val essential = setOf(
+            Manifest.permission.RECEIVE_MMS,
+            Manifest.permission.RECEIVE_WAP_PUSH
+        )
+        val essentialGranted = essential.all { result[it] == true }
+
+        if (!essentialGranted) {
+            Toast.makeText(
+                requireContext(),
+                "MMS reception requires RECEIVE_MMS and RECEIVE_WAP_PUSH permissions.",
+                Toast.LENGTH_LONG
+            ).show()
+            binding.buttonStart.isChecked = false
+            return@registerForActivityResult
+        }
+        Log.d(javaClass.name, "Permissions granted")
+        start()
   }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 575248c and 886d7c4.

📒 Files selected for processing (3)
  • app/src/main/AndroidManifest.xml (2 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/ui/HomeFragment.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt
  • app/src/main/AndroidManifest.xml

@capcom6 capcom6 linked an issue Aug 21, 2025 that may be closed by this pull request
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
app/src/main/java/me/capcom/smsgateway/modules/receiver/data/InboxMessage.kt (2)

28-30: Fix hashCode for Data: use content-based hash to match equals.

equals uses Arrays.equals on the ByteArray, but hashCode uses object identity. This breaks the equals/hashCode contract in sets/maps.

Apply this diff:

-        override fun hashCode(): Int {
-            return 31 * super.hashCode() + data.hashCode()
-        }
+        override fun hashCode(): Int {
+            // Use content hash to mirror Arrays.equals used in equals
+            return 31 * super.hashCode() + (data?.contentHashCode() ?: 0)
+        }

59-64: Potential NPE in base hashCode when subscriptionId is null.

subscriptionId is nullable but hashCode() is called without a null-safe operator. This can crash when subscriptionId is absent (common for single-SIM).

Apply this diff:

-        result = 31 * result + subscriptionId.hashCode()
+        result = 31 * result + (subscriptionId?.hashCode() ?: 0)
♻️ Duplicate comments (2)
app/src/main/java/me/capcom/smsgateway/modules/receiver/data/InboxMessage.kt (1)

33-48: Equality solely on transactionId is a good call for MMS identity.

This decouples MMS identity from mutable/base fields (date, subscriptionId), aligning with typical MMS dedup keyed by transactionId.

app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt (1)

33-81: Offload processing from the broadcast main thread with goAsync().

logsService.insert and receiverSvc.process likely do I/O. Doing this in onReceive risks ANRs. Use goAsync and finish() after background work completes.

Apply this diff:

-        try {
-            val bundle: Bundle? = intent.extras
-            if (bundle == null) {
-                Log.w(TAG, "No extras found in MMS intent")
-                return
-            }
-
-            // Extract MMS metadata from the intent
-            val pdu = bundle.getByteArray("data") ?: bundle.getByteArray("pdu")
-
-            if (pdu == null) {
-                Log.w(TAG, "No PDU data found in MMS intent")
-                return
-            }
-
-            val mmsNotification = MMSParser.parseMNotificationInd(pdu)
-
-            logsService.insert(
-                LogEntry.Priority.DEBUG,
-                MODULE_NAME,
-                "MMS received from ${mmsNotification.from}",
-                mapOf(
-                    "data" to intent.dataString,
-                    "extras" to bundle.keySet().joinToString(", ") { it },
-                    "uri" to intent.extras?.getString("uri"),
-                    "header" to intent.extras?.getByteArray("header")
-                        ?.joinToString("") { "%02x".format(it) },
-                    "pdu" to pdu.joinToString("") { "%02x".format(it) },
-                )
-            )
-
-            Log.d(TAG, "MMS received from ${mmsNotification.from}")
-
-            val mmsMessage = InboxMessage.Mms(
-                transactionId = mmsNotification.transactionId,
-                subject = mmsNotification.subject,
-                size = mmsNotification.messageSize,
-                address = mmsNotification.from.split('/').first(),
-                date = Date(),
-                subscriptionId = SubscriptionsHelper.extractSubscriptionId(context, intent),
-            )
-
-            // Process the message using the existing ReceiverService
-            receiverSvc.process(context, mmsMessage)
-
-        } catch (e: Exception) {
-            Log.e(TAG, "Error processing MMS", e)
-        }
+        val pendingResult = goAsync()
+        Thread {
+            try {
+                val bundle: Bundle? = intent.extras
+                if (bundle == null) {
+                    Log.w(TAG, "No extras found in MMS intent")
+                    return@Thread
+                }
+
+                // Extract MMS metadata from the intent
+                val pdu = intent.getByteArrayExtra("data") ?: intent.getByteArrayExtra("pdu")
+                if (pdu == null) {
+                    Log.w(TAG, "No PDU data found in MMS intent")
+                    return@Thread
+                }
+
+                val mmsNotification = MMSParser.parseMNotificationInd(pdu)
+
+                logsService.insert(
+                    LogEntry.Priority.DEBUG,
+                    MODULE_NAME,
+                    "MMS received from ${mmsNotification.from}",
+                    mapOf(
+                        "data" to intent.dataString,
+                        "extras" to bundle.keySet().joinToString(", ") { it },
+                        "uri" to intent.extras?.getString("uri"),
+                        "header" to intent.extras?.getByteArray("header")
+                            ?.joinToString("") { "%02x".format(it) },
+                        "pdu" to pdu.joinToString("") { "%02x".format(it) },
+                    )
+                )
+
+                Log.d(TAG, "MMS received from ${mmsNotification.from}")
+
+                val mmsMessage = InboxMessage.Mms(
+                    transactionId = mmsNotification.transactionId,
+                    subject = mmsNotification.subject,
+                    size = mmsNotification.messageSize,
+                    address = mmsNotification.from.split('/').first(),
+                    date = Date(),
+                    subscriptionId = SubscriptionsHelper.extractSubscriptionId(context, intent),
+                )
+
+                // Process the message using the existing ReceiverService
+                receiverSvc.process(context, mmsMessage)
+            } catch (e: Exception) {
+                Log.e(TAG, "Error processing MMS", e)
+            } finally {
+                pendingResult.finish()
+            }
+        }.start()
🧹 Nitpick comments (5)
app/src/main/java/me/capcom/smsgateway/helpers/SubscriptionsHelper.kt (1)

73-86: Prefer platform constants and normalize sentinel values in subscription-id extraction.

Use SubscriptionManager’s EXTRA_* constants and getIntExtra/hasExtra to avoid string typos and default-0 pitfalls. Also, return null for -1 sentinels and convert slotIndex to subscriptionId only when non-negative.

Apply this diff:

@@
-    fun extractSubscriptionId(context: Context, intent: Intent): Int? {
-        return when {
-            intent.extras?.containsKey("android.telephony.extra.SUBSCRIPTION_INDEX") == true -> intent.extras?.getInt(
-                "android.telephony.extra.SUBSCRIPTION_INDEX"
-            )
-
-            intent.extras?.containsKey("subscription") == true -> intent.extras?.getInt("subscription")
-            intent.extras?.containsKey("android.telephony.extra.SLOT_INDEX") == true -> intent.extras?.getInt(
-                "android.telephony.extra.SLOT_INDEX"
-            )?.let { SubscriptionsHelper.getSubscriptionId(context, it) }
-
-            else -> null
-        }
-    }
+    fun extractSubscriptionId(context: Context, intent: Intent): Int? {
+        val subId = when {
+            intent.hasExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX) ->
+                intent.getIntExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, -1)
+            intent.hasExtra("subscription") -> // legacy/oem key
+                intent.getIntExtra("subscription", -1)
+            intent.hasExtra(SubscriptionManager.EXTRA_SLOT_INDEX) -> {
+                val slot = intent.getIntExtra(SubscriptionManager.EXTRA_SLOT_INDEX, -1)
+                if (slot >= 0) getSubscriptionId(context, slot) ?: -1 else -1
+            }
+            else -> -1
+        }
+        return subId.takeIf { it >= 0 }
+    }
app/src/main/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParser.kt (2)

114-119: Bounds-check and clarify parsing of the FROM value.

Guard against negative/oversized lengths and use the read length safely. Replace the unused type with a named variable.

Apply this diff:

-    private fun parseFrom(buffer: ByteBuffer): String {
-        val length = parseShortInteger(buffer)
-        val type = parseShortInteger(buffer)
-
-        return String((0 until length - 1).map { buffer.get() }.toByteArray(), Charsets.UTF_8)
-    }
+    private fun parseFrom(buffer: ByteBuffer): String {
+        val length = parseShortInteger(buffer)
+        if (length <= 0 || buffer.remaining() < length) return ""
+        val addressType = parseShortInteger(buffer) // 128 == address-present
+        val toRead = (length - 1).coerceAtMost(buffer.remaining())
+        val bytes = ByteArray(toRead)
+        buffer.get(bytes)
+        return String(bytes, Charsets.UTF_8)
+    }

143-150: Handle encoded-string values more safely.

The current lookahead for a zero byte isn’t a reliable length indicator. Try a small length-prefixed read, otherwise fall back to text-string.

Apply this diff:

-    private fun parseEncodedString(buffer: ByteBuffer): String {
-        return if (buffer.remaining() > 0 && buffer.get(buffer.position()).toInt() == 0) {
-            buffer.get() // Skip length indicator for now
-            parseTextString(buffer)
-        } else {
-            parseTextString(buffer)
-        }
-    }
+    private fun parseEncodedString(buffer: ByteBuffer): String {
+        if (!buffer.hasRemaining()) return ""
+        val mark = buffer.position()
+        val lenOrToken = buffer.get().toInt() and 0xFF
+        return if (lenOrToken in 1..64 && buffer.remaining() >= lenOrToken) {
+            val bytes = ByteArray(lenOrToken)
+            buffer.get(bytes)
+            String(bytes, Charsets.UTF_8).trimEnd('\u0000')
+        } else {
+            buffer.position(mark)
+            parseTextString(buffer)
+        }
+    }
app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt (2)

41-42: Access PDU via intent extras directly for broader OEM compatibility.

Some OEMs provide WAP PDU under intent extras rather than the Bundle you retrieve. You already fall back to "pdu"; prefer intent.getByteArrayExtra to cover both.

-            val pdu = bundle.getByteArray("data") ?: bundle.getByteArray("pdu")
+            val pdu = intent.getByteArrayExtra("data") ?: intent.getByteArrayExtra("pdu")

67-75: Normalize “from” to handle insert-address-token and address types.

Some carriers send From as insert-address-token or URIs (e.g., +15551234/TYPE=PLMN). Consider a helper to strip /TYPE=... and fallback to “unknown” when the token is present.

Would you like me to add a small normalizer like:

private fun normalizeFrom(from: String?): String =
    from?.substringBefore('/')?.takeUnless { it.equals("insert-address-token", ignoreCase = true) } ?: "unknown"

and use it in the InboxMessage.Mms constructor?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 886d7c4 and 373a2ca.

📒 Files selected for processing (5)
  • app/src/main/java/me/capcom/smsgateway/helpers/SubscriptionsHelper.kt (2 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/MessagesReceiver.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/data/InboxMessage.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParser.kt (1 hunks)
🔇 Additional comments (4)
app/src/main/java/me/capcom/smsgateway/helpers/SubscriptionsHelper.kt (1)

6-6: Import looks correct and scoped.

Importing Intent here is appropriate for centralizing subscription extraction.

app/src/main/java/me/capcom/smsgateway/modules/receiver/MessagesReceiver.kt (1)

33-34: Centralizing subscription-id extraction improves consistency.

Switching to SubscriptionsHelper.extractSubscriptionId keeps SMS/MMS flows aligned and simplifies maintenance.

Also applies to: 40-41

app/src/main/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParser.kt (1)

125-131: parseLongInteger doesn’t compile and computes an incorrect value.

Using a Long in until, then reduce with an Int accumulator, and reading length + 1 bytes is incorrect. Read exactly length bytes into a Long accumulator.

Apply this diff:

-    private fun parseLongInteger(buffer: ByteBuffer): Long {
-        val length = parseShortInteger(buffer).toLong()
-
-        return (0 until length + 1).reduce { acc, _ ->
-            (acc shl 8) + (buffer.get().toLong() and 0xFF)
-        }
-    }
+    private fun parseLongInteger(buffer: ByteBuffer): Long {
+        val length = parseShortInteger(buffer) // number of octets to read
+        var result = 0L
+        repeat(length.coerceAtMost(buffer.remaining())) {
+            result = (result shl 8) or (buffer.get().toLong() and 0xFF)
+        }
+        return result
+    }

Likely an incorrect or invalid review comment.

app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt (1)

22-31: Action and content-type guards look correct.

You properly gate on WAP_PUSH_RECEIVED_ACTION and the MMS content type.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (2)
app/src/main/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParser.kt (2)

69-105: Be resilient to unknown headers and stop once required fields are parsed.

Throwing on unknown header types is brittle across carriers and can abort otherwise valid notifications. Also, once we have the required fields for our flow, we can stop parsing to reduce failure surface.

Apply this diff to make parsing tolerant and short-circuit once key headers are captured:

-        // Parse headers until buffer is exhausted
+        // Parse headers until buffer is exhausted; stop once required fields are captured
         while (buffer.hasRemaining()) {
             val headerType = buffer.get().toInt() and 0xFF
             val value = when (headerType) {
                 HEADER_MESSAGE_TYPE -> parseShortInteger(buffer)
                 HEADER_TRANSACTION_ID -> parseTextString(buffer)
                 HEADER_MMS_VERSION -> parseShortInteger(buffer)
                 HEADER_FROM -> parseFrom(buffer)
                 HEADER_SUBJECT -> parseEncodedString(buffer)
                 HEADER_DELIVERY_REPORT -> parseShortInteger(buffer)
                 HEADER_STORED -> parseShortInteger(buffer)
                 HEADER_MESSAGE_CLASS -> parseShortInteger(buffer)
                 HEADER_PRIORITY -> parseShortInteger(buffer)
                 HEADER_MESSAGE_SIZE -> parseLongInteger(buffer)
                 HEADER_EXPIRY -> parseExpiry(buffer)
                 HEADER_REPLY_CHARGING -> parseShortInteger(buffer)
                 HEADER_REPLY_CHARGING_DEADLINE -> parseShortInteger(buffer)
                 HEADER_REPLY_CHARGING_SIZE -> parseLongInteger(buffer)
                 HEADER_REPLY_CHARGING_ID -> parseTextString(buffer)
                 HEADER_DISTRIBUTION_INDICATOR -> parseShortInteger(buffer)
 //                HEADER_ELEMENT_DESCRIPTOR -> parseShortInteger(buffer)
                 HEADER_RECOMMENDED_RETRIEVAL_MODE -> parseShortInteger(buffer)
                 HEADER_RECOMMENDED_RETRIEVAL_MODE_TEXT -> parseEncodedString(buffer)
                 HEADER_APPLIC_ID -> parseTextString(buffer)
                 HEADER_REPLY_APPLIC_ID -> parseTextString(buffer)
                 HEADER_AUX_APPLIC_INFO -> parseTextString(buffer)
                 HEADER_CONTENT_CLASS -> parseContentClass(buffer)
                 HEADER_DRM_CONTENT -> parseShortInteger(buffer)
                 HEADER_REPLACE_ID -> parseTextString(buffer)
                 HEADER_CONTENT_LOCATION -> parseTextString(buffer)
                 HEADER_MESSAGE_ID -> parseTextString(buffer)
                 HEADER_DATE -> parseLongInteger(buffer)
 
-                else -> throw IllegalArgumentException("Unknown header type: $headerType")
+                else -> {
+                    // Be tolerant: skip unknown header values and continue
+                    skipUnknownValue(buffer)
+                    continue
+                }
             }
             headers[headerType] = value
+            // Short-circuit: we have everything we need for our workflow
+            if (headers.keys.containsAll(setOf(HEADER_MESSAGE_TYPE, HEADER_TRANSACTION_ID, HEADER_FROM, HEADER_MESSAGE_SIZE))) {
+                break
+            }
         }

Add this helper outside the changed range:

private fun skipUnknownValue(buffer: ByteBuffer) {
    if (!buffer.hasRemaining()) return
    val mark = buffer.position()
    val b = buffer.get().toInt() and 0xFF
    // Heuristic 1: if it looks like a short length (1..64), skip that many bytes
    if (b in 1..64 && buffer.remaining() >= b) {
        buffer.position(buffer.position() + b)
        return
    }
    // Heuristic 2: reset and consume until NUL (text-string) or end
    buffer.position(mark)
    while (buffer.hasRemaining()) {
        if (buffer.get() == 0.toByte()) break
    }
}

112-119: Validate required headers and avoid unsafe casts that can crash.

Direct casts will throw when carriers omit or vary fields. Guard required ones and provide sane defaults for optional.

Apply this diff to safely construct MNotificationInd:

-        return MNotificationInd(
-            transactionId = headers[HEADER_TRANSACTION_ID] as String,
-            date = (headers[HEADER_DATE] as? Long)?.let { Date(it * 1000L) },
-            from = headers[HEADER_FROM] as String,
-            subject = headers[HEADER_SUBJECT] as String?,
-            messageSize = headers[HEADER_MESSAGE_SIZE] as Long,
-            contentClass = headers[HEADER_CONTENT_CLASS] as ContentClass?
-        )
+        val transactionId = headers[HEADER_TRANSACTION_ID] as? String
+            ?: throw IllegalArgumentException("Missing transaction-id")
+        val messageSize = when (val ms = headers[HEADER_MESSAGE_SIZE]) {
+            is Long -> ms
+            is Int -> ms.toLong()
+            is Number -> ms.toLong()
+            else -> throw IllegalArgumentException("Missing message-size")
+        }
+        val fromRaw = headers[HEADER_FROM] as? String
+        val from = if (fromRaw.isNullOrBlank()) "unknown" else fromRaw
+        val date = (headers[HEADER_DATE] as? Long)?.let { Date(it * 1000L) }
+        return MNotificationInd(
+            transactionId = transactionId,
+            date = date,
+            from = from,
+            subject = headers[HEADER_SUBJECT] as? String,
+            messageSize = messageSize,
+            contentClass = headers[HEADER_CONTENT_CLASS] as? ContentClass
+        )
🧹 Nitpick comments (4)
app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt (1)

70-77: Document 1-based SIM numbering across webhook payloads

Verified that throughout the codebase, simNumber is treated as a 1-based value:

  • The local HTTP API enforces simNumber >= 1 (app/src/main/java/me/capcom/smsgateway/modules/localserver/routes/MessagesRoutes.kt lines 192–197).
  • The outgoing‐message model’s KDoc specifies “Sim number (from 1 to 3)” (app/src/main/java/me/capcom/smsgateway/modules/messages/data/SendParams.kt lines 9–11).
  • Internally, MessagesService.selectSimNumber subtracts 1 from the 1-based input to get a zero-based slot index (app/src/main/java/me/capcom/smsgateway/modules/messages/MessagesService.kt lines 290–293).

This confirms that using SubscriptionsHelper.getSimSlotIndex(...) + 1 in ReceiverService.kt to produce a 1-based SIM number matches the established contract. No breaking change will occur, but to prevent future confusion:

• Add a KDoc comment at the val simNumber = … calculation in ReceiverService.kt (around lines 62–64) stating “// SIM slot index + 1 (1-based slot number)”.
• Update the webhook payload schema (SmsEventPayload and/or swagger.json) to include a description such as “SIM slot number (1-based)”.

app/src/main/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParser.kt (2)

138-145: Clamp long-integer width to 8 bytes to avoid silent overflow.

Long-integer values beyond 8 octets will overflow a 64-bit accumulator; clamp and skip extras defensively.

Apply this diff:

-    private fun parseLongInteger(buffer: ByteBuffer): Long {
-        val length = parseShortInteger(buffer) // number of octets to read
-        var result = 0L
-        repeat(length.coerceAtMost(buffer.remaining())) {
-            result = (result shl 8) or (buffer.get().toLong() and 0xFF)
-        }
-        return result
-    }
+    private fun parseLongInteger(buffer: ByteBuffer): Long {
+        val length = parseShortInteger(buffer) // number of octets to read
+        var result = 0L
+        val toRead = minOf(length, 8, buffer.remaining())
+        repeat(toRead) {
+            result = (result shl 8) or (buffer.get().toLong() and 0xFF)
+        }
+        // If length > toRead, skip the remaining octets
+        val leftover = length - toRead
+        if (leftover > 0 && buffer.remaining() >= leftover) {
+            buffer.position(buffer.position() + leftover)
+        }
+        return result
+    }

107-111: Optional: consider tolerant validation for message type when short-circuiting earlier.

If you adopt early-break parsing, ensure X-Mms-Message-Type is included in the early-break set (as in the proposed diff) so the require() check cannot fail due to ordering differences across carriers.

app/src/test/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParserTest.kt (1)

5-7: Strengthen the test name and coverage while you’re here.

  • Rename simpleTest to parseFrom_parsesAddressPresentToken to reflect intent.
  • Add assertions for transactionId and messageSize to catch regressions in header parsing.

If you’d like, I can push an expanded test that also exercises:

  • insert-address-token (FROM=129) defaulting to "unknown"
  • tolerance to unknown headers
  • date parsing from HEADER_DATE
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ab3b4dc and 74f09de.

📒 Files selected for processing (6)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt (2 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/data/InboxMessage.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParser.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/MmsReceivedPayload.kt (1 hunks)
  • app/src/test/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParserTest.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/data/InboxMessage.kt
🔇 Additional comments (4)
app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt (2)

13-13: Import for MMS payload wired correctly.

Import matches the new payload type and is used below. No issues.


86-95: Standardize or document messageId format across SMS and MMS events

Our recent grep didn’t reveal any in-repo tests or consumers explicitly parsing messageId format, but external integrations or future tests might assume the current formats. To avoid subtle bugs in correlation, please audit any webhook consumers, integration tests, or docs that reference messageId.

• In ReceiverService.kt, SMS‐received events use

message.hashCode().toUInt().toString(16)

(hex‐encoded hash)
while MMS‐received events use

message.transactionId

(raw transaction ID).

• In EventsReceiver.kt, outgoing SMS events (SmsSent, etc.) use event.id, which may be a different ID format (e.g. DB‐generated UUID or numeric ID).

Two paths forward:

  1. Document the divergence
    Clearly state in the API docs and swagger.json that:

    • SMS‐received messageId is a hex‐encoded hash
    • MMS‐received messageId is the MMSC transaction ID
    • SMS‐sent/delivery/failure events use the domain event’s id
  2. Unify under one scheme
    • Introduce an EventIdFactory.make(message: InboxMessage): String (or overload for sent events) that produces a stable, deterministic, namespaced ID, e.g.:

    • sms:<hexHash>
    • mms:<transactionId>
    • sent:<domainId>
      • Apply this factory in both ReceiverService and EventsReceiver
      • Update database entities and DAO methods to use the new messageId consistently

Next steps:

  • Audit external webhook clients and any integration/system tests for assumptions about messageId.
  • Decide on documentation vs. unification strategy.
  • If standardizing, I can draft the EventIdFactory helper and show the diff for updating both receiver modules.
app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/MmsReceivedPayload.kt (2)

5-14: Payload shape looks solid and consistent with existing patterns.

Extends MessageEventPayload, captures MMS-specific fields, and keeps nullability where appropriate. Good addition.


11-14: Nit: clarify size semantics and document receivedAt format

  • In MmsReceivedPayload.kt, rename
    val size: Long
    val messageSizeBytes: Long
    to make clear this is a byte count.
  • For
    val receivedAt: Date
    add a short KDoc noting that it’s serialized in ISO-8601 (per GsonBuilder.configure() in extensions/GsonBuilder.kt), e.g.:
    /** Timestamp when the message was received, serialized as an ISO-8601 string. */
    val receivedAt: Date

No change to the Date type is needed since all payloads already use java.util.Date + ISO-8601 formatting.

@capcom6 capcom6 force-pushed the issue/240-base-mms-support branch from 74f09de to cf9d0fe Compare August 22, 2025 03:12
@capcom6 capcom6 marked this pull request as ready for review August 22, 2025 03:13
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Aug 22, 2025

🤖 Pull request artifacts

file commit
app-release.apk e26468a
app-release.aab e26468a
app-insecure.apk e26468a
app-insecure.aab e26468a

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
app/src/test/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParserTest.kt (2)

3-3: Use JUnit assertions instead of Kotlin’s assert (disabled unless -ea).

Add the JUnit Assert import:

 import org.junit.Test
+import org.junit.Assert.assertEquals

163-165: Replace Kotlin runtime assert with assertEquals for reliable failure signaling.

-        assert(notification.from == "+19162255887/TYPE=PLMN")
+        assertEquals("+19162255887/TYPE=PLMN", notification.from)
🧹 Nitpick comments (3)
app/src/test/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParserTest.kt (3)

6-7: Nit: make the test name descriptive.

simpleTest doesn’t convey intent. Consider something like parseMNotificationInd_parsesFrom().

-    fun simpleTest() {
+    fun parseMNotificationInd_parsesFrom() {

5-5: Test discovery: prefer default (public) visibility for JUnit4.

Kotlin’s internal compiles to public on the JVM, but some tooling/runners can be picky. If you’ve seen any flakiness, drop internal.

-internal class MMSParserTest {
+class MMSParserTest {

If your CI already runs this test reliably, feel free to ignore.


8-161: Optional: improve payload readability and maintainability.

The gigantic inline PDU literal is hard to audit. Consider one of:

  • Keep nums inline but wrap via bytesOf as above (already proposed).
  • Move the payload to a test fixture file and load it.
  • Store as a compact hex string and decode in-test.

Example hex approach (drop-in replacement for the helper):

private fun hexToBytes(hex: String) = hex
  .replace(Regex("\\s"), "")
  .chunked(2)
  .map { it.toInt(16).toByte() }
  .toByteArray()

// usage
val data = hexToBytes("""
  8C 82 98 69 64 31 5F 31 35 36 33 39 34 38 38 34 30 00
  8D 92 8B 35 38 63 66 63 36 64 61 2D ... 31
""".trimIndent())
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 74f09de and cf9d0fe.

📒 Files selected for processing (13)
  • app/src/main/AndroidManifest.xml (2 hunks)
  • app/src/main/java/me/capcom/smsgateway/helpers/SubscriptionsHelper.kt (2 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/MessagesReceiver.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt (2 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/data/InboxMessage.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParser.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/domain/WebHookEvent.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/MessageEventPayload.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/MmsReceivedPayload.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/SmsEventPayload.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/ui/HomeFragment.kt (1 hunks)
  • app/src/test/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParserTest.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (11)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/MmsReceivedPayload.kt
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/MessagesReceiver.kt
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/MessageEventPayload.kt
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/data/InboxMessage.kt
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/domain/WebHookEvent.kt
  • app/src/main/java/me/capcom/smsgateway/ui/HomeFragment.kt
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/SmsEventPayload.kt
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParser.kt
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt
  • app/src/main/AndroidManifest.xml
🔇 Additional comments (2)
app/src/main/java/me/capcom/smsgateway/helpers/SubscriptionsHelper.kt (2)

6-6: LGTM: import is required for the new helper signature

Intent import is necessary for the added API; all good.


73-87: SubscriptionsHelper.extractSubscriptionId: verified key coverage and null handling

• I searched all *.kt files for other extras keys—literal "slot", "phone", "simId"—and only found the existing cases: SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, "subscription", and EXTRA_SLOT_INDEX. No additional vendor-specific extras were in use.
• Call sites in MessagesReceiver.kt (lines 28–40) and MmsReceiver.kt (lines 72–80) pass the returned Int? directly into the InboxMessage constructors, whose subscriptionId property is declared Int?. Nulls flow through safely and there are no unchecked dereferences.
• No occurrences of an INVALID_SUBSCRIPTION_ID constant were found—null is the canonical “no subscription” value.

No further changes needed here.

@capcom6 capcom6 changed the title [receiver] prepare for MMS debugging [receiver] basic MMS support in webhooks Aug 22, 2025
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
README.md (2)

92-94: Clarify MMS scope (WAP push vs full content).

Given the PR adds a WAP‑push receiver and parses M‑Notification.ind PDUs, say explicitly that MMS support is for incoming MMS notifications (WAP push), not downloading MMS parts/content. This sets correct expectations.

Apply this minimal wording tweak:

- - 🔔 **Real-time incoming message notifications:** Receive instant notifications for incoming SMS and MMS messages via webhook.
+ - 🔔 **Real-time incoming message notifications:** Receive instant webhooks for incoming SMS and MMS WAP‑push notifications (M‑Notification.ind).

238-239: Name the supported webhook events explicitly.

Since MMS webhooks were added, list the concrete event keys to avoid guesswork by integrators.

- Webhooks can be utilized to get notifications of messaging events, such as incoming SMS and MMS messages.
+ Webhooks can be utilized to get notifications of messaging events, such as incoming SMS and MMS messages (e.g., `sms:received`, `mms:received`).

If helpful, I can draft a second, compact example showing registration and a sample mms:received payload.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between cf9d0fe and 5442a1f.

📒 Files selected for processing (1)
  • README.md (4 hunks)
🧰 Additional context used
🪛 LanguageTool
README.md

[style] ~156-~156: You have already used this phrasing in nearby sentences. Consider replacing it to add variety to your writing.
Context: ...: This permissions are optional. If you want to receive webhooks on incoming MMS messag...

(REP_WANT_TO_VB)


[style] ~156-~156: You have already used this phrasing in nearby sentences. Consider replacing it to add variety to your writing.
Context: ... webhooks on incoming MMS messages, you need to grant these permissions. ### Installat...

(REP_NEED_TO_VB)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build release / build-apk
  • GitHub Check: Build insecure / build-apk
🔇 Additional comments (1)
README.md (1)

258-258: Webhooks payload ID naming is consistent

A sweep of all webhook payload classes and the README example confirms every payload field uses messageId—there are no instances of a bare id field in SMS/MMS/status payloads. We can close this concern.

• README.md (line 258): "messageId"
MessageEventPayload, SmsEventPayload, MmsReceivedPayload, etc.: all define messageId in Kotlin data classes
• No id keys found in any webhook payload definitions

Comment thread README.md
- **READ_PHONE_STATE**: This permission is optional. If you want to select the SIM card, you can grant this permission.
- **READ_SMS**: This permission is optional. If you want to read previous SMS messages, you need to grant this permission.
- **RECEIVE_SMS**: This permission is optional. If you want to receive webhooks on incoming SMS, you need to grant this permission.
- **RECEIVE_MMS**, **RECEIVE_WAP_PUSH**: This permissions are optional. If you want to receive webhooks on incoming MMS messages, you need to grant these permissions.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix grammar and reduce repetition in permissions line.

Plural agreement and phrasing need correction in user-facing docs.

- - **RECEIVE_MMS**, **RECEIVE_WAP_PUSH**: This permissions are optional. If you want to receive webhooks on incoming MMS messages, you need to grant these permissions.
+ - **RECEIVE_MMS**, **RECEIVE_WAP_PUSH**: These permissions are optional. Grant them to receive webhooks for incoming MMS notifications.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- **RECEIVE_MMS**, **RECEIVE_WAP_PUSH**: This permissions are optional. If you want to receive webhooks on incoming MMS messages, you need to grant these permissions.
- **RECEIVE_MMS**, **RECEIVE_WAP_PUSH**: These permissions are optional. Grant them to receive webhooks for incoming MMS notifications.
🧰 Tools
🪛 LanguageTool

[style] ~156-~156: You have already used this phrasing in nearby sentences. Consider replacing it to add variety to your writing.
Context: ...: This permissions are optional. If you want to receive webhooks on incoming MMS messag...

(REP_WANT_TO_VB)


[style] ~156-~156: You have already used this phrasing in nearby sentences. Consider replacing it to add variety to your writing.
Context: ... webhooks on incoming MMS messages, you need to grant these permissions. ### Installat...

(REP_NEED_TO_VB)

🤖 Prompt for AI Agents
In README.md around line 156, the permissions sentence has grammar and
repetition issues; replace "This permissions are optional. If you want to
receive webhooks on incoming MMS messages, you need to grant these permissions."
with a corrected, concise phrasing such as: state that RECEIVE_MMS and
RECEIVE_WAP_PUSH are optional permissions and, if you want webhooks for incoming
MMS, you must grant them — ensure singular/plural agreement and remove redundant
wording so it reads clearly for users.

Comment thread README.md
"message": "Received SMS text",
"phoneNumber": "+19162255887",
"simNumber": 1,
"receivedAt": "2024-06-07T11:41:31.000+07:00"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Document SIM numbering semantics (0‑ vs 1‑based).

The example shows "simNumber": 1. Please state whether this is slot index (0‑based per SubscriptionManager) or user‑facing SIM number (1‑based). This avoids off‑by‑one misunderstandings for multi‑SIM devices.

To confirm what the code emits:

If it’s 0‑based, either change the example to 0 or add a note like “simNumber: 0 for SIM slot 1.” If it’s 1‑based, mention “1 = SIM slot 1”. I can update the README once you confirm.


🏁 Script executed:

#!/usr/bin/env bash
# Look for simNumber derivation and types in payloads/services.
rg -nC3 -e '\bsimNumber\b' -e 'SubscriptionManager' -e 'slotIndex' --type kotlin

Length of output: 35767


Clarify simNumber as 1-based in README

The simNumber field in our webhook payloads and request bodies is user-facing and 1-based (1 = SIM slot 1). Internally, we convert this to a 0-based slot index (by doing simNumber – 1 in MessagesService.selectSimNumber). To avoid confusion, please update README.md around line 262 to document this. For example:

// Before sending:
// simNumber is 1-based: 1 = SIM slot 1, 2 = SIM slot 2, etc.
{
  "simNumber": 1,
  "receivedAt": "2024-06-07T11:41:31.000+07:00"
}
🤖 Prompt for AI Agents
In README.md around line 262, the documentation doesn't state that simNumber is
user-facing and 1-based; update the README to explicitly note that simNumber is
1-based (1 = SIM slot 1, 2 = SIM slot 2, etc.) and show the example before
sending (as in the comment) and mention that the service converts it internally
to a 0-based index via simNumber - 1 (e.g., a short note after the JSON
example).

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (5)
README.md (2)

259-263: Document simNumber semantics (1‑based) to avoid off‑by‑one confusion.

The example shows "simNumber": 1. Please add a short note clarifying whether this is 1‑based (user‑facing) or 0‑based (slot index). Prior guidance indicates it’s 1‑based.

Proposed note to insert right below the JSON example:

+ Note: simNumber is user‑facing and 1‑based: 1 = SIM slot 1, 2 = SIM slot 2, etc. The app converts this to a 0‑based slot index internally.

157-157: Fix grammar and reduce repetition in permissions line.

Plural agreement and phrasing need correction in user-facing docs.

Apply:

- - **RECEIVE_MMS**, **RECEIVE_WAP_PUSH**: This permissions are optional. If you want to receive webhooks on incoming MMS messages, you need to grant these permissions.
+ - **RECEIVE_MMS**, **RECEIVE_WAP_PUSH**: These permissions are optional. Grant them to receive webhooks for incoming MMS notifications.
app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt (2)

33-84: Avoid I/O and parsing on broadcast main thread: wrap with goAsync().

logsService.insert() and PDU parsing are I/O/CPU heavy. Doing this on the main broadcast thread risks ANRs and truncated processing. Use goAsync() and finish().

Apply:

-        try {
-            val bundle: Bundle? = intent.extras
-            if (bundle == null) {
-                Log.w(TAG, "No extras found in MMS intent")
-                return
-            }
-
-            // Extract MMS metadata from the intent
-            val pdu = bundle.getByteArray("data") ?: bundle.getByteArray("pdu")
-
-            if (pdu == null) {
-                Log.w(TAG, "No PDU data found in MMS intent")
-                return
-            }
-
-            logsService.insert(
-                LogEntry.Priority.DEBUG,
-                MODULE_NAME,
-                "MMS received",
-                mapOf(
-                    "data" to intent.dataString,
-                    "extras" to bundle.keySet().joinToString(", ") { it },
-                    "uri" to intent.extras?.getString("uri"),
-                    "header" to intent.extras?.getByteArray("header")
-                        ?.joinToString("") { "%02x".format(it) },
-                    "pdu" to pdu.joinToString("") { "%02x".format(it) },
-                )
-            )
-
-            val mmsNotification = MMSParser.parseMNotificationInd(pdu)
-            Log.d(TAG, "MMS received from ${mmsNotification.from}")
-
-            val mmsMessage = InboxMessage.Mms(
-                messageId = mmsNotification.messageId,
-                transactionId = mmsNotification.transactionId,
-                subject = mmsNotification.subject,
-                size = mmsNotification.messageSize,
-                contentClass = mmsNotification.contentClass?.name,
-                address = mmsNotification.from.substringBefore('/'),
-                date = mmsNotification.date ?: Date(),
-                subscriptionId = SubscriptionsHelper.extractSubscriptionId(context, intent),
-            )
-
-            // Process the message using the existing ReceiverService
-            receiverSvc.process(context, mmsMessage)
-
-        } catch (e: Exception) {
-            Log.e(TAG, "Error processing MMS", e)
-        }
+        val pendingResult = goAsync()
+        Thread {
+            try {
+                val bundle: Bundle? = intent.extras
+                if (bundle == null) {
+                    Log.w(TAG, "No extras found in MMS intent")
+                    return@Thread
+                }
+
+                // Extract MMS metadata from the intent
+                val pdu = bundle.getByteArray("data") ?: bundle.getByteArray("pdu")
+                if (pdu == null) {
+                    Log.w(TAG, "No PDU data found in MMS intent")
+                    return@Thread
+                }
+
+                logsService.insert(
+                    LogEntry.Priority.DEBUG,
+                    MODULE_NAME,
+                    "MMS received",
+                    mapOf(
+                        "data" to intent.dataString,
+                        "extras" to bundle.keySet().joinToString(", ") { it },
+                        "uri" to intent.extras?.getString("uri"),
+                        "header" to intent.extras?.getByteArray("header")?.joinToString("") { "%02x".format(it) },
+                        "pdu" to pdu.joinToString("") { "%02x".format(it) },
+                    )
+                )
+
+                val mmsNotification = MMSParser.parseMNotificationInd(pdu)
+                Log.d(TAG, "MMS received from ${mmsNotification.from}")
+
+                val mmsMessage = InboxMessage.Mms(
+                    messageId = mmsNotification.messageId,
+                    transactionId = mmsNotification.transactionId,
+                    subject = mmsNotification.subject,
+                    size = mmsNotification.messageSize,
+                    contentClass = mmsNotification.contentClass?.name,
+                    address = mmsNotification.from.substringBefore('/'),
+                    date = mmsNotification.date ?: Date(),
+                    subscriptionId = SubscriptionsHelper.extractSubscriptionId(context, intent),
+                )
+
+                receiverSvc.process(context, mmsMessage)
+            } catch (e: Exception) {
+                Log.e(TAG, "Error processing MMS", e)
+            } finally {
+                pendingResult.finish()
+            }
+        }.start()

86-88: Compile error: MODULE_NAME is undefined.

logsService.insert() references MODULE_NAME which isn’t declared in this file.

Apply:

     companion object {
         private const val TAG = "MmsReceiver"
+        private const val MODULE_NAME = "receiver:mms"
     }
app/src/main/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParser.kt (1)

124-134: Fix FROM parsing per WSP: use value‑length, handle insert‑address‑token, and avoid dropping an extra byte.

Current logic uses parseShortInteger for the From-value length and truncates one byte; this corrupts the stream and address.

Apply:

-    private fun parseFrom(buffer: ByteBuffer): String {
-        val length = parseShortInteger(buffer)
-        if (length <= 0 || buffer.remaining() < length) return ""
-        val addressType = parseShortInteger(buffer) // 128 == address-present
-        require(addressType == 128) { "Unexpected address type: $addressType" }
-
-        val toRead = (length - 1).coerceAtMost(buffer.remaining())
-        val bytes = ByteArray(toRead)
-        buffer.get(bytes)
-        return String(bytes.copyOfRange(0, toRead - 1), Charsets.UTF_8)
-    }
+    private fun parseFrom(buffer: ByteBuffer): String {
+        val length = parseValueLength(buffer) // WSP From-value length
+        if (length <= 0) return ""
+        if (buffer.remaining() < length) {
+            // Truncated value — skip what remains and bail out
+            val toEnd = buffer.remaining()
+            buffer.position(buffer.position() + toEnd)
+            return ""
+        }
+        val addressType = parseShortInteger(buffer) // 128 = address-present, 129 = insert-address-token
+        if (addressType == 129) return "unknown"
+        require(addressType == 128) { "Unexpected address type: $addressType" }
+
+        val toRead = (length - 1).coerceAtMost(buffer.remaining())
+        val bytes = ByteArray(toRead)
+        buffer.get(bytes)
+        // Trim trailing NUL if present
+        val end = bytes.indexOf(0)
+        val take = if (end >= 0) end else bytes.size
+        return String(bytes, 0, take, Charsets.UTF_8)
+    }
🧹 Nitpick comments (4)
README.md (2)

92-95: Nice additions; one tiny wording polish for readability.

"using the same webhook system used..." repeats "used". Consider this minor rephrase.

Apply:

- - 📖 **Read received messages:** Access [previously received messages](./features/reading-messages.md) using the same webhook system used for real-time notifications.
+ - 📖 **Read received messages:** Access [previously received messages](./features/reading-messages.md) using the same webhook system as real-time notifications.

239-239: Tighten the sentence and add an MMS example payload for completeness.

The line reads fine but can be more direct. Also, since MMS webhooks are a headline feature in this PR, include a minimal mms:received example alongside sms:received.

Apply:

- Webhooks can be utilized to get notifications of messaging events, such as incoming SMS and MMS messages.
+ Use webhooks to receive notifications for messaging events (incoming SMS and MMS).

Suggested MMS example to add right after the SMS example block:

{
  "event": "mms:received",
  "payload": {
    "messageId": "msg_6da5b0...",
    "transactionId": "T123456789",
    "phoneNumber": "+19162255887",
    "simNumber": 1,
    "subject": "Hello from MMS",
    "size": 34567,
    "contentClass": "IMAGE_RICH",
    "receivedAt": "2024-06-07T11:41:31.000+07:00"
  }
}
app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt (1)

48-61: Guard detailed PDU logging or make it opt‑in to avoid PII-heavy logs.

Hex-logging the full WAP PDU can expose sender/recipient identifiers in logs. Consider gating behind a debug flag or redacting sensitive fields.

If you have a debug build flag, wrap logsService.insert(...) with it, or add a user setting (e.g., “Verbose radio logs”) to control this.

app/src/main/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParser.kt (1)

159-172: Encoded string handling is minimal; consider respecting charset if present.

The current implementation largely ignores the optional charset/token aspect of Encoded-String-Value. This is acceptable for a baseline, but carriers sometimes use non-UTF-8.

If you need broader compatibility, extend parseEncodedString to parse and honor the charset when lenOrToken < 32 (value-length with well-known-charset), falling back to UTF-8 when absent.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5442a1f and e15c582.

📒 Files selected for processing (5)
  • README.md (4 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt (2 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/data/InboxMessage.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParser.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/data/InboxMessage.kt
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt
🧰 Additional context used
🪛 LanguageTool
README.md

[grammar] ~94-~94: There might be a mistake here.
Context: ...system used for real-time notifications. 🔒 Security and Privacy: - 🔐 **End-to-e...

(QB_NEW_EN)


[style] ~157-~157: You have already used this phrasing in nearby sentences. Consider replacing it to add variety to your writing.
Context: ...: This permissions are optional. If you want to receive webhooks on incoming MMS messag...

(REP_WANT_TO_VB)


[style] ~157-~157: You have already used this phrasing in nearby sentences. Consider replacing it to add variety to your writing.
Context: ... webhooks on incoming MMS messages, you need to grant these permissions. ### Installat...

(REP_NEED_TO_VB)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build release / build-apk
  • GitHub Check: Build insecure / build-apk
🔇 Additional comments (1)
app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt (1)

22-31: Good early guards for action and MIME type.

The WAP_PUSH_RECEIVED_ACTION and MMS content-type checks are correct and help avoid unnecessary work.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
README.md (2)

157-157: Fix grammar and reduce repetition in permissions line.

Plural agreement and phrasing in user-facing docs.

- **RECEIVE_MMS**, **RECEIVE_WAP_PUSH**: This permissions are optional. If you want to receive webhooks on incoming MMS messages, you need to grant these permissions.
+ **RECEIVE_MMS**, **RECEIVE_WAP_PUSH**: These permissions are optional. Grant them to receive webhooks for incoming MMS notifications.

255-266: Document messageId semantics and clarify SIM numbering (1‑based).

Add a concise note after the example to avoid ambiguity for clients parsing payloads. The SIM note mirrors prior guidance.

     {
       "event": "sms:received",
       "payload": {
         "messageId": "msg_12345abcde",
         "message": "Received SMS text",
         "phoneNumber": "+19162255887",
         "simNumber": 1,
         "receivedAt": "2024-06-07T11:41:31.000+07:00"
       }
     }
     ```
+
+Note:
+- simNumber is user‑facing and 1‑based (1 = SIM slot 1, 2 = SIM slot 2, etc.).
+- messageId is an opaque identifier for the message; treat it as a stable string identifier in your system.

Follow‑up:

  • Please confirm whether messageId is unique per device or globally so we can add that detail here.
🧹 Nitpick comments (2)
README.md (2)

92-95: Tighten phrasing and maintain parallelism in new feature bullets.

Small wording tweaks improve clarity and fix the grammar flagged by the tool.

-- 🔔 **Real-time incoming message notifications:** Receive instant notifications for incoming SMS and MMS messages via webhook.
-- 📱 **Multimedia message support:** Handle MMS messages through dedicated webhooks.
-- 📖 **Read received messages:** Access [previously received messages](https://docs.sms-gate.app/features/reading-messages/) using the same webhook system as real-time notifications.
+- 🔔 **Real-time incoming message notifications:** Receive instant SMS and MMS notifications via webhooks.
+- 📱 **Multimedia message support:** Handle MMS via dedicated webhooks.
+- 📖 **Read received messages:** Access [previously received messages](https://docs.sms-gate.app/features/reading-messages/) via the same webhooks used for real-time notifications.

239-241: Name the event types explicitly.

Call out the concrete event keys so users know what to register for.

-Use webhooks to receive notifications for messaging events (e.g., incoming SMS and MMS).
+Use webhooks to receive notifications for messaging events (e.g., incoming SMS and MMS) via event types such as "sms:received" and "mms:received".

If helpful, I can add a short MMS registration example alongside the SMS one.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e15c582 and cacd1ed.

📒 Files selected for processing (1)
  • README.md (4 hunks)
🧰 Additional context used
🪛 LanguageTool
README.md

[grammar] ~94-~94: There might be a mistake here.
Context: ...bhook system as real-time notifications. 🔒 Security and Privacy: - 🔐 **End-to-e...

(QB_NEW_EN)


[style] ~157-~157: You have already used this phrasing in nearby sentences. Consider replacing it to add variety to your writing.
Context: ...: This permissions are optional. If you want to receive webhooks on incoming MMS messag...

(REP_WANT_TO_VB)


[style] ~157-~157: You have already used this phrasing in nearby sentences. Consider replacing it to add variety to your writing.
Context: ... webhooks on incoming MMS messages, you need to grant these permissions. ### Installat...

(REP_NEED_TO_VB)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build release / build-apk
  • GitHub Check: Build insecure / build-apk

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
README.md (2)

156-156: Fix grammar and reduce repetition in permissions line.

This is a user-facing string; tighten wording and correct plural agreement.

- **RECEIVE_MMS**, **RECEIVE_WAP_PUSH**: This permissions are optional. If you want to receive webhooks on incoming MMS messages, you need to grant these permissions.
+ **RECEIVE_MMS**, **RECEIVE_WAP_PUSH**: These permissions are optional. Grant them to receive webhooks for incoming MMS notifications.

261-262: Document simNumber semantics (1‑based) next to the example.

The example shows "simNumber": 1, but the README doesn’t state whether this is 0‑ or 1‑based. Prior discussion suggests it’s user‑facing and 1‑based (1 = SIM slot 1). Please add a one‑line note to avoid off‑by‑one confusion.

Suggested note to insert immediately below the JSON example:

  • simNumber is user‑facing and 1‑based: 1 = SIM slot 1, 2 = SIM slot 2, etc. (Internally converted to a 0‑based slot index.)
🧹 Nitpick comments (3)
README.md (3)

92-94: Clarify SMS/MMS phrasing; verify “Read received messages” claim.

  • Line 92 looks good but consider naming the concrete events for discoverability.
  • Line 93 could mislead: you don’t typically “read previous messages via webhooks” (webhooks are push). If documentation really provides a way to retrieve previously received items through the same webhook pipeline, please confirm; otherwise, rephrase to reflect how to access historical messages (e.g., server logs or an API).

Proposed edit:

- 🔔 **Real-time incoming message notifications:** Receive instant SMS and MMS notifications via webhooks.
- 📖 **Read received messages:** Access [previously received messages](https://docs.sms-gate.app/features/reading-messages/) via the same webhooks used for real-time notifications.
+ 🔔 **Real-time incoming message notifications:** Receive instant notifications via webhooks (events such as `sms:received` and `mms:received`).
+ 📖 **Read received messages:** See [Reading Messages](https://docs.sms-gate.app/features/reading-messages/) for how to access previously received messages (e.g., from your webhook receiver’s storage or the provided API).

238-238: Name the webhook events explicitly.

Spell out the event names to help readers wire the correct value into the registration payload.

-Use webhooks to receive notifications for messaging events (e.g., incoming SMS and MMS).
+Use webhooks to receive notifications for messaging events such as incoming SMS (`sms:received`) and MMS (`mms:received`).

Add an auxiliary MMS registration example (no diff needed):

curl -X POST -u <username>:<password> \
  -H "Content-Type: application/json" \
  -d '{ "id": "unique-id-mms", "url": "https://webhook.site/<your-uuid>", "event": "mms:received" }' \
  http://<device_local_ip>:8080/webhooks

258-258: Good addition: messageId in webhook payload.

Including a stable messageId improves idempotency and deduplication on the receiver side. Consider adding a short note after the JSON block specifying its uniqueness and lifetime (e.g., unique per device; persists in logs for N days), if applicable.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between cacd1ed and b90cb30.

📒 Files selected for processing (1)
  • README.md (4 hunks)
🧰 Additional context used
🪛 LanguageTool
README.md

[grammar] ~93-~93: There might be a mistake here.
Context: ...bhooks used for real-time notifications. 🔒 Security and Privacy: - 🔐 **End-to-e...

(QB_NEW_EN)


[style] ~156-~156: You have already used this phrasing in nearby sentences. Consider replacing it to add variety to your writing.
Context: ...: This permissions are optional. If you want to receive webhooks on incoming MMS messag...

(REP_WANT_TO_VB)


[style] ~156-~156: You have already used this phrasing in nearby sentences. Consider replacing it to add variety to your writing.
Context: ... webhooks on incoming MMS messages, you need to grant these permissions. ### Installat...

(REP_NEED_TO_VB)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build release / build-apk
  • GitHub Check: Build insecure / build-apk

@capcom6 capcom6 force-pushed the issue/240-base-mms-support branch from b90cb30 to 1f07ad0 Compare August 25, 2025 01:52
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (7)
app/src/main/java/me/capcom/smsgateway/helpers/SubscriptionsHelper.kt (1)

73-87: Avoid ClassCastException and INVALID_SUBSCRIPTION_ID leakage in extractSubscriptionId.
Using Bundle.getInt(...) will crash if OEMs put Longs in extras, and returning -1 as a valid subId cascades bad state. Parse extras via safe casts and filter invalid values; resolve slotIndex only after conversion.

Apply this diff:

-    @SuppressLint("InlinedApi")
-    fun extractSubscriptionId(context: Context, intent: Intent): Int? {
-        return when {
-            intent.extras?.containsKey(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX) == true -> intent.extras?.getInt(
-                SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX
-            )
-
-            intent.extras?.containsKey("subscription") == true -> intent.extras?.getInt("subscription")
-            intent.extras?.containsKey(SubscriptionManager.EXTRA_SLOT_INDEX) == true -> intent.extras?.getInt(
-                SubscriptionManager.EXTRA_SLOT_INDEX
-            )?.let { getSubscriptionId(context, it) }
-
-            else -> null
-        }
-    }
+    @SuppressLint("InlinedApi")
+    fun extractSubscriptionId(context: Context, intent: Intent): Int? {
+        val extras = intent.extras ?: return null
+
+        fun anyToInt(v: Any?): Int? = when (v) {
+            is Int -> v
+            is Long -> if (v in Int.MIN_VALUE..Int.MAX_VALUE) v.toInt() else null
+            is Number -> v.toInt()
+            else -> null
+        }
+
+        anyToInt(extras.get(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX))?.let { subId ->
+            if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) return subId
+        }
+        anyToInt(extras.get(LEGACY_EXTRA_SUBSCRIPTION))?.let { subId ->
+            if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) return subId
+        }
+        anyToInt(extras.get(SubscriptionManager.EXTRA_SLOT_INDEX))?.let { slotIndex ->
+            return getSubscriptionId(context, slotIndex)
+        }
+        return null
+    }

Add this helper constant (outside the range above):

private const val LEGACY_EXTRA_SUBSCRIPTION = "subscription"

I can add unit tests covering Int/Long extras and INVALID_SUBSCRIPTION_ID handling; say the word and I’ll push a patch.

README.md (2)

258-263: Document simNumber semantics (1-based) in the example.
To avoid off‑by‑one confusion on multi‑SIM devices, clarify that simNumber is user‑facing and 1‑based.

Apply this diff to annotate the example:

       "phoneNumber": "+19162255887",
-      "simNumber": 1,
+      // simNumber is 1-based: 1 = SIM slot 1, 2 = SIM slot 2, ...
+      "simNumber": 1,
       "receivedAt": "2024-06-07T11:41:31.000+07:00"

156-156: Fix grammar and tighten phrasing for MMS permissions.
User-facing docs; please correct plurality and reduce repetition.

Apply this diff:

- - **RECEIVE_MMS**, **RECEIVE_WAP_PUSH**: This permissions are optional. If you want to receive webhooks on incoming MMS messages, you need to grant these permissions.
+ - **RECEIVE_MMS**, **RECEIVE_WAP_PUSH**: These permissions are optional. Grant them to receive webhooks for incoming MMS notifications.
app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt (1)

62-68: Guard against invalid slot index to avoid emitting simNumber=0.
getSimSlotIndex can return -1; adding 1 yields an invalid 0 for consumers. Guard before +1.

Apply this diff:

-        val simNumber = message.subscriptionId?.let {
-            SubscriptionsHelper.getSimSlotIndex(
-                context,
-                it
-            )
-        }?.let { it + 1 }
+        val simNumber = message.subscriptionId
+            ?.let { SubscriptionsHelper.getSimSlotIndex(context, it) }
+            ?.takeIf { it >= 0 }
+            ?.plus(1)

Optionally, extract into a small helper to keep this consistent across call sites.

app/src/main/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParser.kt (3)

70-106: Don’t abort on unknown headers; skip them and optionally stop once required fields are parsed

Throwing on vendor/unknown headers can drop notifications from some carriers. Also, you can exit the loop once MESSAGE_TYPE, TRANSACTION_ID, and MESSAGE_SIZE are parsed.

Apply:

-        // Parse headers until buffer is exhausted
+        // Parse headers until buffer is exhausted; be liberal in what we accept
         while (buffer.hasRemaining()) {
             val headerType = buffer.get().toInt() and 0xFF
             val value = when (headerType) {
                 HEADER_MESSAGE_TYPE -> parseShortInteger(buffer)
                 HEADER_TRANSACTION_ID -> parseTextString(buffer)
                 HEADER_MMS_VERSION -> parseShortInteger(buffer)
                 HEADER_FROM -> parseFrom(buffer)
                 HEADER_SUBJECT -> parseEncodedString(buffer)
                 HEADER_DELIVERY_REPORT -> parseShortInteger(buffer)
                 HEADER_STORED -> parseShortInteger(buffer)
                 HEADER_MESSAGE_CLASS -> parseShortInteger(buffer)
                 HEADER_PRIORITY -> parseShortInteger(buffer)
                 HEADER_MESSAGE_SIZE -> parseLongInteger(buffer)
                 HEADER_EXPIRY -> parseExpiry(buffer)
                 HEADER_REPLY_CHARGING -> parseShortInteger(buffer)
                 HEADER_REPLY_CHARGING_DEADLINE -> parseShortInteger(buffer)
                 HEADER_REPLY_CHARGING_SIZE -> parseLongInteger(buffer)
                 HEADER_REPLY_CHARGING_ID -> parseTextString(buffer)
                 HEADER_DISTRIBUTION_INDICATOR -> parseShortInteger(buffer)
 //                HEADER_ELEMENT_DESCRIPTOR -> parseShortInteger(buffer)
                 HEADER_RECOMMENDED_RETRIEVAL_MODE -> parseShortInteger(buffer)
                 HEADER_RECOMMENDED_RETRIEVAL_MODE_TEXT -> parseEncodedString(buffer)
                 HEADER_APPLIC_ID -> parseTextString(buffer)
                 HEADER_REPLY_APPLIC_ID -> parseTextString(buffer)
                 HEADER_AUX_APPLIC_INFO -> parseTextString(buffer)
                 HEADER_CONTENT_CLASS -> parseContentClass(buffer)
                 HEADER_DRM_CONTENT -> parseShortInteger(buffer)
                 HEADER_REPLACE_ID -> parseTextString(buffer)
                 HEADER_CONTENT_LOCATION -> parseTextString(buffer)
                 HEADER_MESSAGE_ID -> parseTextString(buffer)
                 HEADER_DATE -> parseLongInteger(buffer)
-
-                else -> throw IllegalArgumentException("Unknown header type: $headerType")
+                else -> {
+                    skipUnknownValue(buffer)
+                    continue
+                }
             }
             headers[headerType] = value
+            // Optional: bail out once we have the required fields for our workflow
+            if (headers.keys.containsAll(setOf(HEADER_MESSAGE_TYPE, HEADER_TRANSACTION_ID, HEADER_MESSAGE_SIZE))) {
+                break
+            }
         }

Add this helper (outside this range):

// Helper to advance past an unknown value without desynchronizing the stream.
private fun skipUnknownValue(buffer: ByteBuffer) {
    if (!buffer.hasRemaining()) return
    val mark = buffer.position()
    val b = buffer.get().toInt() and 0xFF
    when {
        b in 1..64 && buffer.remaining() >= b -> buffer.position(buffer.position() + b)
        b == LENGTH_QUOTE -> {
            val len = parseUnsignedInt(buffer)
            val toSkip = minOf(len, buffer.remaining())
            buffer.position(buffer.position() + toSkip)
        }
        else -> {
            // Fallback: consume until NUL
            while (buffer.hasRemaining()) {
                if (buffer.get() == 0.toByte()) break
            }
        }
    }
}

113-121: Validate required fields and use safe casts; fall back messageId to transactionId

Direct casts will crash on missing/variant headers. Enforce required fields and keep the rest resilient.

Apply:

-        return MNotificationInd(
-            messageId = headers[HEADER_MESSAGE_ID] as String,
-            transactionId = headers[HEADER_TRANSACTION_ID] as String,
-            date = (headers[HEADER_DATE] as? Long)?.let { Date(it * 1000L) },
-            from = headers[HEADER_FROM] as String,
-            subject = headers[HEADER_SUBJECT] as String?,
-            messageSize = headers[HEADER_MESSAGE_SIZE] as Long,
-            contentClass = headers[HEADER_CONTENT_CLASS] as ContentClass?
-        )
+        val transactionId = headers[HEADER_TRANSACTION_ID] as? String
+            ?: throw IllegalArgumentException("Missing transaction-id")
+        val messageSize = headers[HEADER_MESSAGE_SIZE] as? Long
+            ?: throw IllegalArgumentException("Missing message-size")
+        val from = (headers[HEADER_FROM] as? String) ?: "unknown"
+        val messageId = (headers[HEADER_MESSAGE_ID] as? String) ?: transactionId
+        val date = (headers[HEADER_DATE] as? Long)?.let { Date(it * 1000L) }
+        return MNotificationInd(
+            messageId = messageId,
+            transactionId = transactionId,
+            date = date,
+            from = from,
+            subject = headers[HEADER_SUBJECT] as String?,
+            messageSize = messageSize,
+            contentClass = headers[HEADER_CONTENT_CLASS] as ContentClass?
+        )

124-134: Fix From parsing: use value-length, handle insert-address-token, and avoid buffer desync

Spec requires a value-length then an address-type. Current code reads a short-integer length, drops an extra byte, and doesn’t handle insert-address-token (129).

Apply:

-    private fun parseFrom(buffer: ByteBuffer): String {
-        val length = parseShortInteger(buffer)
-        if (length <= 0 || buffer.remaining() < length) return ""
-        val addressType = parseShortInteger(buffer) // 128 == address-present
-        require(addressType == 128) { "Unexpected address type: $addressType" }
-
-        val toRead = (length - 1).coerceAtMost(buffer.remaining())
-        val bytes = ByteArray(toRead)
-        buffer.get(bytes)
-        return String(bytes.copyOfRange(0, toRead - 1), Charsets.UTF_8)
-    }
+    private fun parseFrom(buffer: ByteBuffer): String {
+        val length = parseValueLength(buffer)
+        if (length <= 0) return ""
+        if (buffer.remaining() < length) {
+            // Truncated value: consume what remains and bail out
+            buffer.position(buffer.limit())
+            return ""
+        }
+        val addressType = parseShortInteger(buffer) // 128=address-present, 129=insert-address-token
+        if (addressType == 129) return "unknown"
+        require(addressType == 128) { "Unexpected address type: $addressType" }
+
+        val toRead = (length - 1).coerceAtMost(buffer.remaining())
+        val bytes = ByteArray(toRead)
+        buffer.get(bytes)
+        // Trim trailing NUL if present
+        val end = bytes.indexOf(0)
+        val take = if (end >= 0) end else bytes.size
+        return String(bytes, 0, take, Charsets.UTF_8)
+    }
🧹 Nitpick comments (5)
README.md (1)

238-238: Add an MMS payload example for completeness.
Since MMS is supported, include a compact mms:received example next to sms:received to reduce guesswork.

Proposed snippet to add after the SMS example:

{
  "event": "mms:received",
  "payload": {
    "messageId": "mms_12345abcde",
    "transactionId": "T12345",
    "subject": "Photos",
    "size": 45678,
    "contentClass": "PERSONAL",
    "phoneNumber": "+19162255887",
    "simNumber": 1,
    "receivedAt": "2024-06-07T11:41:31.000+07:00"
  }
}
app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt (1)

70-77: messageId semantics differ between SMS and MMS; consider unifying or documenting.
SMS uses a hash of the object; MMS uses a protocol/parsed id. This can surprise webhook consumers.

Would you prefer:

  • Unify on stable provider IDs (e.g., include Telephony.Sms._ID in InboxMessage.Text and use it), or
  • Keep as-is and explicitly document the difference in README “Webhooks”?
    I can patch either approach.
app/src/main/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParser.kt (3)

159-172: Encoded-string parsing is incomplete; consume value-length and optional charset before reading text

Current implementation peeks and conditionally consumes, then defers to parseTextString, which can leave bytes unread or misinterpret charset. Suggest a minimal, resilient approach.

Apply:

-    private fun parseEncodedString(buffer: ByteBuffer): String {
-        if (!buffer.hasRemaining()) return ""
-        val mark = buffer.position()
-        val lenOrToken = buffer.get().toInt() and 0xFF
-
-        buffer.position(mark)
-
-        if (lenOrToken < 32) {
-            parseValueLength(buffer)
-            parseShortInteger(buffer)
-        }
-
-        return parseTextString(buffer)
-    }
+    private fun parseEncodedString(buffer: ByteBuffer): String {
+        if (!buffer.hasRemaining()) return ""
+        val mark = buffer.position()
+        val first = buffer.get().toInt() and 0xFF
+        buffer.position(mark)
+        return if (first < 32) {
+            val len = parseValueLength(buffer)
+            if (len <= 0 || !buffer.hasRemaining()) return ""
+            // Optional charset/token byte — consume one, ignore value here
+            if (buffer.hasRemaining()) buffer.get()
+            val toRead = minOf(len - 1, buffer.remaining())
+            val bytes = ByteArray(toRead)
+            buffer.get(bytes)
+            val end = bytes.indexOf(0)
+            val take = if (end >= 0) end else bytes.size
+            String(bytes, 0, take, Charsets.UTF_8)
+        } else {
+            parseTextString(buffer)
+        }
+    }

193-202: Be defensive in value-length parsing to avoid hard crashes on malformed PDUs

Throwing a RuntimeException here will abort parsing; returning 0 (and letting callers handle) or throwing IllegalArgumentException with context is safer.

Apply:

-        throw RuntimeException("Value length > LENGTH_QUOTE!")
+        // Malformed or unsupported value-length; let callers decide what to do.
+        return 0

Optionally include the header context in an IllegalArgumentException for easier diagnostics instead of returning 0.


1-218: Add unit tests for edge cases: unknown headers, insert-address-token, and truncated values

To lock in the resilience, add tests that:

  • Include a vendor-specific header before/after required ones and ensure parsing succeeds.
  • Exercise From with insert-address-token (129) and assert “unknown”.
  • Provide a truncated From value-length and assert we don’t crash.

I can sketch the test vectors if helpful.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b90cb30 and 1f07ad0.

📒 Files selected for processing (14)
  • README.md (4 hunks)
  • app/src/main/AndroidManifest.xml (2 hunks)
  • app/src/main/java/me/capcom/smsgateway/helpers/SubscriptionsHelper.kt (2 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/MessagesReceiver.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt (2 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/data/InboxMessage.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParser.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/domain/WebHookEvent.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/MessageEventPayload.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/MmsReceivedPayload.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/SmsEventPayload.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/ui/HomeFragment.kt (1 hunks)
  • app/src/test/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParserTest.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • app/src/test/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParserTest.kt
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/domain/WebHookEvent.kt
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/MessagesReceiver.kt
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/MessageEventPayload.kt
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/MmsReceivedPayload.kt
🧰 Additional context used
🪛 LanguageTool
README.md

[grammar] ~93-~93: There might be a mistake here.
Context: ...bhooks used for real-time notifications. 🔒 Security and Privacy: - 🔐 **End-to-e...

(QB_NEW_EN)


[style] ~156-~156: You have already used this phrasing in nearby sentences. Consider replacing it to add variety to your writing.
Context: ...: This permissions are optional. If you want to receive webhooks on incoming MMS messag...

(REP_WANT_TO_VB)


[style] ~156-~156: You have already used this phrasing in nearby sentences. Consider replacing it to add variety to your writing.
Context: ... webhooks on incoming MMS messages, you need to grant these permissions. ### Installat...

(REP_NEED_TO_VB)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build release / build-apk
  • GitHub Check: Build insecure / build-apk
🔇 Additional comments (8)
app/src/main/java/me/capcom/smsgateway/helpers/SubscriptionsHelper.kt (1)

6-6: Import looks correct.
Intent import is required for the new API surface.

app/src/main/java/me/capcom/smsgateway/ui/HomeFragment.kt (1)

340-340: Good call adding RECEIVE_MMS to runtime requests.
RECEIVE_MMS is dangerous and needs runtime grant. Not requesting RECEIVE_WAP_PUSH here is fine (normal permission; manifest-only).

README.md (1)

92-94: Feature line reads well.
Clear that webhooks now cover SMS and MMS.

app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt (2)

13-13: Import OK.
New payload type usage requires this import.


86-95: MMS payload construction looks correct.
Fields line up with InboxMessage.Mms and the new MmsReceivedPayload; nullable subject/contentClass handled as-is.

app/src/main/java/me/capcom/smsgateway/modules/receiver/data/InboxMessage.kt (2)

29-31: Good fix: use contentHashCode for byte arrays.
Prevents collisions and matches equals(Arrays.equals(...)).


33-49: Equality/hash for MMS based solely on transactionId is sensible.
This aligns identity with the MMS protocol and avoids coupling to address/date.

app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/SmsEventPayload.kt (1)

6-9: Based on the lack of serialization annotations on SmsEventPayload/MessageEventPayload and the use of Gson in SendWebhookWorker (which reflects on inherited val properties), constructors delegating to the base class will still produce messageId, phoneNumber, and simNumber in the JSON payload. Gson’s default behavior includes inherited fields unless explicitly excluded. No callers rely on property reflection within SmsEventPayload itself—payloads are passed as plain objects into the worker.

Please verify:

  • That SendWebhookWorker indeed uses GsonBuilder().create().toJson(payload) (reflecting inherited val props).
  • There are no custom exclusion strategies or @Expose annotations limiting inherited fields.
  • Your downstream webhook consumers expect these three fields in the same JSON path.

If all checks pass, this consolidation is safe.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt (1)

62-68: Don’t emit simNumber=0 when slot index is -1

Guard negative indices before adding 1; otherwise unknown slot (-1) becomes 0.

-        val simNumber = message.subscriptionId?.let {
-            SubscriptionsHelper.getSimSlotIndex(
-                context,
-                it
-            )
-        }?.let { it + 1 }
+        val simNumber = message.subscriptionId
+            ?.let { SubscriptionsHelper.getSimSlotIndex(context, it) }
+            ?.takeIf { it >= 0 }
+            ?.plus(1)

Optional: extract a resolveSimNumber(context, subscriptionId) helper to reuse across call sites.

🧹 Nitpick comments (2)
app/src/main/java/me/capcom/smsgateway/modules/receiver/data/InboxMessage.kt (1)

33-49: MMS identity by transactionId: good; add minor hardening

Equality/hash now match the intended identity. Consider also treating blank messageId as absent when constructing payloads.

If you want to enforce immutability for Data equality/hash (to avoid mutated arrays changing identity), clone the byte[] at construction time:

class Data(raw: ByteArray?, address: String, date: Date, subscriptionId: Int?) :
    InboxMessage(address, date, subscriptionId) {
    val data: ByteArray? = raw?.clone()
    ...
}
app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt (1)

86-95: MMS payload looks correct; handle blank messageId

Use isNullOrBlank to avoid forwarding empty IDs from parser.

-                messageId = message.messageId ?: message.transactionId,
+                messageId = message.messageId?.takeUnless { it.isBlank() } ?: message.transactionId,
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1f07ad0 and e01c57b.

📒 Files selected for processing (4)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt (2 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/data/InboxMessage.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParser.kt (1 hunks)
  • app/src/test/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParserTest.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParser.kt
  • app/src/test/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParserTest.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build insecure / build-apk
  • GitHub Check: Build release / build-apk
🔇 Additional comments (2)
app/src/main/java/me/capcom/smsgateway/modules/receiver/data/InboxMessage.kt (1)

29-31: Correct hash for ByteArray; nice null-safety

Switching to contentHashCode aligns hashCode with equals for Data. LGTM.

app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt (1)

74-74: Centralized simNumber usage

Good reuse after centralizing the mapping.

Also applies to: 82-82

@capcom6 capcom6 force-pushed the issue/240-base-mms-support branch from e01c57b to e26468a Compare September 3, 2025 01:11
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/java/me/capcom/smsgateway/modules/receiver/data/InboxMessage.kt (1)

61-66: Fix NPE risk in base hashCode when subscriptionId is null.

subscriptionId is nullable; calling hashCode() directly can crash in hash-based collections.

Apply:

-        result = 31 * result + subscriptionId.hashCode()
+        result = 31 * result + (subscriptionId?.hashCode() ?: 0)
♻️ Duplicate comments (7)
app/src/main/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParser.kt (2)

110-123: Validate required headers and use safe casts to avoid crashes.

Direct casts can throw when carriers omit fields. Require transactionId/messageSize; default from; tolerate absent messageId.

Apply:

-        return MNotificationInd(
-            messageId = headers[HEADER_MESSAGE_ID] as String?,
-            transactionId = headers[HEADER_TRANSACTION_ID] as String,
-            date = (headers[HEADER_DATE] as? Long)?.let { Date(it * 1000L) },
-            from = headers[HEADER_FROM] as String,
-            subject = headers[HEADER_SUBJECT] as String?,
-            messageSize = headers[HEADER_MESSAGE_SIZE] as Long,
-            contentClass = headers[HEADER_CONTENT_CLASS] as ContentClass?
-        )
+        val transactionId = headers[HEADER_TRANSACTION_ID] as? String
+            ?: throw IllegalArgumentException("Missing transaction-id")
+        val from = (headers[HEADER_FROM] as? String) ?: "unknown"
+        val messageSize = headers[HEADER_MESSAGE_SIZE] as? Long
+            ?: throw IllegalArgumentException("Missing message-size")
+        val messageId = headers[HEADER_MESSAGE_ID] as? String
+        return MNotificationInd(
+            messageId = messageId,
+            transactionId = transactionId,
+            date = (headers[HEADER_DATE] as? Long)?.let { Date(it * 1000L) },
+            from = from,
+            subject = headers[HEADER_SUBJECT] as String?,
+            messageSize = messageSize,
+            contentClass = headers[HEADER_CONTENT_CLASS] as ContentClass?
+        )

126-134: Fix FROM parsing: use value-length, handle insert-address-token, and fully consume bytes.

Current code uses short-integer for length and may desync the stream or misread the address.

Apply:

-    private fun parseFrom(buffer: ByteBuffer): String {
-        val length = parseShortInteger(buffer)
-        if (length <= 0 || buffer.remaining() < length) return ""
-
-        val addressType = parseShortInteger(buffer) // 128 == address-present
-        require(addressType == FROM_ADDRESS_PRESENT_TOKEN) { "Unexpected address type: $addressType" }
-
-        return parseEncodedString(buffer)
-    }
+    private fun parseFrom(buffer: ByteBuffer): String {
+        val length = parseValueLength(buffer)
+        if (length <= 0) return ""
+        if (buffer.remaining() < length) {
+            buffer.position(buffer.limit()) // truncated; bail safely
+            return ""
+        }
+        val addressType = parseShortInteger(buffer) // 128=address-present, 129=insert-address-token
+        if (addressType == 129) { // insert-address-token
+            val toSkip = minOf(length - 1, buffer.remaining())
+            buffer.position(buffer.position() + toSkip)
+            return "unknown"
+        }
+        require(addressType == FROM_ADDRESS_PRESENT_TOKEN) { "Unexpected address type: $addressType" }
+        val toRead = (length - 1).coerceAtMost(buffer.remaining())
+        val bytes = ByteArray(toRead)
+        buffer.get(bytes)
+        val end = bytes.indexOf(0)
+        val take = if (end >= 0) end else bytes.size
+        return String(bytes, 0, take, Charsets.UTF_8)
+    }
app/src/test/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParserTest.kt (3)

3-3: Import JUnit assertions for reliable failures.

Use Assert methods instead of Kotlin’s assert.

Apply:

 import org.junit.Test
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue

5-5: Add helper to avoid Byte vs Int literal mismatch.

Prevents compile errors in byte array literals.

Apply:

-internal class MMSParserTest {
+private fun bytesOf(vararg nums: Number) = ByteArray(nums.size) { i -> nums[i].toInt().toByte() }
+
+internal class MMSParserTest {

71-74: Replace Kotlin assert with JUnit assertTrue.

Ensures tests fail under default runners.

Apply:

-            val notification = MMSParser.parseMNotificationInd(hexToBytes(it))
-            assert(notification.from.contains("19162255887"))
+            val notification = MMSParser.parseMNotificationInd(hexToBytes(it))
+            assertTrue(notification.from.contains("19162255887"))
README.md (2)

258-262: Clarify simNumber semantics (1‑based).

Avoid off‑by‑one mistakes for multi‑SIM devices.

Apply (add note below the JSON example):

       "payload": {
         "messageId": "msg_12345abcde",
         "message": "Received SMS text",
         "phoneNumber": "+19162255887",
         "simNumber": 1,
         "receivedAt": "2024-06-07T11:41:31.000+07:00"
       }
     }
   }
 }
+
+Note: simNumber is user-facing and 1-based (1 = SIM slot 1).

156-156: Grammar: plural agreement and brevity.

Fix user-facing docs.

Apply:

-- **RECEIVE_MMS**, **RECEIVE_WAP_PUSH**: This permissions are optional. If you want to receive webhooks on incoming MMS messages, you need to grant these permissions.
+- **RECEIVE_MMS**, **RECEIVE_WAP_PUSH**: These permissions are optional. Grant them to receive webhooks for incoming MMS notifications.
🧹 Nitpick comments (1)
README.md (1)

238-238: Document MMS webhook event explicitly.

You mention MMS, but only show sms:received. Add mms:received example or enumerate supported events.

Apply:

-Use webhooks to receive notifications for messaging events (e.g., incoming SMS and MMS).
+Use webhooks to receive notifications for messaging events (e.g., incoming SMS and MMS). Supported events include: `sms:received`, `mms:received`.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e01c57b and e26468a.

📒 Files selected for processing (14)
  • README.md (4 hunks)
  • app/src/main/AndroidManifest.xml (2 hunks)
  • app/src/main/java/me/capcom/smsgateway/helpers/SubscriptionsHelper.kt (2 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/MessagesReceiver.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt (2 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/data/InboxMessage.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParser.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/domain/WebHookEvent.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/MessageEventPayload.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/MmsReceivedPayload.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/SmsEventPayload.kt (1 hunks)
  • app/src/main/java/me/capcom/smsgateway/ui/HomeFragment.kt (1 hunks)
  • app/src/test/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParserTest.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (10)
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/MessageEventPayload.kt
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/MessagesReceiver.kt
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/MmsReceivedPayload.kt
  • app/src/main/java/me/capcom/smsgateway/helpers/SubscriptionsHelper.kt
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/domain/WebHookEvent.kt
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/SmsEventPayload.kt
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/me/capcom/smsgateway/ui/HomeFragment.kt
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt
🧰 Additional context used
🪛 LanguageTool
README.md

[grammar] ~93-~93: There might be a mistake here.
Context: ...bhooks used for real-time notifications. 🔒 Security and Privacy: - 🔐 **End-to-...

(QB_NEW_EN)


[style] ~156-~156: You have already used this phrasing in nearby sentences. Consider replacing it to add variety to your writing.
Context: ...: This permissions are optional. If you want to receive webhooks on incoming MMS messag...

(REP_WANT_TO_VB)


[style] ~156-~156: You have already used this phrasing in nearby sentences. Consider replacing it to add variety to your writing.
Context: ... webhooks on incoming MMS messages, you need to grant these permissions. ### Installat...

(REP_NEED_TO_VB)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build insecure / build-apk
  • GitHub Check: Build release / build-apk
🔇 Additional comments (2)
app/src/main/java/me/capcom/smsgateway/modules/receiver/data/InboxMessage.kt (2)

29-31: Correct: null-safe hash for ByteArray content.

Switching to contentHashCode with a null guard is the right call.


33-49: MMS identity by transactionId looks good—confirm uniqueness across carriers.

Equality/hashCode solely on transactionId simplifies de-duplication. Please confirm transactionId is stable/unique across retries and carriers you target; if not, consider also including address.

Comment on lines +72 to +108
// Parse headers until buffer is exhausted
while (buffer.hasRemaining()) {
val headerType = buffer.get().toInt() and 0xFF
val value = when (headerType) {
HEADER_MESSAGE_TYPE -> parseShortInteger(buffer)
HEADER_TRANSACTION_ID -> parseTextString(buffer)
HEADER_MMS_VERSION -> parseShortInteger(buffer)
HEADER_FROM -> parseFrom(buffer)
HEADER_SUBJECT -> parseEncodedString(buffer)
HEADER_DELIVERY_REPORT -> parseShortInteger(buffer)
HEADER_STORED -> parseShortInteger(buffer)
HEADER_MESSAGE_CLASS -> parseShortInteger(buffer)
HEADER_PRIORITY -> parseShortInteger(buffer)
HEADER_MESSAGE_SIZE -> parseLongInteger(buffer)
HEADER_EXPIRY -> parseExpiry(buffer)
HEADER_REPLY_CHARGING -> parseShortInteger(buffer)
HEADER_REPLY_CHARGING_DEADLINE -> parseShortInteger(buffer)
HEADER_REPLY_CHARGING_SIZE -> parseLongInteger(buffer)
HEADER_REPLY_CHARGING_ID -> parseTextString(buffer)
HEADER_DISTRIBUTION_INDICATOR -> parseShortInteger(buffer)
// HEADER_ELEMENT_DESCRIPTOR -> parseShortInteger(buffer)
HEADER_RECOMMENDED_RETRIEVAL_MODE -> parseShortInteger(buffer)
HEADER_RECOMMENDED_RETRIEVAL_MODE_TEXT -> parseEncodedString(buffer)
HEADER_APPLIC_ID -> parseTextString(buffer)
HEADER_REPLY_APPLIC_ID -> parseTextString(buffer)
HEADER_AUX_APPLIC_INFO -> parseTextString(buffer)
HEADER_CONTENT_CLASS -> parseContentClass(buffer)
HEADER_DRM_CONTENT -> parseShortInteger(buffer)
HEADER_REPLACE_ID -> parseTextString(buffer)
HEADER_CONTENT_LOCATION -> parseTextString(buffer)
HEADER_MESSAGE_ID -> parseTextString(buffer)
HEADER_DATE -> parseLongInteger(buffer)

else -> throw IllegalArgumentException("Unknown header type: $headerType")
}
headers[headerType] = value
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Don’t throw on unknown headers—skip to stay carrier‑resilient.

Unknown/vendor headers are common; throwing drops messages. Skip unknown values safely and continue.

Apply:

-        // Parse headers until buffer is exhausted
+        // Parse headers until buffer is exhausted
         while (buffer.hasRemaining()) {
             val headerType = buffer.get().toInt() and 0xFF
             val value = when (headerType) {
                 ...
-                else -> throw IllegalArgumentException("Unknown header type: $headerType")
+                else -> {
+                    skipUnknownValue(buffer)
+                    continue
+                }
             }
             headers[headerType] = value
         }

Add helper near the end of the file:

+    // Advance past an unknown value without desynchronizing the stream.
+    private fun skipUnknownValue(buffer: ByteBuffer) {
+        if (!buffer.hasRemaining()) return
+        val mark = buffer.position()
+        val b = buffer.get().toInt() and 0xFF
+        when {
+            b in 1..SHORT_LENGTH_MAX && buffer.remaining() >= b -> buffer.position(buffer.position() + b)
+            b == LENGTH_QUOTE -> {
+                val len = parseUnsignedInt(buffer)
+                val toSkip = minOf(len, buffer.remaining())
+                buffer.position(buffer.position() + toSkip)
+            }
+            else -> {
+                // Fallback: consume until NUL or end
+                buffer.position(mark)
+                while (buffer.hasRemaining()) if (buffer.get() == 0.toByte()) break
+            }
+        }
+    }
🤖 Prompt for AI Agents
In app/src/main/java/me/capcom/smsgateway/modules/receiver/parsers/MMSParser.kt
around lines 72 to 108, the code currently throws on unknown header types which
drops messages for vendor/unknown headers; change the else branch to call a new
skipUnknownValue(buffer) helper instead of throwing, and add the provided
skipUnknownValue implementation near the end of the file; ensure the helper
safely advances the ByteBuffer without desynchronizing (handle short lengths,
quoted lengths via parseUnsignedInt, and a fallback that consumes until NUL or
end) so parsing continues for remaining headers.

Comment on lines +79 to +84
val data = byteArrayOf(
0x8C.toByte(),
0x82.toByte(),
0x98.toByte(),
0x69,
0x64,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use bytesOf(...) to build the payload and fix the final assertion.

Prevents type mismatch and uses Assert.

Apply:

-        val data = byteArrayOf(
+        val data = bytesOf(
@@
-        val notification = MMSParser.parseMNotificationInd(data)
-        assert(notification.from == "+19162255887/TYPE=PLMN")
+        val notification = MMSParser.parseMNotificationInd(data)
+        assertEquals("+19162255887/TYPE=PLMN", notification.from)

Also applies to: 232-235

@capcom6 capcom6 merged commit 03781d0 into master Sep 3, 2025
4 checks passed
@capcom6 capcom6 deleted the issue/240-base-mms-support branch September 3, 2025 07:53
@coderabbitai coderabbitai Bot mentioned this pull request Dec 3, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Basic processing of incoming images (MMS)

1 participant