Skip to content

Release 8.2.0 - Inbox v2 sync#1008

Merged
piyush-kukadiya merged 2 commits into
developfrom
task/release/core/8.2.0
May 19, 2026
Merged

Release 8.2.0 - Inbox v2 sync#1008
piyush-kukadiya merged 2 commits into
developfrom
task/release/core/8.2.0

Conversation

@piyush-kukadiya
Copy link
Copy Markdown
Contributor

No description provided.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 19, 2026

Warning

Rate limit exceeded

@piyush-kukadiya has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 51 minutes and 1 second before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 52ad0adf-5d6f-4167-a750-5950faf82ba1

📥 Commits

Reviewing files that changed from the base of the PR and between bfe7d50 and e2b5471.

📒 Files selected for processing (75)
  • CHANGELOG.md
  • README.md
  • clevertap-core/build.gradle
  • clevertap-core/src/main/java/com/clevertap/android/sdk/ActivityLifeCycleManager.java
  • clevertap-core/src/main/java/com/clevertap/android/sdk/AnalyticsManager.java
  • clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapAPI.java
  • clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/Constants.java
  • clevertap-core/src/main/java/com/clevertap/android/sdk/ControllerManager.java
  • clevertap-core/src/main/java/com/clevertap/android/sdk/CoreState.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/FetchInboxCallback.java
  • clevertap-core/src/main/java/com/clevertap/android/sdk/db/CtDatabase.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/db/DBAdapter.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/db/dao/InboxMessageDAO.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/db/dao/InboxMessageDAOImpl.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/db/dao/InboxPendingActionsDAO.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxController.java
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTInboxListViewFragment.java
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/CTMessageDAO.java
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/EventSuppressor.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/InboxDeleteCoordinator.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/InboxIndexState.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/InboxMessageSource.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/InboxV2Bridge.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/InboxV2Fetcher.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/inbox/InboxV2Merger.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/login/LoginController.java
  • clevertap-core/src/main/java/com/clevertap/android/sdk/network/api/CtApi.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/network/fetch/CallResult.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/network/fetch/EndpointCall.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/network/fetch/EventRequestBody.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/network/fetch/FetchThrottle.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/network/fetch/FetchTrigger.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/network/fetch/InboxDeleteCall.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/network/fetch/InboxFetchCall.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/network/fetch/InboxV2EventBuilder.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/network/fetch/NetworkScope.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/response/InboxV2DeliverySource.kt
  • clevertap-core/src/main/java/com/clevertap/android/sdk/response/InboxV2Response.kt
  • clevertap-core/src/main/res/layout/inbox_list_view.xml
  • clevertap-core/src/test/java/com/clevertap/android/sdk/ActivityLifeCycleManagerTest.kt
  • clevertap-core/src/test/java/com/clevertap/android/sdk/AnalyticsManagerTest.kt
  • clevertap-core/src/test/java/com/clevertap/android/sdk/CleverTapAPITest.kt
  • clevertap-core/src/test/java/com/clevertap/android/sdk/ConstantsTest.kt
  • clevertap-core/src/test/java/com/clevertap/android/sdk/MockCoreState.kt
  • clevertap-core/src/test/java/com/clevertap/android/sdk/db/DBAdapterTest.kt
  • clevertap-core/src/test/java/com/clevertap/android/sdk/db/DatabaseHelperMigrationTest.kt
  • clevertap-core/src/test/java/com/clevertap/android/sdk/db/dao/InboxMessageDAOImplTest.kt
  • clevertap-core/src/test/java/com/clevertap/android/sdk/db/dao/InboxPendingActionsDAOImplTest.kt
  • clevertap-core/src/test/java/com/clevertap/android/sdk/inbox/CTInboxControllerTest.kt
  • clevertap-core/src/test/java/com/clevertap/android/sdk/inbox/CTMessageDAOTest.kt
  • clevertap-core/src/test/java/com/clevertap/android/sdk/inbox/EventSuppressorTest.kt
  • clevertap-core/src/test/java/com/clevertap/android/sdk/inbox/InboxDeleteCoordinatorTest.kt
  • clevertap-core/src/test/java/com/clevertap/android/sdk/inbox/InboxV2BridgeTest.kt
  • clevertap-core/src/test/java/com/clevertap/android/sdk/inbox/InboxV2FetcherTest.kt
  • clevertap-core/src/test/java/com/clevertap/android/sdk/inbox/InboxV2MergerTest.kt
  • clevertap-core/src/test/java/com/clevertap/android/sdk/login/LoginControllerTest.kt
  • clevertap-core/src/test/java/com/clevertap/android/sdk/network/api/CtApiInboxFetchTest.kt
  • clevertap-core/src/test/java/com/clevertap/android/sdk/network/fetch/CallResultTest.kt
  • clevertap-core/src/test/java/com/clevertap/android/sdk/network/fetch/EventRequestBodyTest.kt
  • clevertap-core/src/test/java/com/clevertap/android/sdk/network/fetch/FetchThrottleTest.kt
  • clevertap-core/src/test/java/com/clevertap/android/sdk/network/fetch/InboxDeleteCallTest.kt
  • clevertap-core/src/test/java/com/clevertap/android/sdk/network/fetch/InboxFetchCallTest.kt
  • clevertap-core/src/test/java/com/clevertap/android/sdk/network/fetch/InboxV2EventBuilderTest.kt
  • clevertap-core/src/test/java/com/clevertap/android/sdk/network/fetch/NetworkScopeTest.kt
  • clevertap-core/src/test/java/com/clevertap/android/sdk/network/http/MockHttpClient.kt
  • clevertap-core/src/test/java/com/clevertap/android/sdk/response/InboxV2ResponseTest.kt
  • docs/CTCORECHANGELOG.md
  • docs/CTGEOFENCE.md
  • docs/CTPUSHTEMPLATES.md
  • docs/EXAMPLES.md
  • gradle/libs.versions.toml
  • sample/build.gradle
  • templates/CTCORECHANGELOG.md
  • templates/EXAMPLES.md
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task/release/core/8.2.0

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.

@francispereira
Copy link
Copy Markdown

francispereira commented May 19, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@piyush-kukadiya
Copy link
Copy Markdown
Contributor Author

@coderabbitai update PR description

* task(SDK-5709): add Inbox V2 constants (T0.1)

Adds the 5 shared constants (fetch type, response keys, prefs key, throttle
window) that every Inbox V2 task reads from, so literal strings stay in one
place and can't silently drift across files.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): add sendInboxFetch and sendInboxDelete to CtApi (T0.2)

Routes the V2 inbox to its dedicated endpoint so fetch/delete don't get
batched into /a1 alongside regular events. Also teaches getUriForPath to
split multi-segment relativeUrl on '/' before appending so paths like
inbox/v2/getMessages aren't URL-encoded into a single %2F-laden segment;
single-segment callers are unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): parse isRead from V2 inbox JSON in CTMessageDAO (T0.3)

