# GPS2Audio v2.0.2 — Drag-and-Drop Waypoint Reorder ## Artifacts - Signed APK: `/home/user/workspace/GPS2Audio_v2_0_2_drag_reorder_signed.apk` - Source ZIP (no secrets): `/home/user/workspace/GPS2Audio_v2_0_2_drag_reorder_source.zip` - This handoff: `/home/user/workspace/GPS2Audio_v2_0_2_drag_reorder_handoff.md` ## Version / signing - `applicationId`: `de.waypointaudio` - `versionName`: `2.0.2` - `versionCode`: `30` (was 29 in v2.0.1) - Signed with the existing release keystore (`/home/user/workspace/GPS2Audio_release_keystore.jks`, alias `gps2audio_release`). - Signing certificate (verified with `apksigner verify --print-certs`): - DN: `CN=GPS2Audio, OU=NesoHub, O=GPS2Audio, L=Germany, C=DE` - SHA-256: `7B:0E:A4:D5:52:86:05:17:3D:29:9D:7B:26:8E:7A:B8:76:8D:D5:04:EC:15:B6:6A:52:D4:7B:42:57:B3:BB:46` - SHA-1: `EB:92:2A:83:39:09:ED:6B:2D:CB:62:52:F0:CF:42:5F:26:DF:25:AD` → identical to v2.0.0 / v2.0.1, so installs upgrade in place from any earlier `de.waypointaudio` build. - `aapt dump badging` confirmed: `package: name='de.waypointaudio' versionCode='30' versionName='2.0.2'`. - `gradle :app:assembleRelease` → BUILD SUCCESSFUL (only pre-existing deprecation warnings: `launchWhenStarted`, OSMDroid setters, `Modifier.menuAnchor()` — unchanged from v2.0.1). ## Headline change — direct drag-and-drop reordering Replaces the per-card `▲▼` IconButton column from v2.0.1 with a true drag-and-drop gesture on every waypoint card. The user asked to switch *directly* to drag-and-drop control; the arrow column was removed rather than augmented. ### Drag interaction (user-facing) - Every Waypoint-Track card now shows a left-edge **drag handle** (`Icons.Filled.DragIndicator`). German content description: `Ziehgriff zum Verschieben` (`R.string.drag_handle_cd`). - Two ways to start a reorder: 1. **Press the drag handle** and drag (immediate, no long-press). The handle has its own `pointerInput` block — it doesn't compete with the outer `verticalScroll`, so a press on the handle never starts a page scroll. 2. **Long-press anywhere on the card** and drag. This uses `detectDragGesturesAfterLongPress` on the whole card and exists as an accessibility / touch-target alternative; the long-press threshold protects normal vertical scrolling. - While dragging: - The dragged card is lifted via `graphicsLayer.translationY`, elevated to 8.dp, recoloured to `primaryContainer @ 0.65f`, and pulled above its siblings with `zIndex(1f)`. - The other cards in the affected range shift by their own measured height (+/- pixels via `graphicsLayer.translationY`) so the gap appears at the prospective drop position. Reordering target is computed from cumulative drag offset and the per-item heights measured via `onGloballyPositioned`, with a half-height threshold per neighbour (see `DragReorderState.computeTargetIndex`). - On finger up, `WaypointViewModel.moveWaypoint(from, to)` is called once. That goes through the existing repository.saveAll path, so the final order is persisted exactly the same way the old arrow buttons persisted theirs. Cancelling the drag (e.g. system back / system cancel) snaps the items back without calling moveWaypoint. - Reorder is **disabled while** the GPS service is running OR a track recording is active (`reorderBlocked = serviceRunning || isRecording`): - Both pointerInput blocks short-circuit to `Modifier` so no drag can start. - The DragIndicator icon is shown at alpha 0.20 to read as disabled. - The pre-existing concise hint `Sortieren während GPS/Aufzeichnung deaktiviert` (`R.string.reorder_blocked_hint_short`) continues to appear at the right of the `Wegpunkt-Tracks` heading when more than one waypoint exists. ### Implementation summary All code touch points: - `app/src/main/kotlin/de/waypointaudio/ui/WaypointListScreen.kt` - Added imports: `detectDragGestures`, `detectDragGesturesAfterLongPress`, `graphicsLayer`, `pointerInput`, `onGloballyPositioned`, `semantics`/`contentDescription`, `zIndex`, `kotlin.math.abs`, `kotlin.math.roundToInt`. - Replaced the `WaypointCard` arrow column with a drag handle Box containing `Icons.Filled.DragIndicator`. - Wired both gesture detectors: * `detectDragGestures` on the handle (immediate, no long-press). * `detectDragGesturesAfterLongPress` on the card surface (accessibility alternative). - Added a per-list `DragReorderState` (held in a `remember { }` slot inside the list block via the `rememberDragReorderState` helper). The state tracks: `itemCount`, `draggingIndex`, `dragOffsetPx`, a `mutableStateMapOf` of item heights, and exposes `onDragStart/onDrag/onDragEnd/onDragCancel`, `computeTargetIndex`, `shiftDirectionFor`, `itemHeightFor`. - The `WaypointCard` Composable now takes `index: Int, reorderState: DragReorderState?` instead of the old `canMoveUp / canMoveDown / onMoveUp / onMoveDown` parameters. - Card root applies `graphicsLayer.translationY` (drag offset or sibling shift), `zIndex` for the dragged card, and `onGloballyPositioned` to report height to the state. - `app/src/main/res/values/strings.xml` - Added `drag_handle_cd = "Ziehgriff zum Verschieben"`. - Added `drag_reorder_hint_short = "Ziehgriff halten und verschieben"` (kept for future use; not displayed yet — heading stays compact). - `app/build.gradle.kts` - `versionCode = 30`, `versionName = "2.0.2"`. ### Why this approach (tradeoffs) - **Column, not LazyColumn.** The Waypoint list shares the page's outer `verticalScroll`. Switching the inner list to a LazyColumn inside a scrolling Column is illegal (nested scroll) without significant re-architecture of the whole page, which carries regression risk for Atmo / PTT / map / draft / playlist features. The drag state is therefore implemented over the existing `Column` with `onGloballyPositioned` height measurement and `graphicsLayer` translation — minimal invasiveness, no new dependencies. - **Dedicated handle + long-press fallback.** Using only long-press delays the gesture; using only direct drag on the card surface competes with `verticalScroll`. A dedicated handle with its own `pointerInput` solves both: page scrolling stays untouched, the gesture starts immediately when the handle is grabbed, and long-press is still available for users who don't realise the handle exists. This is the same UX shape Google uses in Material reorder lists. - **No new external library.** No `reorderable-compose`, no `androidx.compose.foundation` snapshot dependency; only stable Compose Foundation APIs already in BOM. - **Persist on drop.** `viewModel.moveWaypoint(from, to)` is called exactly once on drag end, so the existing `repository.saveAll` pipeline is invoked once per drag — same persistence semantics as the v2.0.1 arrow taps. The half-neighbour-height threshold keeps the drop position visually predictable. - **Tour ordering dialog unchanged.** The `Touren ordnen` compact dialog still uses `▲▼` arrows (task says this is acceptable; tours list is short, dialog is rarely used). ## Preserved features (regression checklist) Verified by reading the source — no other files were touched: - [x] `applicationId = de.waypointaudio`, signing config + keystore properties path unchanged → upgrade-in-place from v2.0.0 / v2.0.1. - [x] Wake-Lock settings (`Theme`/`MainActivity`/`WakeLockController`): untouched. - [x] Map provider selection (`MapProvider`, `MapProviderDialog`, `MapScreen`): untouched. - [x] Atmo layout (Begleitmusik mini-player above the track list, `BegleitmusikMiniPlayer` + `BegleitmusikDialog`) and Atmo resume manager: untouched. - [x] Compact top bar / `ScrollableTabRow` IndexOutOfBounds fix: untouched (`tabs.joinToString("|")` key still in place). - [x] Track-Editor / Drafts (`TrackDraftEditorScreen`, `TrackDraftListScreen`, `TrackDraft`, `TrackRecordingManager`): untouched. - [x] Backup import/export (`BackupImportManager`, `BackupExportManager`, `ImportExportManager`): untouched. - [x] Map search (`PlaceSearchDialog`, `NominatimClient`): untouched. - [x] Audio library playlist (`TourMusicStore`, `BackgroundMusicManager`, `BackgroundMusicPlayer`): untouched. - [x] PTT Atmo resume (`LivePttManager`, `LivePttService`, `AtmoResumeManager`): untouched. - [x] OSM attribution: untouched. - [x] TabRow fix: untouched. - [x] About PayPal / version notice (`AboutDialog`, `VersionNoticeDialog`, `VersionNoticeStore`, `AppVersion`): untouched. `AppVersion` reads version at runtime from the PackageManager, so the About dialog will show `2.0.2 (30)` automatically. ## Regression checklist (manual smoke) When installing the APK over v2.0.1 on a real device, please verify: 1. App launches; About dialog reads `2.0.2 (30)`. 2. Tab row scrolls horizontally; adding / renaming / deleting tours does not crash the tab row. 3. Waypoint list shows the DragIndicator handle on every card. 4. Press handle, drag a card up/down past at least 2 neighbours, let go — the order is updated and survives an app restart. 5. Long-press a card body (not the handle) for ~500 ms, drag, drop — same result. 6. Start GPS service (▶ in top bar). Handles fade out; pressing them or long-pressing a card does nothing. The hint `Sortieren während GPS/Aufzeichnung deaktiviert` appears. 7. Stop GPS, start a track recording. Same disabled behaviour. 8. While not reordering, normal vertical page scroll still works (PTT → Atmo → manual player → track list → spacer). 9. Atmo mini-player still plays / pauses / next / prev. 10. Manual player bar still plays / pauses / next / prev. 11. Map screen opens, OSM tiles render, attribution still visible, map provider dialog still works. 12. Track-Editor / drafts: open from FAB / overflow, recording flow still works. 13. Backup import + export still produce/consume the same JSON / ZIP shapes. 14. Map search dialog (Nominatim) still returns results. 15. PayPal link in About still opens correctly. ## Build / signature result - `gradle :app:assembleRelease` → **BUILD SUCCESSFUL** in 47s. - Warnings: only pre-existing deprecations (`launchWhenStarted`, OSMDroid setters, `Modifier.menuAnchor()`). - `apksigner verify --print-certs`: * Signer #1 DN matches v2.0.1. * SHA-256 matches v2.0.1 (`7B:0E:A4:…:B3:BB:46`). - `aapt dump badging`: * `package: name='de.waypointaudio' versionCode='30' versionName='2.0.2'` * `sdkVersion:'26'`, `targetSdkVersion:'35'` * `application-label:'GPS2Audio'`