Add mms:downloaded Webhook Event#323
Add mms:downloaded Webhook Event#323capcom6 merged 4 commits intocapcom6:feature/mms-downloaded-webhookfrom
Conversation
This adds a second-stage MMS event that fires after an MMS has been fully downloaded by the system, providing the actual message content (body, subject, attachments). The existing mms:received event only fires on initial notification — before the content is available.
Three main components:
1. MmsContentObserver — Watches for new MMS messages via a ContentObserver on
content://mms.
- Runs on a dedicated HandlerThread to avoid blocking the main thread.
- Tracks a high-water mark (the last processed MMS _id) in SharedPreferences so it never re-processes old messages across restarts.
- On initialization, seeds the high-water mark to the current max ID (so it only fires for messages arriving after the observer starts).
- When onChange fires, it queries for new rows where m_type = 132 (retrieve-conf, meaning fully downloaded) and msg_box = 1 (inbox), then invokes the callback for each.
2. MmsContentReader — A stateless utility that reads MMS content from the content provider given an MMS ID. It extracts:
- Message-level fields: subject, date (converting from seconds to millis), and sub_id (subscription/SIM, API 22+).
- Sender address: queries the addr sub-table with type = 137 (FROM).
- Parts: iterates content://mms/<id>/part — aggregates text/plain parts into a body string, skips SMIL parts, and collects metadata (MIME type, filename, file size) for all other parts as attachments. No binary data is included in the payload.
3. Integration into ReceiverService:
- Creates and starts the MmsContentObserver alongside the existing broadcast receivers.
- The callback (processMmsDownloaded) reads the MMS via MmsContentReader, resolves the SIM slot/phone number from the subscription ID, builds an MmsDownloadedPayload, and emits it as a WebHookEvent.MmsDownloaded webhook event.
4. Supporting changes:
- WebHookEvent enum gets a new MmsDownloaded("mms:downloaded") variant.
- MmsDownloadedPayload extends MessageEventPayload with body, subject, attachments list, and receivedAt timestamp.
- swagger.json documents the new event, payload schema, and attachment schema.
Data flow:
System downloads MMS →
content://mms changes →
ContentObserver fires →
query for new retrieve-conf inbox rows →
MmsContentReader parses content →
MmsDownloadedPayload built →
webhook emitted to subscribers
WalkthroughAdds MMS download detection, extraction, and webhook emission: new content observer and reader for MMS, new MmsDownloaded webhook payload and enum value, API schema updates, and wiring in ReceiverService to emit MMS downloaded events. Changes
Sequence DiagramsequenceDiagram
participant Android as Android OS
participant Observer as MmsContentObserver
participant Receiver as ReceiverService
participant Reader as MmsContentReader
participant Webhook as Webhook System
Android->>Observer: Content change notification
activate Observer
Observer->>Observer: Query MMS provider for new _id > highWaterMark
Observer->>Receiver: onMmsDownloaded(mmsId)
deactivate Observer
activate Receiver
Receiver->>Reader: read(context, mmsId)
activate Reader
Reader->>Reader: Query metadata (subject, date), addr (sender)
Reader->>Reader: Read parts -> body + attachments (Base64)
Reader-->>Receiver: MmsMessage
deactivate Reader
Receiver->>Receiver: Build MmsDownloadedPayload (sender, recipient, attachments, receivedAt)
Receiver->>Webhook: emit(MmsDownloaded event)
activate Webhook
Webhook->>Webhook: Deliver webhook to configured endpoints
deactivate Webhook
deactivate Receiver
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt (1)
25-49: Make lifecycle guard atomic for concurrentstart/stopcalls.
started/mmsContentObserverare mutable shared state without synchronization. Concurrent calls can still interleave and double-register observers.🔧 Suggested hardening
class ReceiverService : KoinComponent { @@ private val eventsReceiver by lazy { EventsReceiver() } + private val lifecycleLock = Any() private var started = false private var mmsContentObserver: MmsContentObserver? = null fun start(context: Context) { - if (started) { - return - } + synchronized(lifecycleLock) { + if (started) return - MessagesReceiver.register(context) - MmsReceiver.register(context) - eventsReceiver.start() + MessagesReceiver.register(context) + MmsReceiver.register(context) + eventsReceiver.start() - val observer = MmsContentObserver(context) { mmsId -> - processMmsDownloaded(context, mmsId) - } - observer.start() - mmsContentObserver = observer - started = true + val observer = MmsContentObserver(context) { mmsId -> + processMmsDownloaded(context, mmsId) + } + observer.start() + mmsContentObserver = observer + started = true + } } fun stop(context: Context) { - mmsContentObserver?.stop() - mmsContentObserver = null - started = false - eventsReceiver.stop() - MmsReceiver.unregister(context) - MessagesReceiver.unregister(context) + synchronized(lifecycleLock) { + mmsContentObserver?.stop() + mmsContentObserver = null + started = false + eventsReceiver.stop() + MmsReceiver.unregister(context) + MessagesReceiver.unregister(context) + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt` around lines 25 - 49, The start/stop lifecycle is not thread-safe: the mutable fields started and mmsContentObserver can be accessed concurrently causing double-registration or lost cleanup. Make access atomic by guarding start() and stop() with a single synchronization mechanism (e.g., use a private lock object or replace started with an AtomicBoolean) so that registration (MessagesReceiver.register, MmsReceiver.register, eventsReceiver.start, creating/starting and assigning MmsContentObserver) and teardown (mmsContentObserver?.stop, nulling mmsContentObserver, setting started to false, eventsReceiver.stop) happen as an atomic critical section; ensure you check and set the guard atomically (compare-and-set if using AtomicBoolean) and only perform observer start/stop when the guard transition succeeds to avoid double-registers or races.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/src/main/assets/api/swagger.json`:
- Around line 57-97: The OpenAPI schema MmsDownloadedAttachment is missing the
emitted Attachment.data field and still claims "no binary data"; update the
MmsDownloadedAttachment schema to include a data property that represents the
attachment payload as a Base64-encoded string (e.g., type: "string", format:
"byte" or plain "string", and nullable if applicable), and update the
description to reflect that binary content is provided as Base64 in the data
field (remove or amend the "no binary data" statement elsewhere in the
MmsDownloadedPayload/attachments description). Ensure you modify the
MmsDownloadedAttachment definition and any references to attachments in the
MmsDownloadedPayload so generated clients will expect Attachment.data (Base64)
at runtime.
In
`@app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsContentObserver.kt`:
- Around line 80-93: The code advances the newMark/highWaterMark even when
onMmsDownloaded throws, causing failed MMS to be acknowledged and not retried;
to fix, only update newMark (and therefore highWaterMark) after successful
processing by moving the newMark = mmsId assignment inside the try block (or use
a local succeeded flag set when onMmsDownloaded(mmsId) completes without
exception) and update highWaterMark from that successful newMark (keep the
existing check newMark > mark), and apply the same "ack only on success" pattern
in ReceiverService where acknowledgements or mark advances occur.
In `@app/src/main/java/me/capcom/smsgateway/modules/receiver/MmsContentReader.kt`:
- Around line 148-150: The readPartData() implementation currently calls
resolver.openInputStream(... ) and uses input.readBytes(), which can load large
attachments into memory; change it to stream the part in bounded chunks (e.g., a
fixed-size byte buffer) and either write into a ByteArrayOutputStream with
iterative reads or wrap the OutputStream in an android.util.Base64OutputStream
so you never hold the whole file in memory; update the logic around
resolver.openInputStream(Uri.parse("content://mms/part/$partId")) and the
Base64.encodeToString usage to perform incremental reads and incremental Base64
encoding and return the resulting string (or null) only after the streamed
encoding completes.
- Around line 30-44: The app is missing the READ_MMS permission needed for
MmsContentReader.read()'s ContentResolver queries; add the permission to the
manifest and include it in the runtime grant flow. Specifically, add
android.permission.READ_MMS to AndroidManifest.xml's uses-permission entries,
and update the runtime permission request list used in HomeFragment (the code
path that requests Manifest.permission.READ_SMS and
Manifest.permission.RECEIVE_MMS) to also request Manifest.permission.READ_MMS so
resolver.query(...) in MmsContentReader.read(...) can succeed.
---
Nitpick comments:
In `@app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt`:
- Around line 25-49: The start/stop lifecycle is not thread-safe: the mutable
fields started and mmsContentObserver can be accessed concurrently causing
double-registration or lost cleanup. Make access atomic by guarding start() and
stop() with a single synchronization mechanism (e.g., use a private lock object
or replace started with an AtomicBoolean) so that registration
(MessagesReceiver.register, MmsReceiver.register, eventsReceiver.start,
creating/starting and assigning MmsContentObserver) and teardown
(mmsContentObserver?.stop, nulling mmsContentObserver, setting started to false,
eventsReceiver.stop) happen as an atomic critical section; ensure you check and
set the guard atomically (compare-and-set if using AtomicBoolean) and only
perform observer start/stop when the guard transition succeeds to avoid
double-registers or races.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: d842a064-eaeb-413a-94ab-8fb31bbc4da2
📒 Files selected for processing (6)
app/src/main/assets/api/swagger.jsonapp/src/main/java/me/capcom/smsgateway/modules/receiver/MmsContentObserver.ktapp/src/main/java/me/capcom/smsgateway/modules/receiver/MmsContentReader.ktapp/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.ktapp/src/main/java/me/capcom/smsgateway/modules/webhooks/domain/WebHookEvent.ktapp/src/main/java/me/capcom/smsgateway/modules/webhooks/payload/MmsDownloadedPayload.kt
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
There was a problem hiding this comment.
♻️ Duplicate comments (1)
app/src/main/assets/api/swagger.json (1)
78-94:⚠️ Potential issue | 🟠 MajorAdd
format: "int64"topartIdandsizefields to match KotlinLongtype.The runtime fields are
Long(64-bit), but the schema uses plainintegerwithout format. OpenAPI generators default to 32-bit integers without an explicit format, risking overflow and client code mismatch.🔧 Proposed fix
"partId": { "description": "The _id from content://mms/part.", + "format": "int64", "type": "integer" }, @@ "size": { "description": "Size in bytes, 0 if unknown.", + "format": "int64", "type": "integer" },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/assets/api/swagger.json` around lines 78 - 94, The OpenAPI schema fields "partId" and "size" are declared as type: integer but need format: "int64" to match the Kotlin Long runtime type; update the swagger.json entries for the properties named partId and size to include "format": "int64" (keeping their type as integer) so OpenAPI generators and clients treat them as 64-bit integers and avoid 32-bit overflow/mismatches.
🧹 Nitpick comments (1)
app/src/main/assets/api/swagger.json (1)
57-63: Consider cappingattachmentsarray size in the contract.
attachmentsis currently unbounded. AddingmaxItemsimproves contract safety and resolves the static-analysis concern for unbounded arrays.💡 Suggested schema hardening
"attachments": { "description": "Metadata for non-text MMS parts, including optional Base64 content.", "items": { "$ref": "#/components/schemas/MmsDownloadedAttachment" }, + "maxItems": 50, "type": "array" },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/main/assets/api/swagger.json` around lines 57 - 63, The attachments array in swagger.json under the MmsDownloadedAttachment reference is unbounded; add a maxItems constraint to that "attachments" schema (and optionally minItems if appropriate) to cap the number of attachments (e.g., maxItems: 10 or another project-appropriate limit) so the OpenAPI contract enforces a safe upper bound for "attachments" and resolves static-analysis warnings.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@app/src/main/assets/api/swagger.json`:
- Around line 78-94: The OpenAPI schema fields "partId" and "size" are declared
as type: integer but need format: "int64" to match the Kotlin Long runtime type;
update the swagger.json entries for the properties named partId and size to
include "format": "int64" (keeping their type as integer) so OpenAPI generators
and clients treat them as 64-bit integers and avoid 32-bit overflow/mismatches.
---
Nitpick comments:
In `@app/src/main/assets/api/swagger.json`:
- Around line 57-63: The attachments array in swagger.json under the
MmsDownloadedAttachment reference is unbounded; add a maxItems constraint to
that "attachments" schema (and optionally minItems if appropriate) to cap the
number of attachments (e.g., maxItems: 10 or another project-appropriate limit)
so the OpenAPI contract enforces a safe upper bound for "attachments" and
resolves static-analysis warnings.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: d4e678eb-8eea-4d83-b51f-59927a545fed
📒 Files selected for processing (1)
app/src/main/assets/api/swagger.json
|
Looks good to me. It implements the feature with minimal changes to the existing API. It is tested and works well. Additional updates to the OpenAPI schema or added docstrings would be up to the project maintainer. I hope it's useful, thanks for the great project! |
|
Hello! Thank you for your contribution! My main question is: have you tested whether the receiver will still work if the app is killed for any reason? For SMS and other events, the system will restart the app when the The only solution I see is to run the observer inside a sticky foreground service. But running this service continuously would drain the battery, so it should only run when there's at least one active webhook for the What are your thoughts on this? |
|
Hi! Thanks for the thoughtful feedback. That makes sense, and I agree with your reliability concern in the general In my deployment, the app runs as a dedicated relay (foreground, plugged in, I haven’t tested kill/restart behavior specifically, and I don’t plan further If you’d like this feature to target stronger availability, your idea |
|
Thanks for the detailed explanation! Regarding the PR: I've noticed a few code style and architecture issues. Would you prefer that I leave inline comments on the specific lines, or would you like me to handle the refactoring myself? |
|
Either works for me! If you leave inline comments I'm happy to address them, but I also won't be offended if you'd rather just refactor it yourself--I know it can sometimes be faster to make changes directly than to explain them. Whatever's easier for you. |
|
Sounds good - I'll go ahead and refactor it myself when I have some free time. Since I don't use MMS personally, I'd really appreciate it if you could test the refactored version once it's ready to make sure everything still works as expected. Thanks again for your contribution! |
|
Yes, absolutely. Just let me know when it's ready and I'll test. Thanks for your effort. |
Introduces a new mms:downloaded webhook event that fires after an MMS has been fully downloaded and parsed from the content provider, as opposed to mms:received which fires at delivery time.
What's new:
MmsContentObserver - A ContentObserver watching content://mms for newly downloaded messages (m_type=132, msg_box=1). Uses a persisted high-water mark to avoid reprocessing on restart.
MmsContentReader - Reads a fully downloaded MMS by ID: resolves sender address, subject, date, plain-text body parts, subscription/SIM info, and attachment metadata + Base64-encoded binary data from content://mms/part.
MmsDownloadedPayload - New webhook payload extending MessageEventPayload, carrying body, subject, receivedAt, and an attachments list (partId, contentType, name, size, Base64 data).
ReceiverService - Starts/stops the content observer alongside existing receivers; on callback, reads the message and emits the mms:downloaded event.
WebHookEvent - Added MmsDownloaded("mms:downloaded") enum entry.
swagger.json - Documented MmsDownloadedPayload, MmsDownloadedAttachment, and the new enum value in the API spec.
Key distinction from mms:received: This event includes the actual message content (text body + Base64 attachment data), making it suitable for integrations that need to process MMS content rather than just be notified of arrival.
Summary by CodeRabbit