Files
textbee/android/MIGRATION.md
isra el 243bbdd1d0 feat(android): complete Kotlin migration, UI overhaul, and dashboard polish
Kotlin migration (phases 3-5):
- Port all DTOs, helpers, models, workers, receivers, and services from Java to Kotlin
- Room DB files ported to Kotlin with all logic kept commented out (not yet enabled)
- Add SMSFilterScreen and SMSFilterViewModel in Compose (replaces Java SMSFilterActivity in new UI)
- All helpers exposed as Kotlin objects with @JvmStatic for Java interop

Onboarding improvements:
- Rewrote copy on all 5 onboarding screens (Welcome, Credentials, DeviceSetup, Permissions, SetupComplete)
- Added receive SMS toggle on SetupCompleteScreen (defaults on)
- Gateway now set to enabled by default after successful registration

Dashboard improvements:
- Add receive SMS toggle and SIM subscription ID display in device card
- Add permission warning card when SMS permissions are missing
- Remove all-time stats section (replaced by subscription usage bars)
- Trim Quick Actions to Dashboard + Docs; move Get Support and Share to Settings
- Merge user greeting into TopAppBar subtitle
- Device ID now has inline copy button

Dashboard UI polish (community review fixes):
- Redundant Enabled badge removed; only shows when gateway is Disabled
- Add Gateway label below the main switch for clarity
- Replace infinity symbol with Unlimited in usage display
- SIM subscription IDs use neutral color instead of primary/orange
- Status bar matches background color instead of primary orange

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 08:57:16 +03:00

192 lines
11 KiB
Markdown

