Repository navigation
feat(adb): SMS read/send/delete/mark bridge via ContentProvider - #1
Conversation
🤖 CodeAnt AI — Review Status
Updated in place by CodeAnt AI · last 5 reviews |
Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughAdds an exported ADB-accessible SMS ContentProvider with repository-backed queries and mutations, Dagger injection, and UID checks. Also adds a scheduled GitHub Actions workflow that merges upstream changes into ChangesADB SMS bridge
Upstream synchronization
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant ADB
participant AdbMessagesProvider
participant MessageRepository
participant ConversationRepository
ADB->>AdbMessagesProvider: query, insert, update, or delete request
AdbMessagesProvider->>MessageRepository: read or mutate messages
AdbMessagesProvider->>ConversationRepository: refresh affected conversations
AdbMessagesProvider-->>ADB: cursor, URI, or mutation count
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 Checkov (3.3.8).github/workflows/sync-upstream.ymlTraceback (most recent call last): 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: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@presentation/src/main/java/com/moez/QKSMS/adb/AdbMessagesProvider.kt`:
- Around line 231-233: The parseIdsFromSelection function must only accept
complete supported predicates: id = <n> or id IN (<n>, ...), using matchEntire
rather than extracting arbitrary digits. Reject unsupported or compound
selections (unless selectionArgs are handled), and deduplicate the returned IDs
before delete() or update() uses them.
- Around line 87-92: Make the lazy initialization in ensureReady thread-safe:
mark injected as volatile and synchronize the check-and-inject sequence so only
one caller invokes appComponent.inject(this), with the flag published after
injection completes. Ensure provider methods access messageRepo and
conversationRepo only after this synchronized initialization.
- Around line 95-103: The AdbMessagesProvider.checkToken authorization must
default-deny when no token is configured: remove the open-access return and
reject non-shell callers, while preserving token validation for configured
tokens. Update ADB_BRIDGE.md at lines 70-79 to document the required production
token lifecycle and explicitly describe any supported Android shell exception.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 3727f4ce-281e-40e6-aa23-e2dad29be0ff
📒 Files selected for processing (4)
ADB_BRIDGE.mdpresentation/src/main/AndroidManifest.xmlpresentation/src/main/java/com/moez/QKSMS/adb/AdbMessagesProvider.ktpresentation/src/main/java/com/moez/QKSMS/injection/AppComponent.kt
| /** Dagger graph isn't ready at provider onCreate (before Application.onCreate); inject lazily. */ | ||
| private fun ensureReady() { | ||
| if (!injected) { | ||
| appComponent.inject(this) | ||
| injected = true | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate and inspect the target file and related component/provider lifecycle.
printf 'Files matching AdbMessagesProvider.kt:\n'
fd -a 'AdbMessagesProvider\.kt$' . || true
printf '\nTarget file outline:\n'
ast-grep outline presentation/src/main/java/com/moez/QKSMS/adb/AdbMessagesProvider.kt || true
printf '\nRelevant source with line numbers:\n'
cat -n presentation/src/main/java/com/moez/QKSMS/adb/AdbMessagesProvider.kt | sed -n '1,180p'
printf '\nReferences to appComponent/injected/inject(this) in provider:\n'
rg -n "appComponent|`@Inject`|`@Component`|interface Adb|class Adb|ensureReady|injected" presentation/src/main/java/com/moez/QKSMS/adb/AdbMessagesProvider.ktRepository: circlesac/quik
Length of output: 9953
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Remaining provider methods:\n'
cat -n presentation/src/main/java/com/moez/QKSMS/adb/AdbMessagesProvider.kt | sed -n '179,250p'
printf '\nDagger/appComponent definitions:\n'
rg -n "appComponent|interface .*Component|`@Component`|AppComponent|inject\\(AdbMessagesProvider" -S .
printf '\nContentProvider lifecycle/threading usage in repo:\n'
fd -a 'AndroidManifest.xml|Manifest.java|AndroidManifest\\.xml' . -x sh -c 'echo "--- $1"; sed -n "1,220p" "$1"' sh {} | sed -n '1,220p'
printf '\nAndroid manifest provider registration snippets:\n'
rg -n "AdbMessagesProvider|adb|ContentProviderProvider|android:multiprocess|android:exported" -S ./presentation ./app || true
printf '\nKotlin/dependency versions relevant for lateinit/lateinit semantics:\n'
rg -n "kotlin|dagger|hilt|androidx|AndroidManifest|provider" -S build.gradle* settings.gradle* || trueRepository: circlesac/quik
Length of output: 28122
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'AppComponentManager:\n'
cat -n presentation/src/main/java/com/moez/QKSMS/injection/AppComponentManager.kt
printf '\nQKApplication lifecycle init order:\n'
cat -n presentation/src/main/java/com/moez/QKSMS/common/QKApplication.kt | sed -n '1,140p'
printf '\nInjection component around provider target:\n'
cat -n presentation/src/main/java/com/moez/QKSMS/injection/AppComponent.kt | sed -n '50,80p'
printf '\nRepository properties/types:\n'
rg -n "class .*MessageRepository|class .*ConversationRepository|lateinit var|MessageRepository =|ConversationRepository =" presentation/src/main/java -S .Repository: circlesac/quik
Length of output: 50370
Synchronize the lazy injection flag before provider operations.
ensureReady() is called from multiple provider methods, but injected is a plain mutable property. A concurrent caller can observe true without a happens-before edge to Dagger’s field assignments and call messageRepo/conversationRepo while they are still uninitialized. Mark it @Volatile and guard the injection in a synchronization block, or use a thread-safe lazy holder, before accessing the repositories.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@presentation/src/main/java/com/moez/QKSMS/adb/AdbMessagesProvider.kt` around
lines 87 - 92, Make the lazy initialization in ensureReady thread-safe: mark
injected as volatile and synchronize the check-and-inject sequence so only one
caller invokes appComponent.inject(this), with the flag published after
injection completes. Ensure provider methods access messageRepo and
conversationRepo only after this synchronized initialization.
| /** Pull the numeric ids out of a `where` clause like `id IN (1, 2, 3)` or `id = 5`. */ | ||
| private fun parseIdsFromSelection(selection: String?): List<Long> = | ||
| selection?.let { Regex("\\d+").findAll(it).map { m -> m.value.toLong() }.toList() } ?: emptyList() |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Strictly parse supported ID predicates.
This extracts digits from any selection: threadId = 42 becomes message ID 42, and compound clauses can collect unrelated numeric values. delete() and update() can therefore mutate unintended messages. This is a destructive targeting bug, not SQL injection.
Accept only complete id = <n> / id IN (<n>, ...) clauses with matchEntire, reject unsupported selections (or honor selectionArgs), and deduplicate IDs.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@presentation/src/main/java/com/moez/QKSMS/adb/AdbMessagesProvider.kt` around
lines 231 - 233, The parseIdsFromSelection function must only accept complete
supported predicates: id = <n> or id IN (<n>, ...), using matchEntire rather
than extracting arbitrary digits. Reject unsupported or compound selections
(unless selectionArgs are handled), and deduplicate the returned IDs before
delete() or update() uses them.
| private fun ensureReady() { | ||
| if (!injected) { | ||
| appComponent.inject(this) | ||
| injected = true | ||
| } | ||
| } |
There was a problem hiding this comment.
Suggestion: ensureReady() is not thread-safe for a Binder-exposed ContentProvider: two concurrent calls can race on injected, leading to duplicate injection or visibility issues where one thread observes injected = true before injected fields are safely visible. Guard initialization with synchronization (or another thread-safe one-time init primitive) so injection and flag publication are atomic. [race condition]
Severity Level: Major ⚠️
❌ Concurrent adb calls can crash provider initialization.
⚠️ On-device automation unreliable under parallel bridge usage.Steps of Reproduction ✅
1. Register the adb bridge provider as seen in
`presentation/src/main/AndroidManifest.xml:258-260`, where `.adb.AdbMessagesProvider` is
declared as a "provider" with `android:authorities="${applicationId}.adb"` and exported
for external access (also documented in `ADB_BRIDGE.md:68-78`).
2. From a host or on-device automation, issue two concurrent commands, for example `adb
shell content query --uri content://dev.octoshrimpy.quik.adb/messages` and `adb shell
content insert --uri content://dev.octoshrimpy.quik.adb/messages ...`, which are
dispatched into `AdbMessagesProvider.query()` (AdbMessagesProvider.kt:105-158) and
`insert()` (160-177) on separate binder threads.
3. Both entrypoints immediately call `ensureReady()` at AdbMessagesProvider.kt:88-93,
which checks the non-volatile `injected` flag at line 72 and, if false, executes
`appComponent.inject(this)` at line 90 to initialize the `@Inject lateinit var` fields
`messageRepo` and `conversationRepo` defined at lines 69-70.
4. Because `ensureReady()` is unsynchronized, two threads can race: writes to `injected`
and the injected fields may be observed out of order, allowing a third thread to see
`injected == true` while `messageRepo` / `conversationRepo` are not safely published, then
dereference `messageRepo` in `insert()` (line 172) or `delete()` (line 194), causing
intermittent initialization errors or crashes in the adb bridge under parallel usage.(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** presentation/src/main/java/com/moez/QKSMS/adb/AdbMessagesProvider.kt
**Line:** 88:93
**Comment:**
*Race Condition: `ensureReady()` is not thread-safe for a Binder-exposed `ContentProvider`: two concurrent calls can race on `injected`, leading to duplicate injection or visibility issues where one thread observes `injected = true` before injected fields are safely visible. Guard initialization with synchronization (or another thread-safe one-time init primitive) so injection and flag publication are atomic.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| val limit = uri.getQueryParameter("limit")?.toIntOrNull() ?: results.size | ||
|
|
||
| MatrixCursor(MESSAGE_COLUMNS).apply { | ||
| results.take(limit).forEach { m -> |
There was a problem hiding this comment.
Suggestion: limit is accepted as any integer, but take() throws IllegalArgumentException for negative values; a request like ?limit=-1 will crash the provider. Clamp limit to a non-negative range before passing it to take. [logic error]
Severity Level: Major ⚠️
❌ Negative limit crashes adb message queries unexpectedly.
⚠️ Automation scripts fail when misconfigured query parameters.Steps of Reproduction ✅
1. Use the documented adb SMS bridge from `ADB_BRIDGE.md:20-34` but supply a negative
limit, e.g. `adb shell content query --uri
"content://dev.octoshrimpy.quik.adb/messages?limit=-1"`; this enters
`AdbMessagesProvider.query()` at
`presentation/src/main/java/com/moez/QKSMS/adb/AdbMessagesProvider.kt:105-158`.
2. Inside `query()`, Realm results are loaded into `results` (lines 113-123). The code
computes `limit` from the URI at line 124: `val limit =
uri.getQueryParameter("limit")?.toIntOrNull() ?: results.size`.
3. With `?limit=-1`, `getQueryParameter("limit")` returns the string `"-1"`,
`toIntOrNull()` yields `-1`, and therefore `limit` is set to `-1` instead of defaulting to
`results.size`.
4. At line 127, `results.take(limit)` calls Kotlin's `List.take(n)` with `n = -1`, which
throws `IllegalArgumentException` for a negative count, causing the ContentProvider to
crash and the adb `content query` operation to fail unexpectedly.(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** presentation/src/main/java/com/moez/QKSMS/adb/AdbMessagesProvider.kt
**Line:** 124:127
**Comment:**
*Logic Error: `limit` is accepted as any integer, but `take()` throws `IllegalArgumentException` for negative values; a request like `?limit=-1` will crash the provider. Clamp `limit` to a non-negative range before passing it to `take`.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| if (ids.isEmpty()) return 0 | ||
|
|
||
| // Look up affected threads before deleting so we can refresh those conversations after. | ||
| val threadIds = ids.mapNotNull { messageRepo.getUnmanagedMessage(it)?.threadId }.distinct() |
There was a problem hiding this comment.
Suggestion: Resolving thread IDs by calling getUnmanagedMessage per ID creates one Realm lookup per message, which scales poorly for bulk operations (id IN (...)). Fetch all needed messages in one query to avoid N+1 database access in delete/update paths. [performance]
Severity Level: Major ⚠️
⚠️ Bulk delete/update operations slow for large ID sets.
⚠️ On-device automation suffers delays on multi-message actions.Steps of Reproduction ✅
1. Use the bridge bulk delete pattern from `ADB_BRIDGE.md:49-57`, e.g. `adb shell content
delete --uri content://dev.octoshrimpy.quik.adb/messages --where "id IN
(1,2,3,...,1000)"`, which reaches `AdbMessagesProvider.delete()` at
`presentation/src/main/java/com/moez/QKSMS/adb/AdbMessagesProvider.kt:179-199`.
2. `delete()` parses the IDs from the selection string via
`parseIdsFromSelection(selection)` (lines 183-186), producing a `List<Long>` with many
message IDs for large operations.
3. To compute affected threads, `delete()` executes `val threadIds = ids.mapNotNull {
messageRepo.getUnmanagedMessage(it)?.threadId }.distinct()` at line 191. Each
`getUnmanagedMessage(it)` call opens a new Realm instance and queries by ID, as
implemented in `MessageRepositoryImpl.getUnmanagedMessage()` at
`data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt:13-16`.
4. For N IDs, this results in N separate Realm get/open operations before
`messageRepo.deleteMessages(ids)` at line 194, causing N+1 database queries and degrading
performance noticeably when automation or scripts perform large multi-message
delete/update actions.(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** presentation/src/main/java/com/moez/QKSMS/adb/AdbMessagesProvider.kt
**Line:** 191:191
**Comment:**
*Performance: Resolving thread IDs by calling `getUnmanagedMessage` per ID creates one Realm lookup per message, which scales poorly for bulk operations (`id IN (...)`). Fetch all needed messages in one query to avoid N+1 database access in delete/update paths.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| if (ids.isEmpty()) return 0 | ||
|
|
||
| // read/unread operate per-thread in the repository, so resolve threads from the message ids. | ||
| val threadIds = ids.mapNotNull { messageRepo.getUnmanagedMessage(it)?.threadId }.distinct() | ||
|
|
||
| if (read != 0) messageRepo.markRead(threadIds) else messageRepo.markUnread(threadIds) | ||
| conversationRepo.updateConversations(threadIds) | ||
| return ids.size |
There was a problem hiding this comment.
Suggestion: update() returns the requested ID count, not the number of rows actually modified; nonexistent IDs still report success, which breaks ContentProvider row-count semantics and can mislead callers into thinking updates were applied. Return the real affected-row count based on existing/resolved messages. [api mismatch]
Severity Level: Major ⚠️
⚠️ Callers misinterpret update success, masking stale IDs.
⚠️ Bridge status checks unreliable for failed read/unread.Steps of Reproduction ✅
1. Invoke the read/unread bridge as documented in `ADB_BRIDGE.md:59-64`, but with an ID
that does not exist, e.g. `adb shell content update --uri
content://dev.octoshrimpy.quik.adb/messages/999999 --bind read:i:1`. This enters
`AdbMessagesProvider.update()` at
`presentation/src/main/java/com/moez/QKSMS/adb/AdbMessagesProvider.kt:201-223`.
2. `update()` resolves the IDs from either the URI or the selection at lines 210-213; for
`/messages/999999`, it sets `ids = listOf(ContentUris.parseId(uri))`, yielding a single ID
`999999L`.
3. It then constructs `threadIds` via `ids.mapNotNull {
messageRepo.getUnmanagedMessage(it)?.threadId }.distinct()` at line 218. Since
`messageRepo.getUnmanagedMessage(999999L)` (implemented in
`MessageRepositoryImpl.kt:13-16`) returns `null` when the message is missing, `threadIds`
becomes an empty list and `messageRepo.markRead(threadIds)` / `markUnread(threadIds)` at
line 220 perform no state changes.
4. Despite no rows being modified, `update()` returns `ids.size` at line 222 (in this case
`1`), violating ContentProvider semantics by reporting success based on requested IDs
rather than actual affected messages, and misleading callers that rely on the return count
to detect stale or nonexistent IDs.(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** presentation/src/main/java/com/moez/QKSMS/adb/AdbMessagesProvider.kt
**Line:** 215:222
**Comment:**
*Api Mismatch: `update()` returns the requested ID count, not the number of rows actually modified; nonexistent IDs still report success, which breaks ContentProvider row-count semantics and can mislead callers into thinking updates were applied. Return the real affected-row count based on existing/resolved messages.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| private fun parseIdsFromSelection(selection: String?): List<Long> = | ||
| selection?.let { Regex("\\d+").findAll(it).map { m -> m.value.toLong() }.toList() } ?: emptyList() |
There was a problem hiding this comment.
Suggestion: The ID parser extracts every digit sequence from the full selection string, so unrelated numbers (phone numbers, timestamps, other predicates) are treated as message IDs and can cause unintended delete/update of wrong rows. Parse only validated id predicates (or use structured selectionArgs) instead of global \\d+ scraping. [incorrect variable usage]
Severity Level: Critical 🚨
❌ Wrong SMS messages deleted due to misparsed selection.
⚠️ Threads marked read/unread incorrectly from complex predicates.Steps of Reproduction ✅
1. Follow the documented delete interface in `ADB_BRIDGE.md:49-57` but include additional
numeric predicates, e.g. `adb shell content delete --uri
content://dev.octoshrimpy.quik.adb/messages --where "threadId = 42 AND date <
1234567890"`. This routes to `AdbMessagesProvider.delete()` in
`presentation/src/main/java/com/moez/QKSMS/adb/AdbMessagesProvider.kt:179-199`.
2. For `/messages` URIs, `delete()` computes `ids` via `parseIdsFromSelection(selection)`
at lines 183-186, passing the entire selection string `threadId = 42 AND date <
1234567890`.
3. `parseIdsFromSelection()` at lines 232-233 uses `Regex("\\d+")` across the raw
selection and maps every digit sequence (`"42"`, `"1234567890"`) to `Long`s, without
checking that they belong to an `id` predicate; the resulting `ids` list therefore
contains `42L` and `1234567890L`.
4. `messageRepo.deleteMessages(ids)` at line 194 subsequently deletes messages whose IDs
match these unrelated numbers, and `conversationRepo.updateConversations(threadIds)` at
line 195 refreshes threads accordingly, leading to unintended deletion or state changes in
messages whose IDs happen to equal thread IDs, timestamps, or other numeric values
embedded in complex where clauses.(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** presentation/src/main/java/com/moez/QKSMS/adb/AdbMessagesProvider.kt
**Line:** 232:233
**Comment:**
*Incorrect Variable Usage: The ID parser extracts every digit sequence from the full `selection` string, so unrelated numbers (phone numbers, timestamps, other predicates) are treated as message IDs and can cause unintended delete/update of wrong rows. Parse only validated `id` predicates (or use structured `selectionArgs`) instead of global `\\d+` scraping.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fixf101381 to
b4d11a9
Compare
Add AdbMessagesProvider, a ContentProvider folded into QUIK that relays
read / send / delete / mark-read over `adb shell content`, reusing QUIK's
own MessageRepository/ConversationRepository. Because QUIK is the permanent
default SMS app it already holds full read+write rights on the system SMS
provider, so no default-role juggling is needed (unlike a standalone adbsms
helper, which seizes the role and disables normal messaging while active).
Authority `<applicationId>.adb`. Endpoints: /messages, /messages/#,
/conversations. Usage lives in the provider's KDoc header.
Security: the provider must stay exported (shell is a separate uid, so adb
can only reach an exported provider), which would otherwise let any app
read/send/delete SMS with no permission of its own. Every call is gated
fail-closed by Binder.getCallingUid() (kernel-supplied, unspoofable): only
QUIK's own uid and the shell uid (adb; already gated by USB debugging + an
authorized host) may call; everything else is rejected with
SecurityException. This is an adb-only bridge by design — deliberately not
exposed to other apps (on-device automation belongs inside QUIK as a
feature, not behind an app-facing provider). No URL token, which would
leak into logcat/dumpsys.
Verified E2E on a physical device (Galaxy S24 Ultra, Android 17), QUIK as
default SMS app: read returned the real message store; delete removed a row
from both Realm and content://sms/{id}; send produced a real SMS (self
round-trip: sent + received rows, also in content://sms/sent); after
reinstall the shell uid still reads through the caller gate. Deletes only
reach the system provider once the default-SMS role is settled (a role flip
immediately before a delete no-ops silently — a test-harness artifact, not
a concern when QUIK is the standing default).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
b4d11a9 to
fb67883
Compare
Daily (and on-demand) job that merges octoshrimpy/quik master into this fork's master, carrying the fork's additive changes (the adb SMS bridge). The bridge is a new file + two tiny additive edits, so the merge is normally conflict-free; on a conflict the job aborts and opens an issue. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Adds
AdbMessagesProvider— aContentProviderfolded into QUIK that relays read / send / delete / mark-read overadb shell contentand on-device automation, reusing QUIK's ownMessageRepository/ConversationRepository.Why this shape
Deleting/writing SMS on Android is allowed only for the default SMS app (OS-enforced). Because QUIK is your permanent default messenger, it already holds those rights at all times — so this bridge needs no default-role juggling and normal texting keeps working. (A standalone helper like
adbsmshas to seize the default role, which disables messaging while active.)Docs live in the provider's KDoc header; the usage + security summary is inlined below.
Interface
Authority
${applicationId}.adb—content://dev.octoshrimpy.quik.adb(release),...debug.adb(debug). Swap to match your build.Read (
_id, id, threadId, contentId, address, body, date, dateSent, read, seen, type, boxId, subId, locked):Send (real SMS via QUIK's send path; optional
--bind subId:i:<n>):adb shell content insert --uri content://dev.octoshrimpy.quik.adb/messages \ --bind address:s:+821012345678 --bind body:s:hello_from_adbDelete (removed from Realm and the system SMS provider):
adb shell content delete --uri content://dev.octoshrimpy.quik.adb/messages/137 adb shell content delete --uri content://dev.octoshrimpy.quik.adb/messages --where "id IN (1,2,3)"Mark read / unread (per-thread, matching the repository):
Security
The provider must stay
exported(theshelluid is a separate process, so adb can only reach an exported provider). Left unguarded that would let any installed app read/send/delete SMS with no permission of its own — a route to leak OTPs or send SMS silently. So every call is gated fail-closed byBinder.getCallingUid()(kernel-supplied, unspoofable), uniformly across read/send/delete/mark. Only two callers are allowed:shell(uid 2000), soadbworks — already gated by USB debugging + an authorized host.Every other caller is rejected with
SecurityException. This is an adb-only bridge by design — it is deliberately not exposed to other apps (on-device automation belongs inside QUIK as a feature, not behind an app-facing provider). There is no URL token either — a query-parameter token leaks into logcat/dumpsys, and the uid gate is stronger and uniform.Verified E2E
Physical Galaxy S24 Ultra (Android 17), QUIK set as default SMS app:
content://sms/{id}("No result found" after).content://sms/sent).shelluid still reads through the gate (noSecurityException, real data returned).Note: system-provider deletes only land when the default-SMS role is settled; flipping the role immediately before a delete no-ops silently (test-harness artifact — not a concern when QUIK is the standing default).
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Chores