• v2.0.2 b2aa1c167c

    marcel released this 2026-05-15 14:30:30 +02:00 | 7 commits to main since this release

    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<Int,Int> 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:

    • applicationId = de.waypointaudio, signing config + keystore
      properties path unchanged → upgrade-in-place from v2.0.0 / v2.0.1.
    • Wake-Lock settings (Theme/MainActivity/WakeLockController):
      untouched.
    • Map provider selection (MapProvider, MapProviderDialog,
      MapScreen): untouched.
    • Atmo layout (Begleitmusik mini-player above the track list,
      BegleitmusikMiniPlayer + BegleitmusikDialog) and Atmo resume
      manager: untouched.
    • Compact top bar / ScrollableTabRow IndexOutOfBounds fix:
      untouched (tabs.joinToString("|") key still in place).
    • Track-Editor / Drafts (TrackDraftEditorScreen,
      TrackDraftListScreen, TrackDraft, TrackRecordingManager):
      untouched.
    • Backup import/export (BackupImportManager,
      BackupExportManager, ImportExportManager): untouched.
    • Map search (PlaceSearchDialog, NominatimClient): untouched.
    • Audio library playlist (TourMusicStore,
      BackgroundMusicManager, BackgroundMusicPlayer): untouched.
    • PTT Atmo resume (LivePttManager, LivePttService,
      AtmoResumeManager): untouched.
    • OSM attribution: untouched.
    • TabRow fix: untouched.
    • 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:assembleReleaseBUILD 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'
    Downloads