Reads the new isRead boolean from V2 responses so cross-device read state
survives into the DAO. Missing field defaults to unread, preserving V1
behaviour unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): add CallResult sealed class for V2 network calls (DF1)

Unifies the five possible outcomes of any single-shot V2 network call
(success, throttled, disabled, HTTP error, transport failure) so callers
pattern-match once and the compiler enforces exhaustive handling.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): add EndpointCall interface for V2 network calls (DF2)

Defines the single-method contract that every V2 endpoint implements, so
orchestration, error mapping, and testing all plug into one shape regardless
of whether the call is a fetch, delete, or future event push.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): add NetworkScope coroutine owner per SDK instance (DF3)

Introduces one CoroutineScope per CleverTap instance, backed by a
SupervisorJob and an injectable dispatcher (default Dispatchers.IO), so V2
network coroutines can be launched with structured lifetimes and isolated
failures without blocking the existing CTExecutorFactory threads.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): add EventRequestBody helper for V2 call payloads (DF4)

Centralizes the [header, event] JSON array layout every direct V2 call
needs, so each EndpointCall builds only its own event object and never
hand-rolls the surrounding array.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): add FetchThrottle generic per-account rate limiter (DF5)

Adds a reusable persistent throttle keyed by account id and pref name, so
V2 fetch callers (public fetchInbox and pull-to-refresh) enforce the 5-min
window across app restarts without baking feature-specific state into the
class. Uses the existing Clock interface and TestClock for determinism.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): add InboxFetchCall — first V2 EndpointCall implementor (DF6)

Wires the V2 inbox fetch end-to-end: builds the wzrk_fetch event, wraps it
in EventRequestBody, hits sendInboxFetch on the injected dispatcher, and
maps each HTTP outcome to a CallResult (200→Success, 403→Disabled, else
→HttpError, empty/parse/IO→NetworkFailure). Sets the per-endpoint error
matrix pattern every future EndpointCall will follow.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): add InboxV2Merger pure dual-filter functions (T1.4b)

Extracts the V2 response merge math into a zero-dependency Kotlin object so
the controller method can orchestrate DB and lock while the filter logic
stays unit-testable with no mocks. Single predicate definition is shared by
the pre-write and post-read passes so they can't drift.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): add processV2Response orchestration to CTInboxController (T1.4)

Threads the InboxV2Merger dual-filter through the controller's DB and lock:
pre-write filter → upsert → re-read under messagesLock → post-read cleanup
→ batch delete of stale rows → in-memory swap. Returns whether anything
changed so the response handler can decide when to fire the UI callback,
matching the V1 updateMessages contract. Pending sets are empty today; T3.3
wires them to the pending_deletes/pending_reads tables.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): add InboxV2Response mirroring V1 guards (T1.4a)

Routes V2 fetch payloads through the same four safety checks V1 uses
(analytics-only, key presence, parse try/catch, null-controller init) so
the active-fetch path can't bypass guards the passive path enforces.
Bridges to CTInboxController.processV2Response and fires
inboxMessagesDidUpdate on real changes.

Opens the minimum accessors the cross-package response handler needs:
CTMessageDAO.initWithJSON becomes public and CTInboxController gains a
getUserId() getter. Both classes remain @RestrictTo(LIBRARY) so consumer
visibility is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): add InboxV2Fetcher orchestrator (DF7)

Puts the inbox-specific glue between the generic EndpointCall and the
response handler: session-scoped disable flag, throttle gate (honoured for
user-initiated triggers, bypassed for app-launch and onUserLogin),
recordFetch on the allowed path, and hand-off to InboxV2Response on
success so the lock, controller-init, and UI callback stay where T1.4a
owns them.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): add InboxV2Bridge Java-interop adapter and wire the V2 chain (DF8)

Lets Java callers fire-and-forget an Inbox V2 fetch: the bridge launches
the suspend fetcher on NetworkScope and delivers success/failure to an
optional FetchInboxCallback. The factory now constructs the full
InboxV2Response → InboxFetchCall → FetchThrottle → InboxV2Fetcher →
InboxV2Bridge chain once per SDK instance and exposes the bridge through
CoreState so the trigger wirings (T1.5/T1.6/T1.7) become one-liners.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): fetch Inbox V2 on cold app launch (T1.5)

Drops a single bridge.submit(false, null) into ActivityLifeCycleManager's
cold-launch block so the inbox is actively pulled once per app launch
without a throttle. The factory now constructs the V2 chain before the
lifecycle manager so the bridge is ready at wire time; activityLifeCycle-
Manager and loginController keep their original relative order.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): fetch Inbox V2 after onUserLogin identity switch (T1.6)

Hooks InboxV2Bridge.submit(false, null) into asyncProfileSwitchUser right
after resetInbox() so the new identity's inbox loads immediately from the
server instead of waiting on the next /a1 push. Throttle is bypassed —
every identity switch is a fresh fetch.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): expose public fetchInbox() API on CleverTapAPI (T1.7)

Gives customers with custom inbox UIs a way to force an on-demand refresh
from the CleverTap servers. Two overloads (no-arg and callback-taking)
route through InboxV2Bridge.submit with respectThrottle=true, so rapid
user-initiated calls can't spam the endpoint. Null inbox controller is
handled defensively: the callback fires with false, no network call.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): pull-to-refresh on built-in inbox via V2 fetch (T1.9)

Wraps CTInboxListViewFragment's list in a SwipeRefreshLayout and routes the
refresh gesture through the public fetchInbox API so the throttle applies
and existing inboxMessagesDidUpdate path drives the adapter refresh. The
outer wrapper delegates canChildScrollUp to whichever RecyclerView is
actually on screen — mid-scroll upward drags scroll the list instead of
falsely firing a refresh, which the default mTarget.canScrollVertically(-1)
check on a LinearLayout target would otherwise do.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): add message _id to Viewed and Clicked inbox events (T2.1)

Inserts data.getMessageId() under evtData._id in pushInboxMessageStateEvent
so backend can map Viewed/Clicked events to a specific inbox message and
update server-side isRead. Without this field, cross-device read-state
sync has no way to land.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): suppress rapid repeat inbox events on the same device (T2.2)

Introduces a generic per-key sliding-window EventSuppressor backed by
ConcurrentHashMap.put so two suppressors (Viewed 2s, Clicked 5s) can gate
pushInboxMessageStateEvent without new locks. Closes the public-API dedup
gap: customers building custom inboxes can't inflate analytics by firing
Viewed on every scroll-into-view or double-tapping a message.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): skip Viewed events for already-read inbox messages (T2.3)