# Android Kotlin/Compose Migration
## Overview
TextBee Android is mid-migration from a Java/XML legacy codebase to Kotlin + Jetpack Compose. The new UI runs in parallel with the legacy UI — users can switch between them via Settings. The SplashActivity routes to the appropriate UI on launch.
---
## What's Done
### Theme
| File | Notes |
|---|---|
| `ui/theme/Color.kt` | Brand orange (`#C4620A`), full light/dark palette |
| `ui/theme/Theme.kt` | `TextBeeTheme` wrapper, `dynamicColor = false` to preserve brand color |
| `ui/theme/Type.kt` | Material3 typography scale |
### Infrastructure
| File | Notes |
|---|---|
| `ApiManagerKt.kt` | Kotlin singleton Retrofit client, mirrors `ApiManager.java` |
| `services/GatewayApiServiceKt.kt` | Kotlin suspend-function Retrofit interface (all new UI endpoints) |
| `dtos/GatewayStatsResponse.kt` | Nullable stats DTO |
| `dtos/SubscriptionResponse.kt` | Plan, usage, renewal DTO |
| `dtos/UserProfileResponse.kt` | `name`, `email` from `/auth/who-am-i` |
| `dtos/MessagesResponse.kt` | `SmsMessage`, `PaginationMeta`, `SendSmsRequest` |
### UI — Onboarding
| File | Notes |
|---|---|
| `ui/onboarding/OnboardingActivity.kt` | Compose NavHost shell |
| `ui/onboarding/OnboardingViewModel.kt` | Registration state + API calls |
| `ui/onboarding/screens/WelcomeScreen.kt` | Get Started / I have a Device ID |
| `ui/onboarding/screens/CredentialsScreen.kt` | QR scan + manual API key entry |
| `ui/onboarding/screens/DeviceSetupScreen.kt` | Register or reconnect device |
| `ui/onboarding/screens/PermissionsScreen.kt` | SMS + phone state permissions |
| `ui/onboarding/screens/SetupCompleteScreen.kt` | Success + navigate to dashboard |
| `ui/onboarding/screens/OnboardingComponents.kt` | Shared step indicator, etc. |
### UI — Main App
| File | Notes |
|---|---|
| `ui/splash/SplashActivity.kt` | Routes to legacy/onboarding/main based on SharedPrefs |
| `ui/main/NewMainActivity.kt` | Bottom nav (Dashboard / Messages / Settings) + compose + filters routes |
| `ui/dashboard/DashboardScreen.kt` | Device card, stats, subscription, quick actions |
| `ui/dashboard/DashboardViewModel.kt` | Stats, subscription, user profile, gateway toggle |
| `ui/messages/MessagesScreen.kt` | Filter chips, message list, detail dialog, FAB |
| `ui/messages/MessagesViewModel.kt` | Paginated message fetch, filter state |
| `ui/messages/ComposeScreen.kt` | Multi-recipient input, message field, send with snackbar feedback |
| `ui/messages/ComposeViewModel.kt` | Send SMS, error body parsing, success/error state |
| `ui/settings/SettingsScreen.kt` | Account, Gateway, SMS, Legal, System, UI sections |
| `ui/settings/SettingsViewModel.kt` | Device name save, gateway toggle, SIM picker |
### UI — Settings (Phase 3) ✅
| File | Notes |
|---|---|
| `ui/settings/SMSFilterScreen.kt` | Full Compose SMS filter screen: enable switch, allow/block mode chips, rule list with FAB, add/edit dialog |
| `ui/settings/SMSFilterViewModel.kt` | `AndroidViewModel` — loads/saves `FilterConfig` via StateFlow; deep-copies config on each mutation |
`SMSFilterActivity.java` is still present for the legacy UI. The new UI navigates to the `"filters"` composable route inside `NewMainActivity`; bottom bar is hidden on both `"compose"` and `"filters"` routes.
### Data Layer — Kotlin Stubs (Phase 4) ✅
**Room DB (block-commented — feature not yet enabled):**
| File | Notes |
|---|---|
| `database/local/Sms.kt` | Replaces `SMS.java`; `@Entity` data class inside `/* */` block comment |
| `database/local/SmsDao.kt` | Replaces `SMSDao.java`; `@Dao` interface with suspend funs, block-commented |
| `database/local/AppDatabase.kt` | Replaces `AppDatabase.java`; singleton with companion object, block-commented |
| `database/local/DateConverter.kt` | Replaces `DateConverter.java`; `object` with `@TypeConverter`, block-commented |
**DTOs (all Java originals deleted):**
| File | Notes |
|---|---|
| `dtos/RegisterDeviceInputDTO.kt` | `class` with `var`; `@get:JvmName("isEnabled")` so Java/Kotlin callers get `isEnabled()` not `getEnabled()` |
| `dtos/RegisterDeviceResponseDTO.kt` | Regular `class` with `@JvmField var``MainActivity.java` accesses `.data`/`.error` as direct fields |
| `dtos/SMSDTO.kt` | Regular `class`; `message: String = ""` to avoid null default |
| `dtos/HeartbeatInputDTO.kt` | `var isCharging: Boolean?` (nullable) — generates `getIsCharging()`/`setIsCharging()` matching Java non-standard getter name |
| `dtos/HeartbeatResponseDTO.kt` | `@JvmField var` on all properties — `HeartbeatHelper` accesses fields directly (`.fcmTokenUpdated`) |
| `dtos/SimInfoDTO.kt` | `subscriptionId: Int = 0` (was primitive in Java) |
| `dtos/SimInfoCollectionDTO.kt` | `sims: MutableList<SimInfoDTO>? = null` |
| `dtos/SMSForwardResponseDTO.kt` | Empty class body |
### Helpers & Models (Phase 5) ✅
All helpers are Kotlin `object` with `@JvmStatic` on every public method — at the time of porting, Java workers/receivers called them; those callers were ported in Phase 6.
| File | Notes |
|---|---|
| `helpers/SharedPreferenceHelper.kt` | Replaces `SharedPreferenceHelper.java`; `PREF_FILE = "PREF"`, 7 methods |
| `helpers/SMSFilterHelper.kt` | Replaces `SMSFilterHelper.java`; nested `FilterMode` enum + `FilterConfig` class; Gson-compatible field names |
| `helpers/SMSHelper.kt` | Replaces `SMSHelper.java`; `FLAG_MUTABLE` on API >= S; private PendingIntent helpers |
| `helpers/HeartbeatHelper.kt` | Replaces `HeartbeatHelper.java`; `CountDownLatch` FCM token wait, `@Suppress("DEPRECATION")` for legacy network API |
| `helpers/HeartbeatManager.kt` | Replaces `HeartbeatManager.java`; `PeriodicWorkRequest.Builder(HeartbeatWorker::class.java, ...)` |
| `models/SMSFilterRule.kt` | Replaces `SMSFilterRule.java`; `@JvmOverloads constructor` for Java callers; nested `MatchType` + `FilterTarget` enums |
| `models/SMSPayload.kt` | Replaces `SMSPayload.java`; keeps legacy `receivers` + `smsBody` fields |
---
## What's Left (Java / Legacy)
### Core App
| File | Priority | Notes |
|---|---|---|
| `AppConstants.java` | Low | Constants only — convert when touching other things |
| `SMSGatewayApplication.java` | Low | Application class, minimal logic |
| `TextBeeUtils.java` | Medium | Heavily used utility; convert once helpers are stable |
| `ApiManager.java` | Low | Still used by legacy UI; delete after legacy removal |
### Activities (Legacy UI)
| File | Priority | Notes |
|---|---|---|
| `activities/MainActivity.java` | High | Legacy main UI — remove after full Compose rollout |
| `activities/SMSFilterActivity.java` | Medium | Legacy filter screen — still reachable from legacy UI only |
### Helpers
| File | Priority | Notes |
|---|---|---|
| `helpers/VersionTracker.java` | Low | Update check logic — left for later |
### Services
| File | Priority | Notes |
|---|---|---|
| `services/GatewayApiService.java` | High | Java Retrofit interface — delete after legacy UI removed |
---
## Migration Roadmap
### Phase 3 — SMS Filter Screen ✅ Complete
Ported `SMSFilterActivity.java` to `SMSFilterScreen.kt` (Compose). Integrated as a nested `"filters"` route inside `NewMainActivity`. Legacy `SMSFilterActivity.java` unchanged — still reachable from legacy UI.
---
### Phase 4 — Data Layer ✅ Complete
All DTOs ported to Kotlin; Java originals deleted. Room DB ported to Kotlin stubs with all logic still inside `/* */` block comments (feature remains disabled).
---
### Phase 5 — Helpers & Utilities ✅ Complete
All helpers and models ported to Kotlin `object`s with `@JvmStatic`. Java originals deleted. Java callers (workers, receivers — Phase 6) continue to work unchanged via `@JvmStatic` interop.
---
### Phase 6 — Background Services & Receivers ✅ Complete
All workers, receivers, and services ported to Kotlin; Java originals deleted.
| File | Notes |
|---|---|
| `receivers/BootCompletedReceiver.kt` | Restarts sticky notification + schedules heartbeat on boot |
| `receivers/SMSBroadcastReceiver.kt` | Deduplication fingerprint cache; Kotlin property access on `SMSDTO` |
| `receivers/SMSStatusReceiver.kt` | `setFailed()` private helper avoids `errorMessage` property shadowing |
| `workers/HeartbeatWorker.kt` | Simple `Worker` subclass; delegates to `HeartbeatHelper` |
| `workers/SmsSendWorker.kt` | SIM resolution priority chain; `Thread.sleep` rate limiting |
| `workers/SMSReceivedWorker.kt` | Fingerprint-based unique work name for deduplication |
| `workers/SMSStatusUpdateWorker.kt` | Exponential backoff, max 5 retries |
| `services/StickyNotificationService.kt` | Broad `Exception` catch replaces API-31-only `ForegroundServiceStartNotAllowedException` |
| `services/FCMService.kt` | Handles `heartbeat_check` type + SMS payload dispatch |
**Sticky notification fix**: Added service restart to `DashboardViewModel.loadLocalState()` — on every app launch, if gateway + sticky notification are enabled, the service is restarted. This matches legacy `MainActivity` behaviour and fixes the notification disappearing after Android kills the service on newer OS versions.
---
### Phase 7 — Legacy UI Removal
Once Compose UI is stable and rolled out to all users, remove the legacy UI entirely.
**Steps:**
1. Remove the "Switch to Legacy UI" row from `SettingsScreen.kt`
2. Remove `USE_NEW_UI_KEY` logic from `SplashActivity.kt` (always route to new UI)
3. Delete `activities/MainActivity.java` and its XML layouts
4. Delete `activities/SMSFilterActivity.java`
5. Delete `services/GatewayApiService.java` (Java Retrofit interface)
6. Delete `ApiManager.java`
7. Remove "Try New UI" button from any remaining legacy layout XML
8. Clean up `AppConstants.java` — remove `SHARED_PREFS_USE_NEW_UI_KEY`
9. Convert `SMSGatewayApplication.java` → Kotlin
---
## Key Constraints to Keep in Mind
- **`dynamicColor = false`** in `Theme.kt` — Material You overrides the brand orange on Android 12+; must stay false
- **`primaryContainer` avoided** in TopAppBar/nav — causes orange-on-orange in dark mode; use `surface` for bars, `surfaceVariant` for nav indicator
- **Java/Kotlin interop** — Only `ApiManager.java`, `TextBeeUtils.java`, legacy activities, and `GatewayApiService.java` remain Java; all others are Kotlin
- **WorkManager workers** — kept as `Worker` subclass (not `CoroutineWorker`) to avoid adding `work-runtime-ktx`; straightforward conversion candidate in a future cleanup
- **Sticky notification on Android 12+** — `ForegroundServiceStartNotAllowedException` is caught broadly; `DashboardViewModel` restarts service on every launch to compensate for OS killing it in the background
- **Room DB** — all DB logic remains commented out; do not uncomment until the feature is explicitly re-enabled
- **`@JvmField`** on `HeartbeatResponseDTO``HeartbeatHelper.kt` accesses `.fcmTokenUpdated`/`.name` as fields; `@JvmField` keeps direct field access instead of generating getters
- **`@JvmField`** on `RegisterDeviceResponseDTO``MainActivity.java` (legacy, still Java) accesses `.data`/`.error` as direct fields; remove once Phase 7 deletes the legacy activity
- **`@JvmOverloads`** on `SMSFilterRule` — generates no-arg and partial constructors needed by Gson deserialization of persisted filter config JSON
- **`@get:JvmName("isEnabled")`** on `RegisterDeviceInputDTO.enabled` and `@get:JvmName("isCaseSensitive")`on `SMSFilterRule.caseSensitive` — renames generated getter to match Java boolean convention; `MainActivity.java` and `SMSFilterActivity.java` use `isEnabled()`/`isCaseSensitive()`