Improvements
Fix what the rewrite must not inherit: unsafe secrets and PII flows, sync paths that lose data, a chatty multi-service client and year-by-year copy-paste. Each row is a problem seen in today's code with a concrete fix, rated by impact (H/M/L) and effort (S/M/L).
Rows marked Now are worth doing in the current app before the rewrite starts; they are repeated as the hygiene hotfix in the roadmap.
Product and UX
| Problem today | Improvement | Impact | Effort |
|---|---|---|---|
| Farmer-hub tiles are greyed by alpha only, stay clickable and answer with a toast ("update category", "authenticate land") | Show each tile's state and the reason on the tile ("Locked: land not authenticated"), with a button to the fix | H | S |
| Validation is toast-only; the user fixes one error, presses Save, then sees the next | Inline field errors, all shown at once, with a summary at the top of long forms | H | M |
| Category freeze rules (gapStartDate vs admin date vs land authentication vs multi-edit flag) are invisible | One "Category locked because …" message driven by the same rule the server applies | M | S |
| Crop Actual month copy runs silently on open behind a blocking dialog | Explicit "Copy last month" action with a preview of what will be copied | M | M |
| Saving a compact block under 5 acres silently turns into "delete block" through a look-alike dialog | Separate, explicit Delete action; live 5-acre progress bar while picking tiles; explain why a tile is not selectable | H | S |
| Land math is hidden: users see "Land limit exceed" with no numbers | Show total, minus Crop Actual, minus ATM, minus leased out, equals available, in ATM, Crop Actual and land edit | H | S |
| Many non-dismissable dialogs (automatic time, session expired, device conflict) stack up | One "Blocking issues" screen listing what must be fixed and how | M | M |
| Logout warns "un-synced data will be lost" without numbers | Show the pending count, offer "Sync now", and block logout while unsynced items exist unless confirmed twice | H | S |
| Geofence capture shows no accuracy, area or point count | Live accuracy in metres, point count, area, perimeter, undo last point; warn when area differs from declared acres | H | M |
| Activity Tracker 18:00 edit and submit rules fail silently | Show the window on screen with a countdown | L | S |
| Analytics screen is empty unless data was fetched today | Show the last data with a "last updated" label | M | S |
Each agricultural year is a copied screen (ActionPlanFragment2025To2026, FarmerAbstract2425BottomSheet) | Schema-driven forms: the App Service sends the season's form definition and ceilings | H | L |
| Crop Plan (2023-24) and AAP 2024-25 are hidden but still in code | Retire them; re-specify only if the programme asks | M | S |
| Family details (marital status, spouse, father) are captured only for women; father details are saved in spouse columns | Capture family for everyone with separate spouse and father fields, confirmed with the programme | M | M |
| Max land and crop acreage is 99.99 because of a 2-digit input filter | Server-configured limits per field | M | S |
Data and offline
| Problem today | Improvement | Impact | Effort |
|---|---|---|---|
Two databases (agri_db and legacy ie_register.db) hold the same farmers, land and crops; bridge mappers copy between them | One database, one row per entity, screens read through queries | H | L |
| Three farmer shapes joined by four different keys | One farmer table: local UUID primary key, unique nullable server id, foreign keys by local id | H | M |
BaseEntity.appId defaults to a random UUID on parsed server data, so re-downloads can duplicate rows; child label tables are never cleared | Upsert by server id; replace child collections in one transaction | H | M |
insert(List) is IGNORE while insert(T) is REPLACE: compact blocks never refresh, farmer rows lose device-only flags | Explicit @Upsert (or drift insertOnConflictUpdate) with column-level merge rules | H | S |
| No foreign keys and almost no indexes on join columns | Foreign keys plus indexes on every join and filter column; sync_status.sync_key unique | M | S |
Queries use LIKE for equality, substring match on asset codes, TEXT date ordering | Use =, exact code match, INTEGER epoch millis everywhere | M | S |
| JSON blobs in columns (AAP, ATM crops and harvests, change log, practices) | Child tables for ATM crops and harvests; keep only opaque survey answers as JSON | M | M |
Year-specific columns (annualActionPlanData2526, NFCategory2526) | farmer_plan(farmer_id, plan_year, data) | M | S |
| Three error stores and two notification stores | One sync_error table and one notification table | L | S |
| Main-thread queries allowed on two databases | Suspend and Flow DAOs only (or drift isolates) | H | M |
| 62 schema versions with exports missing for 10-51; destructive fallbacks | Fresh schema at v1 with exported, tested migrations from day one; drain the old app first | H | M |
Sync
| Problem today | Improvement | Impact | Effort |
|---|---|---|---|
Two queues (Room sync_ledger feeding the Bridge SYNC_QUEUE_DB) plus per-row syncStatus that can disagree | One outbox table in the same DB as the data, written in the same transaction | H | M |
| Some writes skip the ledger (ATM harvest, AAP 2025-26), so they are invisible in Pending Sync and have no dependencies | Every write goes through the outbox | H | S |
| Client resolves parent server ids through 40 response handlers and a dependency graph | Client sends client UUIDs; the App Service resolves references | H | M |
| No conflict detection; full DTOs overwrite the server | Base version on every update; typed conflict responses; a per-entity policy (see App Service) | H | M |
| Retries up to about 1,176 attempts at a constant delay; handler exceptions drop operations silently | Exponential backoff, a visible "failed" state with reason, manual retry, never silent drops | H | S |
| The queue can be drained by the SyncAdapter process and in-app force sync at once | Single unique WorkManager job; drop SyncAdapter, stub provider and the :sync process | H | S |
| Down-sync can overwrite pending edits | Never overwrite a row that has a pending outbox op; merge after the op settles | H | S |
| Full re-download on "load more" and per-farmer refresh | Snapshot once, then cursor-based deltas with tombstones | H | M |
| Images that already exist on the server are downloaded and re-uploaded by the worker | Upload files once, reference by file id | M | S |
| Firebase Storage fallback stores images the backend never learns about | Remove; resumable uploads to the App Service | M | S |
Backend and API
| Problem today | Improvement | Impact | Effort |
|---|---|---|---|
The app calls about 12 services and 40 datasets, building dataset params and SQL-like criteria= on the phone | One App Service as the only backend; dataset names and query syntax stay server-side | H | L |
| About 300 calls for a 1,500-farmer setup; about 15 calls on every Home open | Hydrated farmer snapshot pages and one ETag'd config bundle | H | M |
| "Opening" a farmer POSTs to create the server IE from the flat record | Server does get-or-create behind GET /farmers/{id} | M | S |
Season and year are baked into endpoints (AAP2025_26, …2425, IsKAP202324) | ?season=current; the App Service maps to the right backend | H | M |
POST used as a read (line-items/ownerEntities/latest) | Proper GET in the App Service | L | S |
| HTTP 206 used as "more pages" for ref data | Standard cursor pagination | L | S |
| No app-version, tenant or device headers, so the server cannot gate versions | Standard headers and a server-enforced minimum version | H | S |
Debug builds send ap.gov users to the production host | Build flavors per environment; one App Service URL per environment | H | S |
Security and privacy
| Problem today | Improvement | Impact | Effort |
|---|---|---|---|
| Release keystore and passwords committed; Nexus credentials in build files (Now) | Rotate the upload key (Play App Signing), rotate Nexus credentials, purge history, move secrets to CI | H | S |
Sync operations with session headers and PII go to Firestore; the whole queue goes to sync-service every 6 hours (Now) | Stop both; send redacted counters only | H | S |
HttpLoggingInterceptor at BODY level in release (Now) | Gate logging to debug builds; one logging facade with redaction | H | S |
| Password stored in plaintext in AccountManager and used for silent re-login | Access and refresh tokens only, refresh token in Keystore-backed storage | H | M |
| Aadhaar, ration card and phone numbers unencrypted in Room | SQLCipher database; mask Aadhaar in UI; ask whether the server can return only the last 4 digits | H | M |
| Support export zips the whole DB and prefs (with the session token) to e-mail or external storage | Redacted diagnostics uploaded to the App Service, never e-mailed | H | S |
| Geofence KML embeds farmer name and village | Ids only inside files | M | S |
| Client-held FCM legacy server key | Server-side push via FCM HTTP v1 | H | S |
Exported StubContentProvider, broad FileProvider roots, implicit sync-failure broadcasts | Remove the provider and broadcasts; narrow FileProvider paths | M | S |
APP_KEY shipped in resources | Drop from the client; use Play Integrity at the App Service if app attestation matters | M | S |
A hard-coded static request header on farmer create/patch (data/.../entity/IE.kt) | Review with the backend team and remove | M | S |
| Device binding is skipped when offline or on Cancel | Bind refresh tokens to the device on the server | M | M |
| R8 off in release | R8 and resource shrinking on, with keep rules tested | M | S |
Code quality and testing
| Problem today | Improvement | Impact | Effort |
|---|---|---|---|
| 9 tests; about 0% coverage of rules, sync and migrations | Unit tests for every gate rule, validator and land-usage calculation; outbox and migration tests; contract tests against the App Service | H | M |
Business rules live in fragments and a 1,970-line AppUtils; 3,395-line DataRepository | A domain module of pure functions (category gate, land usage, validators) and per-feature repositories | H | M |
| Land availability computed three ways in three places | One shared land-usage service, mirrored on the server | H | S |
Roles checked by substring (contains("fmt_l1")) and config by string code everywhere | Typed role enum and typed config object | M | S |
No DI; Executors, raw threads and AsyncTask | Hilt or Koin; coroutines and Flow only | M | S |
| About 8% duplicated lines (two download view models, two data-setup activities) | One generic sync-item engine with role-specific item lists | M | M |
| Startup state spread over 5 activities and several pref flags | One persisted state machine: NoLanguage → NoAccount → NoRole → NeedsWorkNode → Downloading → Ready | M | S |
Asset name string used as a foreign key by portfolio, ATM and geofence | Reference assets by id or UUID | M | S |
| No CI in the repo | CI with lint, tests, R8 build and signed artifacts | M | S |
Performance on low-end devices
| Problem today | Improvement | Impact | Effort |
|---|---|---|---|
Room queries in adapters' bind() and on the main thread | Paged lists from Flow queries; no DB work on the main thread | H | M |
| Missing indexes make farmer, land and portfolio lookups scan | Indexes on join columns (see Data) | M | S |
| No R8, no Baseline Profiles | R8 plus Baseline Profiles for cold start and list scroll | M | S |
| Photos compressed with an integer-division bug (100 px wide) or to about 45 KB in other places | One image pipeline: fixed long edge (for example 1280 px), JPEG quality tuned per use, EXIF stripped | M | S |
| Many periodic workers (1 h ref data, 1 h comments, 15 min endorsements, 6 h and 12 h telemetry) | One sync job plus push; config by ETag on app start | M | S |
| Map tiles fetched online during capture | Pre-cache tiles for the user's villages over Wi-Fi | M | M |
Localization
| Problem today | Improvement | Impact | Effort |
|---|---|---|---|
| Tamil missing 416 strings (40%), Telugu missing 48 | Translation workflow with a missing-string check in CI | H | S |
| 146 hard-coded layout texts, 26 in code, 25 toasts | All user text in resources; lint rule against literals | M | S |
| Settings language change stores a JSON array, so server-side content falls back to English | Store the language code; one source of truth for UI and data language | H | S |
| Category stored as a localized label ("Farmer", "రైతు") and parsed back | Store codes, render labels | H | S |
| Reference data languages hard-coded per table (product master ENG/TEL only, UOM ENG only) | One localized-label table filled from the config bundle | M | M |
| 172 unused string keys | Remove during the port | L | S |
Observability
| Problem today | Improvement | Impact | Effort |
|---|---|---|---|
| Sync health is visible only by zipping device DBs | Redacted sync-health telemetry (pending by type, oldest age, failure codes) to the App Service | H | S |
Four logging facades, 174 printStackTrace | One facade (Timber or Kermit) with PII redaction | M | S |
| No request correlation between app and backend | Request id header echoed and logged end to end | M | S |
| Worker tag collision: endorsement and analytics workers share one unique name | One sync job; distinct names if more are needed | M | S |
Release and config
| Problem today | Improvement | Impact | Effort |
|---|---|---|---|
Environment switched by commenting XML in build_type_config.xml (108 commits) | Build flavors (dev, stage, prod) with per-flavor App Service URL | H | S |
versions.gradle mostly dead but bumped constantly | Gradle version catalog | L | S |
jcenter(), mavenLocal() and a stdlib downgrade hack | Maven Central and Google only; no forced downgrades | M | S |
| Crop Actual gated by listing exact "versionName_versionCode" values in config | Minimum-version gate in the App Service | M | S |
Hard-coded cut-offs (2026-09-01 for compact blocks, 0.5 acre, 5 acres, excluded product ZBNF_FIELD_BEANS) | Move to the config bundle | M | S |
Release-only checks (BuildConfig.DEBUG) make debug behave differently | Feature flags instead of build-type checks | L | S |
Likely bugs to fix or not carry over
| Bug | Where | Effect |
|---|---|---|
| ATM down-sync inserts the unfiltered list instead of the one excluding pending ATMs | app/.../view/option/OptionViewModel.kt (~330), app/.../view/datasetup/DataSetupViewModel.kt (~1085) | Pending ATM edits are overwritten |
VO geo filter clears the whole ie_flat_entity table before inserting results | EndorsementRepositoryImpl / VO filter (clearTable()) | Unsynced farmer rows at risk; list replaced by 10 rows |
insertIEList uses REPLACE | data/.../repo/DataRepository.kt | Device-only flags (hasATM, fetch flags, AAP JSON) reset on re-download |
hasCompactBlock set by "last PLI wins" | data/.../dao/IEPortfolioDao.kt savePortfolioChildEntities | Chip shows wrong state |
updatePortfolio sets currentSeason on every PLI | PortfolioLineItemDao | Global overwrite of season labels |
Latest PLI ordered by TEXT appCreatedAt | getLatestPortfolioItemByTitleId | Wrong "latest" version possible |
ATM harvest sends farmingModelLineItemId = if (id == 0) id else null | app/.../view/atm/harvest/AtmHarvestActivity.kt | Real line-item id is never sent |
| Harvest photo and GPS are captured but never saved or sent | ATM harvest and SelectHarvestBottomSheet / HarvestLogs | Mandatory photo goes nowhere |
| Continuous harvest of one crop marks all ATM crops PENDING; whole marks all COMPLETED | AtmHarvestViewModel.updateFarmingModelStatus | Wrong harvest state on other crops |
| ATM harvest and AAP 2025-26 bypass the sync ledger | AtmHarvestViewModel.kt:47-53, data/.../entity/IEFlatEntity.kt:495-523 | No dependencies, not visible in Pending Sync |
| Compact block list "< 5 Acres" total never reset between rows | CompactBlockListAdapter.bind | Flag accumulates across cards |
| Compact block dates shown as raw milliseconds | Compact block list | Unreadable dates |
Compact blocks before 2026-09-01 hidden by a hard-coded date | CompactBlockViewModel.kt:38 | Older blocks disappear |
| Default CB name "CB <count>" can collide; uniqueness only checked on rename | Compact block create | Duplicate names |
Settings language change stores the whole ["te","తెలుగు"] array | app/.../view/splash/SelectLanguageFragment.kt, LanguageUtil.kt | Server content in English while UI is Telugu |
| "Change VO" in load-more mode may not download the new VO | DataSetupActivity after SelectWorkNodeActivity | Old farmers stay until manual refresh (verify on device) |
| Synthetic work-node ids (village×100 + index) | data/.../setup/SetUpWorknode.kt | Collisions with ≥100 habitations or VOs |
Instructions sorted by instruction_order as text | Program instructions | "10" before "2" |
Name search lower-cases the name but not the query; phone search uses !! | app/.../view/ie/IeFragment.kt | Capitals find nothing; crash on missing phone |
| Model filter is multi-select but applied as if/else-if | Farmer list filter | Only the first ticked model applies |
| Photo height uses integer division | AddIeFragment photo compression | 0 or 100 px heights, distorted images |
Age focus check !b && ageInt < 18 || ageInt > 110 | AddIeFragment | Error shown while still typing |
checkLand() reads farmerCategory while the radio writes currentFarmerCategory | AddIeFragment | Newly changed category not seen |
| Father details stored in spouse columns, meaning decided by marital status at sync | fragment_add_ie.xml, IEMapper.kt | Data meaning flips if marital status changes |
| Land delete removes the local row before the server DELETE succeeds | Land asset delete | Land vanishes locally, stays on server |
| Leased-in edit saves per parcel and pops on the last callback | AddUpdateAssetFragment | Partial saves on mid-way failure |
| DKG creates a new record on every submit | AddDKGActivity | Several answers per month |
"Mark as resolved" overwrites reviewdBy timestamp with the user name | ResolveCommentsFragment | Review time lost |
| AAP PMDS-years check ignores 2024-25; A Grade warning text says C1 but code checks S2S | ActionPlanFragment2025To2026 | Valid input rejected; misleading message |
AAP isDataValid overwritten by the last edited field | ActionPlanFragment2025To2026 | Earlier invalid field can pass |
| Geofence accepts 1 point, any GPS accuracy, no area or self-intersection check | GeofencingMapActivity.kt | Unusable polygons |
AuthInterceptor re-logs in on HTTP 400 | data/.../net/AuthInterceptor.kt | Validation errors trigger re-login |
| Endorsement and analytics workers share one unique-work name | FetchEndorsementDataWorker.kt:84 | One refresh silently skips the other |
| DELETE answered "already deactivated" is retried forever | BaseResponseHandler path | Endless retries |
onSourceExpire arguments swapped | offline-core SyncQueueProcessor.java:276 | Dependents expire with wrong data |
| Queue priority ignored (sorted by create time first) | offline-android OperationDaoSqLiteImpl.java:183 | Priority has no effect |
| Notifications are never marked read | NotificationDao.markNotificationAsRead unused | Unread count only grows |
| Device conflict "Check Sync" clears the task stack | DeviceRegistrationActivity | Back exits the app |
RoFR tooltip compares a sub-category with a functionality constant | AssetMainAdapter.getToolTipString | Tooltip never shows |