Adds an early-return guard at the top of pushInboxMessageStateEvent: if
the message is already read (V2 delivered isRead=true from another
device), suppress the Viewed event to avoid cross-device duplicate view
counts. Clicked semantics are unchanged — distinct taps on separate
devices remain distinct engagement events.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): add pending-actions tables and DAO for offline resilience (T3.1)

Adds two SQLite tables — inbox_pending_deletes and inbox_pending_reads —
keyed by composite (USER_ID, ID) so a local delete/read intent survives
app kill and a later V2 fetch can't resurrect a deleted message or
overwrite a locally-read message until the server confirms the action.
The DB migrates from v6 to v7 additively (no alteration of inboxMessages).
The DAO mirrors existing conventions: belowMemThreshold guard, transaction-
wrapped batch insert with CONFLICT_IGNORE, parameterized IN batch delete,
cursor.use for reads, SQLiteException catches on writes.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): wire pending-reads into V2 fetch path (T3.3)

Records a row in inbox_pending_reads whenever the user marks a message
read, so a V2 fetch with a stale isRead=false can't un-read the message
across app restart. processV2Response now reads both pending sets for
real (replacing the T1.4 empty-set placeholder) and, once a server echo
confirms a pending read, batch-removes the row. The cleanup runs BEFORE
preWriteFilter because the merger's override mutates incoming DAOs in
place and would otherwise make every pending id look server-confirmed.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): sync inbox deletes to the server with retry on init (T3.2)

Adds an action-only EndpointCall<Unit> (InboxDeleteCall) and a coordinator
that batches N deletes into one HTTP call, clears inbox_pending_deletes
rows atomically on 2xx, and drains leftover pending rows at inbox-init
time so a delete that failed offline eventually lands. Local intent is
recorded before the server call so an app kill mid-sync can't lose it.

Three literals are captured behind working assumptions, each flippable
with a one-line edit once backend confirms: delete URL path, event name
("Message Deleted"), and the messages array container key.

DBAdapter is fetched via a supplier so the coordinator's first DB load
happens on the NetworkScope's IO dispatcher instead of the factory's
main-thread construction path.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): make FetchThrottle in-memory (T4.1)

Cross-session persistence was redundant. Every process start runs an
un-throttled V2 fetch (cold launch and onUserLogin submit with
respectThrottle=false), and InboxV2Fetcher calls recordFetch() before
the endpoint runs regardless of the caller. The stored timestamp is
therefore overwritten before any throttled caller can read it, so a
per-instance AtomicLong yields identical observable behavior without
the disk I/O, Context, config, and prefKey plumbing.

Drops INBOX_V2_LAST_FETCH_TS_KEY, removes Robolectric from
FetchThrottleTest, and simplifies construction in CleverTapFactory.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): tag inbox messages with V1/V2 source (T5.1)

V1 and V2 inbox messages live in the same `inboxMessages` table but have
divergent backend contracts: V2 events must carry `_id` and V2 deletes
must sync to the v2 endpoint, while V1 must do neither. The new
`InboxMessageSource` discriminator persists per-message on the DAO and a
`source TEXT NOT NULL DEFAULT 'V1'` column (folded into the existing v7
migration — no version bump, branch is unreleased). `InboxResponse`
tags parsed DAOs V1, `InboxV2Response` tags V2; `CTInboxController`
exposes an `isV2Message(id)` helper so the gate sites in T5.2 / T5.3
can branch without reading source off the public `CTInboxMessage`.

`CTInboxMessage` is intentionally untouched and `CTMessageDAO.toJSON()`
does not include source, so the V1/V2 tag never reaches the public
model via `getData()`. A regression test in `CTMessageDAOTest` asserts
the JSON stays clean.

No behavioral change yet; T5.2 and T5.3 consume the tag.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): include message _id only for V2 inbox events (T5.2)

Backend rejects `_id` on Notification Viewed / Clicked events for V1
inbox messages. Gate the `_id` put in
`AnalyticsManager.pushInboxMessageStateEvent()` on the T5.1 source tag
via a private `isV2InboxMessage(msgId)` helper that consults
`CTInboxController.isV2Message`. Null controller, null message id, and
unknown ids all fall through to V1 behavior (no `_id` emitted).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): gate delete sync and pending tables on V2 source (T5.3)

Backend does not handle V1 deletes, and V1 inbox messages have no
server-side state to sync. Four sites in `CTInboxController` now gate
on the T5.1 source tag:

- `deleteInboxMessage` / `deleteInboxMessagesForIDs` only add to
  `inbox_pending_deletes` and invoke `InboxDeleteCoordinator.syncDelete`
  for V2 messages. V1 paths are local-only.
- `_markReadForMessageWithId` / `_markReadForMessagesWithIds` only add
  to `inbox_pending_reads` for V2 messages. V1 markRead stays local.

Batch paths (`deleteInboxMessagesForIDs`, `_markReadForMessagesWithIds`)
iterate `messages` once under a single `messagesLock` with an id-set
membership check — O(N+M) instead of O(N*M) per-id scans. Single-item
paths read source directly off the DAO returned by `findMessageById`.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): QA polish — V2 event metadata fields and sync controller init

- Stamp standard event metadata (s, pg, ep, f, lsl, pai, n) onto every
  V2 endpoint event body via a new buildInboxV2Event helper, so the V2
  fetch and Message Deleted events match the shape of regular events
  the server expects.
- Add ControllerManager.initializeInboxSync() and have InboxV2Response
  call it instead of the async initializeInbox(), so the very first V2
  response on cold launch / post-login is no longer silently dropped
  while the controller is still initialising.
- Verbose logging across the V2 call, fetcher, and coordinator paths to
  make the QA flow auditable.
- Tests updated/added for all of the above.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): rename V2 inbox event id key from _id to wzrk_mid

Backend updated the contract for V2 inbox Notification Viewed and
Notification Clicked events: the message identifier now travels under
the key wzrk_mid instead of _id. Plain key rename — V1 messages still
emit no id field; V2 gating in isV2InboxMessage() unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): align V2 inbox delete with confirmed backend contract

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): persist full delete payload per pending row (T6.1)

When a delete-sync fails, retryPending re-sends the deletes on the next
inbox init. The retry payload was id-only — the local message was gone,
so wzrk_id / wzrk_pivot / etc. weren't carried, and backend couldn't
correlate the retried delete with the original campaign attribution.

Adds a wzrkParams TEXT column to inbox_pending_deletes (folded into the
unreleased v7 migration; no DATABASE_VERSION bump). The DAO now exposes
two reads — getPendingDeleteIds(userId) for the merger's set-membership
filter and getPendingDeletes(userId) returning a typed List<PendingDelete>
for retry reconstruction. Single and batch insert methods take a
wzrkParams JSONObject? alongside the id; addPendingDeletes operates on
List<PendingDelete> so each row carries its own params.

