Skip to content

Conversation

@plasticChair
Copy link

@plasticChair plasticChair commented Jan 1, 2026

For MMS incoming webhook, there was no body captured. This adds the body content of an MMS to the mms/incoming webhook. This was tested and works for long text only messages 1000 chars+ and MMS with images and text (images are ignored)

Summary by CodeRabbit

  • New Features

    • MMS message bodies are now extracted and included in webhook payloads and stored with incoming MMS metadata.
    • Reception now waits up to 30s for message content to appear before processing to improve body extraction reliability.
  • Bug Fixes / Reliability

    • Progressive retries and sender/address checks reduce missed or partial MMS bodies; processing proceeds with null body only after timeout.
    • Duplicate MMS filtering behavior unchanged.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 1, 2026

Walkthrough

MMS handling now waits for MMS content in the Android MMS ContentProvider (with a 30s timeout) to extract text parts before processing; an optional body: String? was added to InboxMessage.Mms and MmsReceivedPayload, and ReceiverService now forwards the extracted body to webhooks.

Changes

Cohort / File(s) Summary
Core MMS Processing
app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt
Replaces immediate processing with a ContentObserver/watchForMms workflow (30s timeout). Tracks current max MMS ID, retries address/date-based lookup, verifies sender, extracts and aggregates text parts into a body string, and defers processing until body extraction or timeout. Adds helper methods and logging; calls processMmsMessage when ready.
Service / Webhook
app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt
MMS branch now supplies body = message.body when constructing MmsReceivedPayload, propagating the extracted body to webhook emission.
Data Models
app/src/main/java/me/capcom/smsgateway/modules/receiver/data/InboxMessage.kt, app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/MmsReceivedPayload.kt
Added optional body: String? property to InboxMessage.Mms and to MmsReceivedPayload constructors to carry MMS text content.

Sequence Diagram(s)

sequenceDiagram
    participant Android as Android System
    participant Receiver as MmsReceiver
    participant Provider as MMS ContentProvider
    participant Service as ReceiverService
    participant Webhook as Webhook Endpoint

    Android->>Receiver: MMS intent / notification (transactionId, messageId, etc.)
    Receiver->>Receiver: Check de-dup cache (transactionId)
    alt already processed
        Receiver-->>Android: Skip processing
    else new transaction
        Receiver->>Provider: Observe/query for MMS (by transactionId/address/minMmsId)
        par wait for parts (up to 30s)
            Provider-->>Receiver: MMS parts (when available)
        and timeout
            Receiver->>Provider: Fallback query (latest message)
            Provider-->>Receiver: MMS parts or none
        end
        Receiver->>Receiver: verify sender/address, extract text parts -> body (or null)
        Receiver->>Service: processMmsMessage(..., body)
        Service->>Service: Build MmsReceivedPayload(body=...)
        Service->>Webhook: POST payload (includes body)
        Webhook-->>Service: response
        Receiver->>Receiver: Update de-dup cache / logs
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly summarizes the main change: adding MMS body content to the webhook payload.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@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

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

159-170: Consider limiting bodyPreview logging to avoid PII exposure.

The body preview (up to 200 characters) is logged at DEBUG level. While useful for debugging, this could expose sensitive message content in logs. Consider reducing the preview length or ensuring DEBUG logs are not persisted in production.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d7ccf26 and 02c85d6.

📒 Files selected for processing (4)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/data/InboxMessage.kt
  • app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/MmsReceivedPayload.kt
🔇 Additional comments (5)
app/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/MmsReceivedPayload.kt (1)

13-13: LGTM!

The new body field is correctly added as nullable to handle cases where body extraction fails or MMS contains no text parts.

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

98-98: LGTM!

The body field is correctly propagated from the message to the webhook payload.

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

69-83: De-duplication logic looks correct.

The synchronized block properly protects the shared map, and cleanup of old entries prevents unbounded memory growth.


