Files
samplez/rippr-src/docs/v2/16-export-ui.md
uhryniuk 280fd7f988 Add Rippr source snapshot and full-history bundle
Two copies for two jobs. rippr-src/ is a browsable git archive export of
the tracked tree at 2f76983 - no build outputs, no local.properties, no
nested .git - which is convenient to read in gitea but carries no history
and will drift.

rippr-full-history.bundle is the real backup: all 18 commits, verified as
"records a complete history" and test-cloned before committing. This
matters because ~/dojo/rippr has no git remote and otherwise exists only
on one machine.

rippr-src/SNAPSHOT.md explains the difference and how to restore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 08:30:25 -05:00

4.0 KiB

T16 — Export UI

Phase 6 · Depends on T15 · Status Done

Goal

Get an exported ride off the phone — share sheet or save to a chosen location. Done when a GPX can be shared to another app or written to storage and opens correctly there.

Context

T15 produces strings; this task moves them somewhere useful. Two paths cover the real uses:

  • Share — send to Strava, Drive, email, Slack. The common case.
  • Save — write to Downloads or a chosen folder via SAF. For getting it onto a computer.

FileProvider is required — since Android 7 a raw file:// URI in an Intent throws FileUriExposedException. The app currently has no <provider> declared and no file_paths.xml; both are new.

Design

FileProvider

<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data android:name="android.support.FILE_PROVIDER_PATHS"
               android:resource="@xml/file_paths" />
</provider>

Files are written to cacheDir/exports/, exposed as a cache-path. Cache is right: these are transient handoffs, and the system can reclaim them. Clear stale exports on app start so the directory does not grow unbounded.

FLAG_GRANT_READ_URI_PERMISSION on the share Intent, or the receiving app gets a SecurityException.

Save via SAF

ActivityResultContracts.CreateDocument("application/gpx+xml") returns a user-chosen URI; write through contentResolver.openOutputStream. No storage permission needed, which is the point of SAF.

Filename

rippr-2026-08-10-1432.gpx — sortable, unambiguous, no spaces. Derived from startedAt in the device timezone (the filename is for a human, unlike the timestamps inside the file, which are UTC).

UI

Export action on trip detail, offering format (GPX / GeoJSON) then destination (Share / Save). Two small choices rather than four buttons.

Generation happens off the main thread with a progress indicator — a large ride is a few MB of string building.

Implementation

  1. Add the <provider> to AndroidManifest.xml and create res/xml/file_paths.xml.
  2. export/ExportManager.kt: generate on IO, write to cacheDir/exports/, return a FileProvider URI.
  3. Share via Intent.ACTION_SEND with the correct MIME type and read permission flag.
  4. SAF save via CreateDocument.
  5. Export UI on trip detail with format and destination choice.
  6. Clear cacheDir/exports/ on app start.
  7. MIME types: application/gpx+xml, application/geo+json.

Acceptance criteria

  • Share opens the system sheet with a valid attachment
  • The shared file opens correctly in a receiving app
  • SAF save writes to the chosen location — not implemented; share only
  • No FileUriExposedException
  • No storage permission added to the manifest
  • Generation is off the main thread with a progress indicator
  • Stale exports cleared on start

Scope reduced. Only the share sheet shipped. Saving via SAF was dropped: the share sheet already reaches Drive, Files, email and Strava, which covers getting a ride off the phone. Add SAF if a real need appears.

Tests

Instrumented: ExportManager produces a readable FileProvider URI whose content matches GpxWriter.write exactly; cache clearing works.

Manual on emulator: share to a file manager, pull the file with adb pull, and diff it against locally-generated output.

Risks / gotchas

  • FileUriExposedException is the failure mode if FileProvider is skipped — it throws at share time, not build time.
  • Missing FLAG_GRANT_READ_URI_PERMISSION produces a SecurityException in the receiving app, which reads as that app being broken.
  • Authority must be unique — ${applicationId}.fileprovider guarantees it.
  • Do not write to external storage directly. SAF exists to avoid that permission.

Out of scope

Cloud upload, auto-export on trip completion, import.