CTInboxController forwards dao.getWzrkParams() at delete time on both
the single-message and batch-IDs paths, all within the existing single
pass under messagesLock so source resolution still happens before the
local cache wipe. InboxDeleteCoordinator.retryPending rebuilds each
CTInboxMessage with id + wzrkParams so the retry POST body matches the
initial sync body byte-for-byte. The merger is unchanged — it keeps
reading getPendingDeleteIds for filtering.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): two-phase pending delete with TTL-driven cleanup (T6.2)

Backend confirmed in QA that the V2 delete API has a 1-2 hour
delete-propagation window: a 200 from /inbox/v2/deleteMessages only
acknowledges the request was accepted; the message can still come back
in V2 fetch responses for a couple of hours, and absence from a fetch
is also non-authoritative. Treating 200 as "done" therefore lets a
follow-up fetch resurrect the locally-deleted message into cache.

Promotes inbox_pending_deletes through a two-state machine. New rows
default to PENDING_SEND. On HTTP 200 the coordinator transitions the
targeted rows to AWAITING_CONFIRM via markPendingDeletesAwaitingConfirm
instead of deleting them, so InboxV2Merger keeps filtering the message
out of incoming fetches via getPendingDeleteIds (both states). retryPending
reads only PENDING_SEND rows.

Final cleanup is TTL-driven: each pending row stores the message's
wzrk_ttl as expires (folded into the v7 schema, no DATABASE_VERSION
bump). When a V2 message lacks a usable TTL, CTInboxController falls
back to now + 1 day via resolvePendingDeleteExpiry — comfortably past
the propagation window without lingering forever. removeExpiredAwaitingConfirm
sweeps state=AWAITING_CONFIRM AND expires<=now; it runs at the top of
processV2Response (before reading the pending-delete id set so an
expired row's id stops filtering the now-stale message) and at the
start of retryPending (so devices that rarely fetch still self-clean).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): wire InboxV2Response into /a1 decorator chain (T7.1)

Live-behaviour V2 inbox campaigns deliver inbox_notifs_v2 inside the
/a1 event response. The decorator chain only knew about the V1
InboxResponse, so V2 payloads riding /a1 were silently dropped and
live campaigns never reached the controller.

InboxV2Response now extends CleverTapResponseDecorator with a 3-arg
override that delegates to the existing single-arg processResponse,
so the same parser handles both the direct fetch path and the /a1
decorator path. CleverTapFactory builds the instance once and splices
it into cleverTapResponses next to InboxResponse; the V2 fetch
pipeline keeps using the same instance.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): TTL-driven cleanup of pending_reads (T7.2)

A pending_read row is cleared by the existing server-caught-up loop
inside processV2Response — but only when the server eventually echoes
the message back with isRead=1. Several paths starve that echo: user
deletes the message after marking read, message TTL elapses on the
backend, campaign retargeted, identity switch, etc. The row strands
forever, overriding isRead on any future incoming message that
happens to share the id.

Mirroring the T6.2 pattern for pending_deletes:

  - inbox_pending_reads gains an `expires` column (folded into v7,
    no DATABASE_VERSION bump on this unreleased branch).
  - Captures wzrk_ttl at markRead time via the shared
    resolvePendingActionExpiry helper (renamed from
    resolvePendingDeleteExpiry now that both paths use it). 1-day
    fallback when the DAO has no usable TTL.
  - DAO grows addPendingRead(messageId, userId, expiresAt), batch
    addPendingReads(rows, userId), and removeExpiredPendingReads
    sweep. DBAdapter facade updated.
  - processV2Response sweeps expired pending_reads next to the
    existing AWAITING_CONFIRM sweep, before reading the pending sets.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): use controller-fresh DAO for inbox Viewed event

CTInboxBaseMessageViewHolder.markItemAsRead synchronously mutates
inboxMessage.setRead(true) on the UI thread before the async
messageDidShow body runs. By the time pushInboxMessageStateEvent
inspects data.isRead(), the UI mirror already reads true, which
trips the T2.3 cross-device gate (skip Viewed if isRead) and drops
the legitimate first Viewed event for the local mark.

Switch the analytics path to use the freshly-fetched `message` from
getInboxMessageForId (controller-backed, still read=false) and fire
the event before markReadInboxMessage flips the controller state.
Same external behavior, but the gate now sees the correct pre-mark
state and the Viewed event reliably emits.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* task(SDK-5709): cross-device delete sync via index_state sweep (T7.3)

Adds index_state column (PENDING_INDEXING | INDEXED) to inboxMessages.
After each V2 fetch, incoming ids are bulk-promoted to INDEXED; V2 messages
that are INDEXED (or stale PENDING_INDEXING older than the 6 h grace window)
but absent from the fetch response are swept locally as cross-device deletes.
Fresh PENDING_INDEXING rows are never swept, preventing false-positive removal
of live-behaviour messages not yet indexed by the fetch backend.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* task(SDK-5709): wire FETCH sweep into processV2Response (T7.3 complete)

Completes the cross-device delete sync feature started in the previous
T7.3 commit (which added the DAO layer). Adds the controller-level sweep
logic that runs on every V2 fetch response to remove locally-cached V2
messages that have been deleted on another device.

Key changes:
- InboxV2DeliverySource enum (FETCH | A1) disambiguates how a V2 payload
  arrived; the sweep is FETCH-only because only a fetch is a complete
  authoritative inbox snapshot.
- InboxMessageDAO/Impl: findSweepableV2Ids(userId, staleCutoff) returns
  ids of INDEXED V2 rows and stale PENDING_INDEXING rows (older than the
  6 h grace window) — both treated as reliable cross-device delete signals.
- DBAdapter: wrappers for markIndexed and findSweepableV2Ids.
- CTInboxController.processV2Response gains an InboxV2DeliverySource param
  and INDEXING_GRACE_SECONDS = 6 h constant. FETCH path: (1) bulk-promotes
  incoming ids to INDEXED, (2) sweeps absent sweepable ids from DB before
  postReadCleanup re-reads the full table.
- InboxV2Response routes FETCH vs A1 source through to the controller.
- 16 new tests across three test classes (DAO, controller, response).

Co-authored-by: Cursor <[email protected]>

* fix(SDK-5709): infinite-TTL guard, sweep verbose logs, and PBS INDEXED fix

- Infinite TTL (ttl=0): resolvePendingActionExpiry now returns 0 (never
  expires) when the message TTL is 0, preventing premature expiry of
  fire-and-forget LBS non-persistent messages. DAO queries for
  removeExpiredAwaitingConfirm and removeExpiredPendingReads now include
  `expires > 0` so infinite-TTL rows are never purged. findSweepableV2Ids
  adds `expires != 0` to exclude infinite-TTL messages from cross-device
  sweep.