172-182: LGTM!

The MMS message construction correctly includes the extracted body field.

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

39-39: LGTM!

The body field is correctly added as nullable. The existing equals/hashCode implementation based on transactionId is appropriate since the body is derived content that shouldn't affect identity comparison.

Copy link
Owner

@capcom6 capcom6 left a comment

Choose a reason for hiding this comment

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

I appreciate your work. This is a valuable contribution to the project.
However, before I merge these changes, please address some of the issues raised by CodeRabbit and answer the questions in the comments, especially regarding deduplication and "magic numbers" like the delay in the main thread.

Thank you.

…had some issues. To MMS ID and sender verification to increase robustness.
Copy link

@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

Fix all issues with AI Agents 🤖
In @app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt:
- Around line 312-321: In the while loop inside MmsReceiver where
partsCursor.getString(ctIndex) is assigned to ct, guard against ct being null
before calling startsWith by using a null-safe check (e.g., ct != null &&
ct.startsWith("text") or ct?.startsWith("text") == true); ensure you still
retrieve text via partsCursor.getString(textIndex) and keep the existing
isNullOrEmpty check, so the code only appends body when ct indicates text and ct
is non-null.
🧹 Nitpick comments (1)
app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt (1)

170-193: Consider using use {} for cleaner cursor management.

The cursor is closed in multiple code paths (lines 183, 186, 188), which is error-prone. Kotlin's use {} extension ensures the cursor is always closed.

