Sibling libraries

Four in-house "Bridge" libraries carry Urvi's login, HTTP and offline write queue; the rewrite should replace all four and move their platform logic into the App Service.

How they fit together

LayerLibraryRole in Urvi
Request modelcore-apiOperation + CoreAPI run every REST call, now or queued
Queue logicoffline-coreDependency-aware retry processor and garbage collector
Queue storage + drainoffline-androidSQLite SYNC_QUEUE_DB, SyncAdapter in a :sync process
Authaccount-libAccountManager authenticator, login UI, session token

The write path is:

  1. A screen saves an entity to Room with syncStatus=SYNC_NOT_INITIALIZED.
  2. A WorkManager worker (data/.../sync/work/*Worker.kt) builds an Operation and calls CoreAPI.dooWithRetry (data/.../manager/CoreAPIManager.kt:44-58).
  3. CoreAPIOfflineImpl writes the operation to SQLite instead of sending it.
  4. SyncWorkStatusHelper.triggerSyncAdapter asks Android to run the SyncAdapter.
  5. The adapter drains the queue through SyncQueueProcessor and core-api.
  6. About 40 response handlers (data/.../sync/rh/*RH.kt) write server ids back into Room.

Online-only calls (setup data, VO filters, endorsements) skip the queue and use CoreAPI.dooNowOrThrow.

core-api

What it does. A small "operation" abstraction over REST. An Operation<T> holds URL, method, body DTO, multipart params, headers, a response handler and integrators. CoreAPI runs it now or queues it.

ItemValue
Coordinatescom.mindtree.bridge.framework:CoreAPI:8.2.2 (versions.gradle:74), from private Nexus
Source repo8.2.1, one version behind the consumed artifact
Size15 Java files, about 1.2k LOC, Java 1.8 jar
HTTPframework-android-rest-client:4.2.0, which wraps Apache HttpClient
Loose copyCoreAPI-8.2.2.jar at the repo root is not on any classpath

What Urvi uses.

  • Operation heavily: 12 imports in :app, 67 in :data.
  • CoreAPI.dooNowOrThrow for all online calls.
  • CoreAPI.doo only through the dooWithRetry extension.
  • CoreAPIFactory.getCoreAPI (app/.../bridge/BridgeUtil.kt:32, CoreAPIManager.kt:24).
  • ResponseHandler (about 40 subclasses of BaseResponseHandler.kt) and Integrator (IECreateAIACreateIntegrator, AttachmentIntegrator).
  • TryAgain values NOW, LATER, NEVER.
  • RetryScheduler.Policy(1176, 0, retryDelay): up to 1,176 attempts at a constant delay.

Key internals.

  • The factory loads CoreAPIOfflineImpl by reflection, else falls back to CoreAPIDefaultImpl. Each call builds a new instance.
  • URL = session base URL + op.getUrl(). Body is Gson JSON. Content-Type: application/json always.
  • Auth: X-SESSIONID = AccountManager auth token, on every call.
  • On 401 it invalidates the token and retries. The next token fetch silently logs in again with the stored password.
  • On 503 it retries. dooNowOrThrow allows 1 retry, doo allows 2.
  • Dates go on the wire as epoch milliseconds.

Known bugs.

BugWhere
dooNowOrThrow never throws; it swallows every exception, so callers must check getStatus()impl/CoreAPIDefaultImpl.java
Response headers are copied into op.params, not responseHeadersCoreAPIDefaultImpl.processResponse

Unused parts. doo(op, PriorityQueue), PriorityQueue, setDeviceID, deprecated TryAgain.IN*, URLUpdater, MultipartByteSeek, getNewRequestId.

offline-core

What it does. Platform-agnostic logic for a persistent outbound queue: entity model, Operation mapper, processor (dependencies, retries, expiry) and garbage collection. Storage comes from offline-android.

ItemValue
Coordinatescom.mindtree.bridge.framework:OfflineCore:8.2.1 (versions.gradle:86), transitive = false
Size11 Java files, about 1.7k LOC
Repo stateBranch feature/app_version_header; the only local edit is whitespace

What Urvi uses.

  • SyncQueueProcessor.processSyncQueue() directly in data/.../sync/utils/SyncUtils.kt:71-80 (forceSync), and through the SyncAdapter.
  • SyncQueueGarbageCollectorImpl.collectGarbage(dao, 30000) (SyncUtils.kt:78).
  • OperationManagerImpl for the Pending Sync screen (app/.../pendingsync/PendingSyncViewModel.kt:20) and integrator repair (HomeViewModel.kt:63).
  • OperationMapper.mapOperation (HomeViewModel.kt:70).
  • Raw OperationEntity reads in app/.../utility/DataUtil.kt:855 and ClearSyncQueueWorker.kt.

Key internals.

ConcernBehaviour
PersistenceBody as JSON. params (may hold File) as Java-serialized, Base64 HashMap. Handlers and integrators stored as class names, rebuilt by reflection
IdempotencyPOST/PUT/PATCH get X-REQUESTID: <UUID> at enqueue
Three-way ACKEach send also carries X-ACK-REQUESTID (one earlier successful id) so the server can forget it
OrderingRows sorted CREATE_TIME ASC, QUEUE_PRIORITY ASC; priority only breaks ties
DependenciesEach integrator names a source op. Source runs first (depth-first); then integrate(target, source) copies the server id into the target
RetriesLATER sets nextAttempt = now + delaySeed + attempts*gradient. Urvi uses gradient 0, so the delay is constant
401Sets syncBlocked and stops the run
409Treated as "already seen": old request id moves to the ACK set, a new UUID is issued. No payload merge
ExpiryAfter attemptThreshold (1,176) the op and every dependent op are cleared
GCDeletes evaluated rows older than 30 s unless a pending target depends on them
ConcurrencyNone. Plain HashMaps, no lock

Known bugs.

BugWhere
Queueing an op without a retry policy throws an NPEOperationMapper.java:124
onSourceExpire arguments are swappedSyncQueueProcessor.java:276
A response-handler exception becomes NEVER; the op is silently droppedSyncQueueProcessor
Two processes can drain the same SQLite file at once (:sync adapter and in-app forceSync); the isSyncActive check is racySyncUtils.kt, SyncAdapter
Renaming or obfuscating any handler, integrator or DTO class breaks rows already in the queueOperationMapper

Unused parts. OperationManager.addDeviceInPreference / getDeviceID.

offline-android

What it does. The Android side of the queue: SQLite DAO, CoreAPIOfflineImpl (persists instead of sending), SyncAdapter that drains the queue, and failure broadcasts.

ItemValue
Coordinatescom.mindtree.bridge.framework:OfflineAndroid:8.2.3@aar (versions.gradle:75)
Size19 Java files, about 1.4k LOC
BuildcompileSdk/targetSdk 27, minSdk 19, support library (pre-AndroidX, jetified)
Pulls inOfflineCore 8.2.1, bridge-account-lib 2.7.1 (older than Urvi's 2.7.13)

What Urvi uses.

  • CoreAPIOfflineImpl (via reflection): doo writes with queuePriority = 1.
  • OperationDaoSqLiteImpl and SQLiteHelper directly, including raw cursor reads (DataUtil.kt:856).
  • SyncService + SyncAdapter, enabled in HomeActivity.kt:708-728.
  • StubContentProvider, declared in app/src/main/AndroidManifest.xml:178.
  • SyncCallback (SyncUtils.kt:66), SyncListener (AppSyncListener), SyncHelper.getQueueSize (8 call sites).

Key internals.

  • DB SYNC_QUEUE_DB v3. Table OPERATION (22 columns: id, URL, method, message, params, headers, handler class, DTO class, evaluated, user, attempts, next attempt, retry policy and more). Table INTEGRATOR (class name, target id, source id).
  • ACK ids live in SharedPreferences CORE-API-SYNC-3WAY.
  • SyncAdapter.onPerformSync builds AndroidSessionManager(account), runs the processor, then GC, then calls the SyncListener on raw threads. Exceptions are swallowed.
  • SyncCallback sends an implicit, unprotected broadcast (com.mindtree.bridge.coreapi.SYNC_FAIL) for every non-2xx, with status, error and account.

Known bugs.

BugWhere
Queue priority ignored (sort is CREATE_TIME first)OperationDaoSqLiteImpl.java:183
ACK code mutates the set returned by getStringSet; change may not persist, order is arbitraryOperationDaoSqLiteImpl.java:297-339
Targets SDK 27; obsolete READ/WRITE_EXTERNAL_STORAGElibrary manifest
Nexus read credentials hardcodedoffline-android/build.gradle:11-12,21-22,43-44,53-54

Unused parts. Sync (init/start/cancel), SyncHelper.triggerSyncNow / addPeriodicSync, SyncConstants.AUTHORITY (Urvi overrides it in app/src/main/res/xml/syncadapter.xml), SyncResponseBroadcastReceiver (no receiver), OperationSplitter / FileSplitter chunked upload (call commented out).

account-lib

What it does. An Android AccountManager authenticator for "Bridge" accounts: login UI, session token issue and refresh, base URL and tenant (federation) selection, and device-registration headers.

ItemValue
Coordinatescom.mindtree.bridge.framework:bridge-account-lib:2.7.13-urvi (versions.gradle:73); :app takes it transitively, :data with transitive=false
Repo stateBranch dev-firoz, uncommitted edits in 43 files; the published aar may not match the tree
Size41 Java files, about 8k LOC; strings in en, hi, ta, te
Leaks depsapi exposes okhttp 5.2.1, retrofit 3.0.0, converter-gson 3.0.0, which clash with Urvi's retrofit 2.11.0 / okhttp 5.3.0

What Urvi uses.

  • AndroidSessionManager: 26 imports, 55 constructions. getSessionId() = AccountManager.blockingGetAuthToken(account, "Full access"). getUserId() = account name.
  • BridgeAccountHelper.triggerAccountSelection / getSelectedAccount (SplashActivity.kt:116,140,157, SettingsActivity.kt:381,394).
  • BaseURLUtil get/set base URL (app/.../core/ApplicationClass.kt:66-85, RetrofitAPIClient.kt:34, RetrofitProvider.kt:49).
  • SelectAccountActivity (splash pager + "Add account") and AuthenticatorActivity (the login form).
  • Authenticator implicitly. Urvi also calls AccountManager.getAuthToken itself on session expiry (SettingsActivity.kt:419-441, HomeActivity.kt:329-379).

Login flow Urvi depends on.

  1. Validate: online, non-empty username and password, lower-case and trim.
  2. Accepted login shapes: email, login@@tenant, numeric IE id longer than 12 digits, or any username plus an org.
  3. Account name becomes login@@tenant (email@@world when org is empty).
  4. Tenant ap.gov switches the base URL to the federation host. This is the only multi-tenant switch.
  5. PUT <base>//ctld/api/device/deviceid/v1 registers the device (note the double slash).
  6. Login headers: X-APPID, X-APPVERSION, X-APPKEY, X-APPCODE, X-DEVICEID.
  7. BridgeSessionHelper.createSession (in closed-source bridge-security-api) returns the session id.
  8. addAccountExplicitly(account, password, null) stores the plaintext password; setAuthToken stores the session.
  9. On a later 401, Authenticator.getAuthToken repeats steps 5-7 silently using the stored password.

Known bugs and risks.

IssueWhere
Plaintext password in AccountManager, reused for silent re-loginAuthenticatorActivity.java:553, Authenticator.getAuthToken
Offline password compare in plaintext (multi-user mode, off in Urvi)SelectAccountActivity.java:513
Session token written to Log.dAndroidSessionManager.java:54
Static Activity reference leaks memoryBridgeAccountHelper.callingActivity
getBaseURLWithTechl produces //ctldutils/BaseURLUtil
Nexus credentials hardcodedaccount-lib/build.gradle:11-12,21-22,44-45,54-55

Unused parts. Forgot / change password, self-registration (1.9k LOC) and SMS OTP, account activation, multi-instance server picker, DevUtil, DefaultAccountPreferences, GPSTracker, local User DB, rest/IEAPI, rest/UserAPI, RestClient classes, getHeaders(). All are switched off in app/src/main/res/values/configfile.xml.

Keep / replace

The App Service should own session creation, device registration, tenant routing, idempotency storage and cross-entity id resolution. The client then needs one auth flow, one HTTP client, one local DB and one sync worker.

ConcernCompose (Kotlin)FlutterMove to App Service
Request model (Operation)Retrofit 2.11 or Ktor + kotlinx.serializationdio + freezed / json_serializableBase-URL prefixes (ctld, ptld), 206 paging, one error shape
Auth header, refresh on 401OkHttp Authenticator or Ktor Auth bearerdio QueuedInterceptorIssue access + refresh tokens
503 retryOkHttp interceptor / Ktor HttpRequestRetrydio_smart_retryReturn Retry-After
Multipart uploadsRetrofit @Multipart, or presigned PUTdio FormData, or presigned PUTIssue presigned upload URLs
Persistent queueRoom outbox table with Flow for pending countDrift outbox table with streamsn/a
Drain triggerWorkManager CoroutineWorker, unique work, CONNECTED, exponential backoff, expedited "Sync now". Drop SyncAdapter, StubContentProvider, :syncworkmanager plugin + BGTaskScheduler on iOS, connectivity_plusn/a
Dependencies (integrators)Client UUIDs; dependsOn column only if neededsameAccept client ids and resolve parent references
IdempotencyIdempotency-Key = outbox row idsameStore keys with a TTL; ACK set no longer needed
Retry / expiryattempts column, then a visible "failed" state with manual retry. Never drop silentlysamen/a
ConflictsSend If-Match / updatedAtsamePer-entity server-wins or merge policy
Response handlers (40 classes)Sealed OutboxOp + when; no reflection, no class names in DBenum + switchn/a
Failure broadcastStateFlow / Room FlowStream / Riverpodn/a
LoginAppAuth-Android or Credential Manager (OIDC + PKCE)flutter_appauthPOST /auth/login runs device registration and Bridge createSession server-side
Token storageRefresh token in EncryptedDataStore + Keystore. Never store the passwordflutter_secure_storagen/a
Tenant / federationKeep the org field and @@ parsingsameGET /tenants/resolve?name= or remote config
Device headersApp id + version as headers; app-scoped UUID, not ANDROID_ID; drop APP_KEYpackage_info_plus, UUID in secure storagePlay Integrity / App Attest check
Unused account flowsDo not portDo not portAdd later only if product asks

Net result. All four Bridge libraries, Apache HttpClient, the support library, the SyncAdapter and AccountManager go away. In Kotlin, roughly 150 lines of outbox code plus one worker replace the queue.

Migration risk: drain the queue before cutover

Field devices hold unsynced farmer data in SYNC_QUEUE_DB (OPERATION, INTEGRATOR). Those rows are Java-serialized and bound to class names. A Flutter app cannot read them, and a Kotlin app can only read them if every class name stays the same.

StepAction
1Ship a final release of the current app that forces a full drain and shows the pending count
2Block the upgrade (or warn hard) until the old queue is empty
3On first launch, the new app checks for a non-empty legacy queue and refuses to wipe it
4Keep the old 409 / X-REQUESTID handling server-side until all old clients are gone, so replays stay idempotent

Do not rename or obfuscate handler, integrator or DTO classes in any interim release of the current app. That breaks rows already queued.