- Sweep verbose logging: processV2Response now emits verbose logcat lines
  for markIndexed, sweep removals (or nothing-to-remove), and expired
  AWAITING_CONFIRM / pending-read row cleanup, enabling dev QA verification
  without a debugger.

- PBS INDEXED bug fix: brand-new PBS rows delivered for the first time via
  FETCH were stored as PENDING_INDEXING because markIndexed ran before the
  row existed in the DB. processV2Response(FETCH) now stamps all toUpsert
  DAOs with indexState=INDEXED before calling upsertMessages, so the INSERT
  writes INDEXED directly. The ON CONFLICT clause intentionally omits
  index_state, so existing INDEXED rows are unaffected. Fixes PP-001 and
  PP-006 (cross-device sweep was blind until the second FETCH).

Tests added: infinite-TTL guards in InboxMessageDAOImplTest,
InboxPendingActionsDAOImplTest, and CTInboxControllerTest; regression test
for the PBS INDEXED fix in CTInboxControllerTest.

* fix(SDK-5709): record throttle only on Success/HttpError, not NetworkFailure

A transport failure (timeout, no network) no longer stamps the 5-minute
throttle window, so a pull-to-refresh or fetchInbox() retry is never
silently swallowed after a failure where no server contact was made.
HttpError still records the fetch — the server responded, so the
throttle applies.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(SDK-5709): fix linting

* fix(SDK-5709): dispatch fetchInbox failure callback on network thread

The not-initialized early-exit in fetchInbox() was calling the callback
synchronously on the caller's thread, violating the documented contract
that callbacks fire on the SDK's network dispatcher. Routes it through
InboxV2Bridge.submitFailure() so all callback paths use the same thread.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* refactor(SDK-5709): make EndpointCall a fun interface

Single-abstract-method contract is now explicit in the type.
Verified that fun interface with a suspend abstract method compiles
on Kotlin 2.0.10.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* refactor(SDK-5709): remove dead networkScope and inboxDeleteCoordinator from CoreState

Both fields were stored on CoreState but never accessed via it — each
consumer already held its own injected reference (InboxV2Bridge and
InboxDeleteCoordinator via constructor params, ControllerManager via
setter). Removing the redundant fields keeps CoreState as a registry of
things actually needed through it.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* task(SDK-5709): inject Clock into CTInboxController for testable time

Replace raw System.currentTimeMillis() calls with Clock.currentTimeSeconds()
via constructor injection; existing callers get Clock.SYSTEM through the
delegating constructor.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix(SDK-5709): replace Logger with ILogger

* fix(SDK-5709): fix failing tests

* task(SDK-5709): replace AtomicLong with @volatile for FetchThrottle timestamp

Plain read in shouldThrottle and plain write in recordFetch need only
volatile visibility, not atomic compound ops — @volatile is sufficient.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* test(SDK-5709): add DBAdapter V2 method tests and InboxV2Bridge submitFailure tests. Cover markIndexed, findSweepableV2Ids, all pending-delete/read CRUD methods, TTL-expiry sweeps, batch operations, and null/empty guards in DBAdapterTest. Add submitFailure happy-path and scope-cancellation tests in InboxV2BridgeTest.

* fix(SDK-5709): when new user logs in, call fetch after arp is received

* fix(SDK-5709): throttle only user-initiated inbox fetches; introduce FetchTrigger enum
                                                                                                         System calls (app-launch, onUserLogin) no longer record the throttle timestamp, so they cannot block a subsequent pull-to-refresh or fetchInbox() call. Only USER_INITIATED calls both check and record the throttle. Replaces the boolean respectThrottle flag with an enum class FetchTrigger for type-safe, self-documenting call sites.