🔎 Proposed refactor
 private fun getCurrentMaxMmsId(context: Context): Long {
     try {
-        val cursor = context.contentResolver.query(
+        context.contentResolver.query(
             Uri.parse("content://mms"),
             arrayOf("_id"),
             null,
             null,
             "_id DESC LIMIT 1"
-        )
-        if (cursor != null && cursor.moveToFirst()) {
-            val idIndex = cursor.getColumnIndex("_id")
-            if (idIndex >= 0) {
-                val maxId = cursor.getLong(idIndex)
-                cursor.close()
-                return maxId
+        )?.use { cursor ->
+            if (cursor.moveToFirst()) {
+                val idIndex = cursor.getColumnIndex("_id")
+                if (idIndex >= 0) {
+                    return cursor.getLong(idIndex)
+                }
             }
-            cursor.close()
         }
-        cursor?.close()
     } catch (e: Exception) {
         Log.w(TAG, "Error getting max MMS ID: ${e.message}", e)
     }
     return 0L
 }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 02c85d6 and 54a83f9.

📒 Files selected for processing (1)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt
🔇 Additional comments (5)
app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt (5)

67-81: Clean delegation to watchForMms.

The refactored flow that delegates MMS body extraction to watchForMms is well-structured. The early validation and logging are appropriate.


223-231: Sender verification with retry is a good safety measure.

The address verification before extracting body content addresses the previous concern about extracting the wrong message. The retry mechanism handles the case where address data isn't immediately populated.

However, move this to a background thread to avoid the 500ms sleep on the main thread (covered in earlier comment).


276-279: Address normalization and comparison logic is appropriate.

The bidirectional endsWith comparison handles international number formats correctly (e.g., +1234567890 vs 234567890).


369-373: Well-documented constants.

The constants are clearly named with appropriate comments explaining their purpose.


139-168: Main thread is still blocked by body extraction logic.

This method runs on the main thread (via the main looper handler), but it calls tryExtractMmsByAddressAndDate which contains Thread.sleep(500) (line 229). Combined with multiple ContentProvider queries, this can still cause ANR issues.

The previous review suggested using goAsync() with a background thread. While the ContentObserver approach is an improvement, the extraction work should run on a background thread, not the main looper.

Likely an incorrect or invalid review comment.

Copy link

@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

🧹 Nitpick comments (3)
app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt (3)

150-168: Consider extracting parameters into a data class.

This method has 17 parameters, which makes it difficult to read and maintain. Consider grouping related parameters into a data class.

🔎 Example approach
private data class MmsExtractionContext(
    val transactionId: String,
    val messageId: String?,
    val subject: String?,
    val size: Long?,
    val contentClass: String?,
    val address: String,
    val date: Date,
    val subscriptionId: Int?
)

private fun attemptBodyExtractionAndProcess(
    context: Context,
    mmsContext: MmsExtractionContext,
    minMmsId: Long,
    startTime: Long,
    timeoutMs: Long,
    observer: ContentObserver,
    handler: Handler,
    processed: AtomicBoolean,
    timeoutRunnable: Runnable,
    handlerThread: HandlerThread
): Boolean

190-213: Simplify cursor handling with use extension.

The cursor closing logic is fragmented across multiple paths. Using Kotlin's use extension ensures the cursor is always closed, even if an exception occurs.

🔎 Proposed fix
     private fun getCurrentMaxMmsId(context: Context): Long {
         try {
-            val cursor = context.contentResolver.query(
+            context.contentResolver.query(
                 Uri.parse("content://mms"),
                 arrayOf("_id"),
                 null,
                 null,
                 "_id DESC LIMIT 1"
-            )
-            if (cursor != null && cursor.moveToFirst()) {
-                val idIndex = cursor.getColumnIndex("_id")
-                if (idIndex >= 0) {
-                    val maxId = cursor.getLong(idIndex)
-                    cursor.close()
-                    return maxId
+            )?.use { cursor ->
+                if (cursor.moveToFirst()) {
+                    val idIndex = cursor.getColumnIndex("_id")
+                    if (idIndex >= 0) {
+                        return cursor.getLong(idIndex)
+                    }
                 }
-                cursor.close()
             }
-            cursor?.close()
         } catch (e: Exception) {
             Log.w(TAG, "Error getting max MMS ID: ${e.message}", e)
         }
         return 0L
     }

215-265: Simplify cursor handling with use extension.

Similar to getCurrentMaxMmsId, this method would benefit from using the use extension for cleaner cursor management.

🔎 Proposed fix
     private fun tryExtractMmsByAddressAndDate(context: Context, address: String, minMmsId: Long): String? {
         try {
             val mmsUri = Uri.parse("content://mms")
             
-            // Get MMS messages with ID greater than minMmsId (newer messages)
-            val cursor = context.contentResolver.query(
+            context.contentResolver.query(
                 mmsUri,
                 arrayOf("_id", "date"),
                 "_id > ?",
                 arrayOf(minMmsId.toString()),
                 "_id DESC LIMIT 10"
-            )
-            
-            Log.d(TAG, "Looking for MMS with ID > $minMmsId from address=$address, found ${cursor?.count ?: 0} candidates")
-            if (cursor != null && cursor.moveToFirst()) {
+            )?.use { cursor ->
+                Log.d(TAG, "Looking for MMS with ID > $minMmsId from address=$address, found ${cursor.count} candidates")
                 val idIndex = cursor.getColumnIndex("_id")
                 val dateIndex = cursor.getColumnIndex("date")
                 
                 if (idIndex < 0 || dateIndex < 0) {
                     Log.w(TAG, "Required column not found in MMS query")
-                    cursor.close()
                     return null
                 }
                 
-                do {
+                while (cursor.moveToNext()) {
                     val mmsId = cursor.getLong(idIndex)
-                    val mmsDate = cursor.getLong(dateIndex)
-                    
-                    // Check if this MMS is from the expected address...
+                    // ... rest of logic unchanged
                     var senderMatch = checkMmsSender(context, mmsId.toString(), address)
                     if (!senderMatch) {
-                        // Wait for address data to be populated
                         Thread.sleep(ADDRESS_DATA_RETRY_DELAY_MS)
                         senderMatch = checkMmsSender(context, mmsId.toString(), address)
                     }
                     
                     if (senderMatch) {
-                        cursor.close()
                         Log.d(TAG, "Found matching MMS ID=$mmsId for address=$address")
                         return extractBodyFromMmsId(context, mmsId.toString())
                     }
-                } while (cursor.moveToNext())
-                cursor.close()
+                }
             }
         } catch (e: Exception) {
             Log.w(TAG, "Error extracting MMS by address: ${e.message}", e)
         }
         return null
     }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 54a83f9 and 4ea812f.

📒 Files selected for processing (1)
  • app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt
🧰 Additional context used
🧬 Code graph analysis (1)
app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt (2)
app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt (1)
  • start (25-29)
app/src/main/java/me/capcom/smsgateway/ui/HomeFragment.kt (1)
  • start (358-360)
🔇 Additional comments (5)
app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsReceiver.kt (5)

29-88: LGTM - onReceive properly delegates to background thread.

The onReceive method now returns quickly after starting the watchForMms workflow on a background HandlerThread, avoiding the ANR risk from the previous implementation.


267-313: Address comparison logic looks reasonable.

The phone number normalization and endsWith comparison handles common cases like international prefixes. The cursor handling could use the use extension for consistency with other methods.


315-348: LGTM - Body extraction handles null content type correctly.

The null-safe check ct?.startsWith("text") == true properly handles cases where the content type column value is null.


350-376: LGTM - Clean delegation to ReceiverService.

The processMmsMessage method properly constructs the InboxMessage.Mms with all extracted data including the optional body and delegates to the existing service.


378-403: LGTM - Constants and registration methods are well-structured.

The constants are appropriately named and documented. The singleton registration pattern is suitable for broadcast receivers.

@plasticChair
Copy link
Author

Just made some code changes. I fixed an issue in my previous code where it used transactional IDs to find a new MMS. This seemed to be unreliable and had me use a delay. I switched to a content Observer to keep track of MMS Message IDs and wait for a new one to appear. That way it wouldn't try to process an older MMS and only grabs the most recent. Using this process, I had to also check against the sender of the MMS in case multiple MMS' were sent at the same time by different senders.

@capcom6
Copy link
Owner

capcom6 commented Jan 6, 2026

Thank you for your contribution and the detailed explanation of the changes. I appreciate the work you've put into improving MMS webhook functionality.

I've reviewed the implementation and have some concerns about the complexity of the code. The solution involves multiple threads, timeouts, a method with 17 parameters, and several conditional blocks that could benefit from early returns to improve readability. Having all of this logic in a single file also makes it difficult to maintain.

Since I don't use MMS myself and don't have the enough time to refactor this code in the near future, I'm afraid I can't approve this PR in its current form.

Perhaps we should leave the current MMS webhook as is and introduce a new one that relies on monitoring the content://mms provider?
The main question is: at what point is content://mms populated with incoming data? And does this always happen or is it dependent on some settings?

@capcom6
Copy link
Owner

capcom6 commented Jan 6, 2026

By the way, is it possible to completely replace the current MMS webhook logic with content://mms monitoring? According to Google, the observer will trigger twice for each message:

  1. The first trigger occurs when a notification (type 130) is added. This is exactly what we have now.
  2. The second trigger occurs when the full message (type 132) replaces the notification after downloading is complete.

However, if mobile data is disabled or background data is limited, the message can remain in the "notification" state (type 130) indefinitely until a manual download is triggered or data is re-enabled. Therefore, these should be two different webhook events.

@plasticChair
Copy link
Author

No worries. I completely understand about the PR not being ready. This was definitely more involved than I initially thought. Before working on this, I had no experience with Android SMS/MMS processing and limited Android exp in general. So, not too surprised things could be improved.
I would put more weight on your decision as far as what you think the best path forward would be. I do like what you said about creating two webhooks for type 130 and 132. That sounds like a better path forward.

I'm OK with closing this PR for now.

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.

2 participants