* inbox v2 pull-to-refresh disabled on 403 and restore V1 viewed event behaviour SDK-5709 (#1007)

* fix(SDK-5709): hide pull-to-refresh on V2 403 and restore V1 viewed event behaviour

  Two behavioural fixes for the Inbox V2 rollout:

  1. Pull-to-refresh (SwipeRefreshLayout) is now hidden — both eagerly on
     fragment creation and after the first fetch that returns 403 — so the
     gesture affordance disappears once the V2 endpoint is session-disabled.
     Exposes disabledForSession through InboxV2Fetcher → InboxV2Bridge →
     CleverTapAPI (@RestrictTo LIBRARY) for the fragment to query.

  2. The cross-device read gate ("skip Viewed if isRead=true") is now guarded
     by isV2InboxMessage(), restoring pre-feature behaviour for V1 messages.
     Clients upgrading with V2 disabled (403) continue to receive Viewed events
     for already-read V1 messages, avoiding a silent regression.

* fix(SDK-5709): disable swipe gesture only; setVisibility(GONE) hid the message list

* fix(SDK-5709): reset throttle window on user login and consider non 200 status code as disable fetch

* fix(SDK-5709): replace activity context with application context as per PR comment

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
Co-authored-by: Cursor <[email protected]>
@piyush-kukadiya piyush-kukadiya force-pushed the task/release/core/8.2.0 branch from ae9eaae to e84c05e Compare May 19, 2026 16:47
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 19, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Let me gather the PR details first before updating the description.


🏁 Script executed:

#!/bin/bash
# Get PR details
gh pr view 1008 --repo CleverTap/clevertap-android-sdk --json title,body,files,commits,labels,milestone

# Get list of changed files
gh pr diff 1008 --repo CleverTap/clevertap-android-sdk --name-only

Length of output: 46092


Now I have all the details. Let me compose and push a comprehensive PR description.


🏁 Script executed:

#!/bin/bash
gh pr edit 1008 --repo CleverTap/clevertap-android-sdk --body "## Summary

Implements **Inbox V2 sync** for the CleverTap Android SDK (release 8.2.0). This PR introduces an active-fetch pipeline for the V2 inbox endpoint, cross-device read/delete synchronisation, offline resilience via pending-action tables, a public \`fetchInbox()\` API, pull-to-refresh in the built-in inbox UI, and comprehensive analytics guards — all while keeping the existing V1 inbox behaviour unchanged.

Jira: **SDK-5709**

---

## Motivation

The existing inbox relied solely on passive delivery through the \`/a1\` event-response pipeline. V2 inbox requires:
- An **active fetch** to a dedicated endpoint (\`/inbox/v2/getMessages\`) so the inbox loads on cold launch, identity switch, and on-demand user refresh.
- **Cross-device sync** for read state and deletes, which must survive app restarts and backend propagation delays.
- A clear **V1/V2 discriminator** so new backend contracts (event keys, delete endpoint) are never applied to legacy messages.

---

## Changes

### 🔌 Network Fetch Layer (new)
| File | Description |
|---|---|
| \`CallResult.kt\` | Sealed class unifying 5 outcomes of a V2 network call: \`Success\`, \`Throttled\`, \`Disabled\`, \`HttpError\`, \`NetworkFailure\` |
| \`EndpointCall.kt\` | \`fun interface\` contract for all V2 endpoint implementors |
| \`NetworkScope.kt\` | Per-SDK-instance \`CoroutineScope\` backed by \`SupervisorJob\` + injectable \`Dispatchers.IO\` |
| \`EventRequestBody.kt\` | Centralises the \`[header, event]\` JSON array layout every V2 call needs |
| \`FetchThrottle.kt\` | In-memory \`@Volatile\` timestamp throttle; only \`USER_INITIATED\` calls check/record it |
| \`FetchTrigger.kt\` | Enum replacing the former \`respectThrottle\` boolean: \`APP_LAUNCH\`, \`USER_LOGIN\`, \`USER_INITIATED\` |
| \`InboxFetchCall.kt\` | First \`EndpointCall\` implementor — builds \`wzrk_fetch\` event, hits \`sendInboxFetch\`, maps HTTP outcomes |
| \`InboxDeleteCall.kt\` | Action-only \`EndpointCall<Unit>\` that batches pending deletes into one HTTP POST |
| \`InboxV2EventBuilder.kt\` | Stamps standard event metadata (\`s\`, \`pg\`, \`ep\`, \`f\`, \`lsl\`, \`pai\`, \`n\`) onto every V2 event body |
| \`CtApi.kt\` | Adds \`sendInboxFetch\` and \`sendInboxDelete\`; fixes \`getUriForPath\` multi-segment URL encoding |

### 📨 V2 Response Handling (new)
| File | Description |
|---|---|
| \`InboxV2Response.kt\` | Parses V2 fetch payload; extends \`CleverTapResponseDecorator\` so V2 messages delivered via \`/a1\` are also handled |
| \`InboxV2Merger.kt\` | Zero-dependency dual-filter object: \`preWriteFilter\` and \`postReadCleanup\` with shared predicate |
| \`InboxV2DeliverySource.kt\` | \`FETCH\` vs \`A1\` discriminator; cross-device delete sweep runs on \`FETCH\` only |

### 🔄 Inbox Orchestration (new)
| File | Description |
|---|---|
| \`InboxV2Fetcher.kt\` | Session-disable flag, throttle gate, \`recordFetch\`, hands off to \`InboxV2Response\` |
| \`InboxV2Bridge.kt\` | Java-interop adapter; launches suspend fetcher on \`NetworkScope\`, delivers to optional \`FetchInboxCallback\` |
| \`InboxDeleteCoordinator.kt\` | Batches N deletes per HTTP call; clears \`inbox_pending_deletes\` on 2xx; drains pending rows on inbox-init (\`retryPending\`) |

### 🗄️ Database (modified/new)
- **Schema migration v6 → v7** (\`CtDatabase.kt\`)
  - New table \`inbox_pending_deletes\`: \`(USER_ID, ID, wzrkParams, state, expires)\` — two-state machine (\`PENDING_SEND\` → \`AWAITING_CONFIRM\`); TTL-driven expiry after server 200
  - New table \`inbox_pending_reads\`: \`(USER_ID, ID, expires)\` — cleared when server echoes \`isRead=true\` or TTL elapses
  - New column \`source TEXT NOT NULL DEFAULT 'V1'\` on \`inboxMessages\` — V1/V2 discriminator
  - New column \`index_state\` on \`inboxMessages\` — \`PENDING_INDEXING\` / \`INDEXED\`; used for cross-device delete sweep
- \`InboxPendingActionsDAO.kt\` — full CRUD for both pending tables including batch ops, TTL sweeps, and state transitions
- \`InboxMessageDAOImpl.kt\` — adds \`markIndexed\`, \`findSweepableV2Ids\`, and per-message \`indexState\` read/write

### 🏷️ V1/V2 Source Tagging (new)
- \`InboxMessageSource.kt\` — \`V1\` / \`V2\` enum persisted per message row; \`InboxResponse\` tags V1, \`InboxV2Response\` tags V2
- \`InboxIndexState.kt\` — \`PENDING_INDEXING\` / \`INDEXED\` enum for cross-device sweep eligibility
- \`CTInboxController\` exposes \`isV2Message(id)\` helper for gate sites without touching the public \`CTInboxMessage\` model

### 🔁 Cross-Device Sync
- **Read sync**: \`_markReadForMessageWithId/Ids\` writes to \`inbox_pending_reads\`; \`processV2Response\` clears confirmed rows; TTL fallback = \`now + 1 day\`
- **Delete sync**: \`deleteInboxMessage/ForIDs\` writes \`wzrkParams\` + \`expires\` to \`inbox_pending_deletes\` before wiping local cache; coordinator transitions to \`AWAITING_CONFIRM\` on 200; TTL sweep prevents ghost rows
- **Cross-device delete via index sweep**: \`processV2Response(FETCH)\` bulk-promotes incoming ids to \`INDEXED\` then sweeps absent sweepable ids; 6-hour \`PENDING_INDEXING\` grace window prevents false-positive removal of live-behaviour messages; infinite-TTL (\`wzrk_ttl=0\`) messages are excluded

### 📲 Trigger Wiring
| Trigger | Behaviour |
|---|---|
| Cold app launch (\`ActivityLifeCycleManager\`) | \`bridge.submit(APP_LAUNCH)\` — un-throttled |
| \`onUserLogin\` identity switch (\`LoginController\`) | \`bridge.submit(USER_LOGIN)\` after ARP received — throttle reset |
| Public \`fetchInbox()\` API | \`bridge.submit(USER_INITIATED)\` — throttle applied |
| Pull-to-refresh (\`CTInboxListViewFragment\`) | Routes through \`fetchInbox()\` — throttle applied |

### 📡 Public API (new)
- \`CleverTapAPI.fetchInbox()\` / \`fetchInbox(FetchInboxCallback)\` — on-demand server refresh for custom inbox UIs; null controller handled defensively (callback fires \`false\`)
- \`FetchInboxCallback\` — \`@FunctionalInterface\` with \`onInboxFetched(boolean success)\`
- \`CleverTapAPI.isInboxV2DisabledForSession()\` (\`@RestrictTo LIBRARY\`) — queried by \`CTInboxListViewFragment\` to disable swipe gesture on 403

### 🎨 UI (modified)
- \`CTInboxListViewFragment\` — wrapped list in \`SwipeRefreshLayout\`; pull gesture fires \`fetchInbox()\`; swipe disabled (not hidden) after V2 403; \`canChildScrollUp\` delegated to active \`RecyclerView\` so mid-scroll upward drags don't falsely trigger refresh

### 📊 Analytics (modified)
- \`AnalyticsManager.pushInboxMessageStateEvent\` — adds \`wzrk_mid\` field for V2 messages only (gated by \`isV2InboxMessage\`); skips \`Viewed\` for already-read V2 messages; uses controller-fresh DAO to avoid UI-thread pre-mark race
- \`EventSuppressor.kt\` — generic per-key sliding-window dedup backed by \`ConcurrentHashMap\`; Viewed 2 s / Clicked 5 s suppressors gate \`pushInboxMessageStateEvent\`
- \`CTMessageDAO\` — parses \`isRead\` from V2 JSON; missing field defaults to \`false\` (V1 compatible)

---

## Testing

~20 new test files and significant additions to existing ones covering:

- \`CallResultTest\`, \`EventRequestBodyTest\`, \`FetchThrottleTest\`, \`NetworkScopeTest\`
- \`InboxFetchCallTest\`, \`InboxDeleteCallTest\`, \`InboxV2EventBuilderTest\`
- \`InboxV2MergerTest\`, \`InboxV2FetcherTest\`, \`InboxV2BridgeTest\`, \`InboxV2ResponseTest\`
- \`InboxDeleteCoordinatorTest\`, \`EventSuppressorTest\`
- \`InboxPendingActionsDAOImplTest\`, \`InboxMessageDAOImplTest\` (extended), \`CTMessageDAOTest\`
- \`DBAdapterTest\` (extended), \`DatabaseHelperMigrationTest\`
- \`CTInboxControllerTest\` (extended — +681 lines), \`AnalyticsManagerTest\` (+237 lines)
- \`CtApiInboxFetchTest\`, \`ConstantsTest\`, \`CleverTapAPITest\` (extended)

---

## Backward Compatibility

- All V1 inbox paths are **unchanged**: V1 messages are tagged at parse time and bypass V2 network, delete-sync, and \`wzrk_mid\` emission.
- DB migration is **additive** (new tables and columns only); existing \`inboxMessages\` rows are unaffected.
- \`CTInboxMessage\` public model is **untouched**; \`CTMessageDAO.toJSON()\` does not expose the \`source\` or \`index_state\` columns.
"

Length of output: 746


@piyush-kukadiya — the automated gh pr edit command is blocked due to workflow restriction settings on this repository. Please copy the description below and paste it directly into the PR description field:


Summary

Implements Inbox V2 sync for the CleverTap Android SDK (release 8.2.0). This PR introduces an active-fetch pipeline for the V2 inbox endpoint, cross-device read/delete synchronisation, offline resilience via pending-action tables, a public fetchInbox() API, pull-to-refresh in the built-in inbox UI, and comprehensive analytics guards — all while keeping the existing V1 inbox behaviour unchanged.

Jira: SDK-5709


Motivation

The existing inbox relied solely on passive delivery through the /a1 event-response pipeline. V2 inbox requires:

  • An active fetch to a dedicated endpoint (/inbox/v2/getMessages) so the inbox loads on cold launch, identity switch, and on-demand user refresh.
  • Cross-device sync for read state and deletes, which must survive app restarts and backend propagation delays.
  • A clear V1/V2 discriminator so new backend contracts (event keys, delete endpoint) are never applied to legacy messages.

Changes

🔌 Network Fetch Layer (new)

File Description
CallResult.kt Sealed class unifying 5 outcomes of a V2 network call: Success, Throttled, Disabled, HttpError, NetworkFailure
EndpointCall.kt fun interface contract for all V2 endpoint implementors
NetworkScope.kt Per-SDK-instance CoroutineScope backed by SupervisorJob + injectable Dispatchers.IO
EventRequestBody.kt Centralises the [header, event] JSON array layout every V2 call needs
FetchThrottle.kt In-memory @Volatile timestamp throttle; only USER_INITIATED calls check/record it
FetchTrigger.kt Enum replacing the former respectThrottle boolean: APP_LAUNCH, USER_LOGIN, USER_INITIATED
InboxFetchCall.kt First EndpointCall implementor — builds wzrk_fetch event, hits sendInboxFetch, maps HTTP outcomes
InboxDeleteCall.kt Action-only EndpointCall<Unit> that batches pending deletes into one HTTP POST
InboxV2EventBuilder.kt Stamps standard event metadata (s, pg, ep, f, lsl, pai, n) onto every V2 event body
CtApi.kt Adds sendInboxFetch and sendInboxDelete; fixes getUriForPath multi-segment URL encoding

📨 V2 Response Handling (new)

File Description
InboxV2Response.kt Parses V2 fetch payload; extends CleverTapResponseDecorator so V2 messages delivered via /a1 are also handled
InboxV2Merger.kt Zero-dependency dual-filter object: preWriteFilter and postReadCleanup with shared predicate
InboxV2DeliverySource.kt FETCH vs A1 discriminator; cross-device delete sweep runs on FETCH only

🔄 Inbox Orchestration (new)

File Description
InboxV2Fetcher.kt Session-disable flag, throttle gate, recordFetch, hands off to InboxV2Response
InboxV2Bridge.kt Java-interop adapter; launches suspend fetcher on NetworkScope, delivers to optional FetchInboxCallback
InboxDeleteCoordinator.kt Batches N deletes per HTTP call; clears inbox_pending_deletes on 2xx; drains pending rows on inbox-init (retryPending)

🗄️ Database (modified/new)

  • Schema migration v6 → v7 (CtDatabase.kt)
    • New table inbox_pending_deletes: (USER_ID, ID, wzrkParams, state, expires) — two-state machine (PENDING_SENDAWAITING_CONFIRM); TTL-driven expiry after server 200
    • New table inbox_pending_reads: (USER_ID, ID, expires) — cleared when server echoes isRead=true or TTL elapses
    • New column source TEXT NOT NULL DEFAULT 'V1' on inboxMessages — V1/V2 discriminator
    • New column index_state on inboxMessagesPENDING_INDEXING / INDEXED; used for cross-device delete sweep
  • InboxPendingActionsDAO.kt — full CRUD for both pending tables including batch ops, TTL sweeps, and state transitions
  • InboxMessageDAOImpl.kt — adds markIndexed, findSweepableV2Ids, and per-message indexState read/write

🏷️ V1/V2 Source Tagging (new)

  • InboxMessageSource.ktV1 / V2 enum persisted per message row; InboxResponse tags V1, InboxV2Response tags V2
  • InboxIndexState.ktPENDING_INDEXING / INDEXED enum for cross-device sweep eligibility
  • CTInboxController exposes isV2Message(id) helper for gate sites without touching the public CTInboxMessage model

🔁 Cross-Device Sync

  • Read sync: _markReadForMessageWithId/Ids writes to inbox_pending_reads; processV2Response clears confirmed rows; TTL fallback = now + 1 day
  • Delete sync: deleteInboxMessage/ForIDs writes wzrkParams + expires to inbox_pending_deletes before wiping local cache; coordinator transitions to AWAITING_CONFIRM on 200; TTL sweep prevents ghost rows
  • Cross-device delete via index sweep: processV2Response(FETCH) bulk-promotes incoming ids to INDEXED then sweeps absent sweepable ids; 6-hour PENDING_INDEXING grace window prevents false-positive removal of live-behaviour messages; infinite-TTL (wzrk_ttl=0) messages are excluded

📲 Trigger Wiring

Trigger Behaviour
Cold app launch (ActivityLifeCycleManager) bridge.submit(APP_LAUNCH) — un-throttled
onUserLogin identity switch (LoginController) bridge.submit(USER_LOGIN) after ARP received — throttle reset
Public fetchInbox() API bridge.submit(USER_INITIATED) — throttle applied
Pull-to-refresh (CTInboxListViewFragment) Routes through fetchInbox() — throttle applied

📡 Public API (new)

  • CleverTapAPI.fetchInbox() / fetchInbox(FetchInboxCallback) — on-demand server refresh for custom inbox UIs; null controller handled defensively (callback fires false)
  • FetchInboxCallback@FunctionalInterface with onInboxFetched(boolean success)
  • CleverTapAPI.isInboxV2DisabledForSession() (@RestrictTo LIBRARY) — queried by CTInboxListViewFragment to disable swipe gesture on 403

🎨 UI (modified)

  • CTInboxListViewFragment — wrapped list in SwipeRefreshLayout; pull gesture fires fetchInbox(); swipe disabled (not hidden) after V2 403; canChildScrollUp delegated to active RecyclerView so mid-scroll upward drags don't falsely trigger refresh

📊 Analytics (modified)

  • AnalyticsManager.pushInboxMessageStateEvent — adds wzrk_mid field for V2 messages only (gated by isV2InboxMessage); skips Viewed for already-read V2 messages; uses controller-fresh DAO to avoid UI-thread pre-mark race
  • EventSuppressor.kt — generic per-key sliding-window dedup backed by ConcurrentHashMap; Viewed 2 s / Clicked 5 s suppressors gate pushInboxMessageStateEvent
  • CTMessageDAO — parses isRead from V2 JSON; missing field defaults to false (V1 compatible)

Testing

~20 new test files and significant additions to existing ones covering:

  • CallResultTest, EventRequestBodyTest, FetchThrottleTest, NetworkScopeTest
  • InboxFetchCallTest, InboxDeleteCallTest, InboxV2EventBuilderTest
  • InboxV2MergerTest, InboxV2FetcherTest, InboxV2BridgeTest, InboxV2ResponseTest
  • InboxDeleteCoordinatorTest, EventSuppressorTest
  • InboxPendingActionsDAOImplTest, InboxMessageDAOImplTest (extended), CTMessageDAOTest
  • DBAdapterTest (extended), DatabaseHelperMigrationTest
  • CTInboxControllerTest (extended — +681 lines), AnalyticsManagerTest (+237 lines)
  • CtApiInboxFetchTest, ConstantsTest, CleverTapAPITest (extended)

Backward Compatibility

  • All V1 inbox paths are unchanged: V1 messages are tagged at parse time and bypass V2 network, delete-sync, and wzrk_mid emission.
  • DB migration is additive (new tables and columns only); existing inboxMessages rows are unaffected.
  • CTInboxMessage public model is untouched; CTMessageDAO.toJSON() does not expose the source or index_state columns.

* chore(SDK-5709): update docs
                                                                                                         System calls (app-launch, onUserLogin) no longer record the throttle timestamp, so they cannot block a subsequent pull-to-refresh or fetchInbox() call. Only USER_INITIATED calls both check and record the throttle. Replaces the boolean respectThrottle flag with an enum class FetchTrigger for type-safe, self-documenting call sites.

* chore(SDK-5709): improve docs
                                                                                                         System calls (app-launch, onUserLogin) no longer record the throttle timestamp, so they cannot block a subsequent pull-to-refresh or fetchInbox() call. Only USER_INITIATED calls both check and record the throttle. Replaces the boolean respectThrottle flag with an enum class FetchTrigger for type-safe, self-documenting call sites.

* chore(SDK-5709): bump version and run copyTemplates
                                                                                                         System calls (app-launch, onUserLogin) no longer record the throttle timestamp, so they cannot block a subsequent pull-to-refresh or fetchInbox() call. Only USER_INITIATED calls both check and record the throttle. Replaces the boolean respectThrottle flag with an enum class FetchTrigger for type-safe, self-documenting call sites.

* chore(SDK-5709): update CTCORECHANGELOG.md regarding accounts not using v2 and uses 8.2.0

* chore(SDK-5709): update release date

* chore(SDK-5709): update sample app core version
@github-actions
Copy link
Copy Markdown

Code Coverage Debug

Overall Project 68.41% -0.38%
Files changed 90.1%

Module Coverage
clevertap-core 67.34% -0.43%
Files
Module File Coverage
clevertap-core InboxV2EventBuilder.kt 100%
FetchThrottle.kt 100%
CallResult.kt 100%
EventRequestBody.kt 100%
NetworkScope.kt 100%
Constants.java 100%
EventSuppressor.kt 100%
InboxDeleteCoordinator.kt 100%
InboxV2Merger.kt 99.42% -0.58%
InboxV2Response.kt 98.48% -1.52%
CleverTapFactory.kt 96.38% -0.27%
InboxV2Fetcher.kt 96.32% -3.68%
InboxFetchCall.kt 95.65% -4.35%
CtApi.kt 95.41% -0.95%
DBAdapter.kt 94.19%
InboxV2Bridge.kt 91.43% -8.57%
FetchTrigger.kt 90.48% -4.76%
InboxV2DeliverySource.kt 90.48% -4.76%
InboxMessageSource.kt 90.48% -4.76%
InboxDeleteCall.kt 88.27% -11.73%
AnalyticsManager.java 87.97% -0.08%
CTInboxController.java 86.88% -3.9%
InboxMessageDAOImpl.kt 83.18% -5.33%
CTMessageDAO.java 82.66%
InboxPendingActionsDAO.kt 79.58% -20.42%
CoreState.kt 78.22%
CtDatabase.kt 64.32%
LoginController.java 59.81%
ActivityLifeCycleManager.java 53.71%
CleverTapAPI.java 31.3% -0.32%
ControllerManager.java 26.09% -12.61%

@piyush-kukadiya piyush-kukadiya merged commit 97776be into develop May 19, 2026
9 checks passed
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.

4 participants