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>
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
- Add the
<provider>toAndroidManifest.xmland createres/xml/file_paths.xml. export/ExportManager.kt: generate on IO, write tocacheDir/exports/, return aFileProviderURI.- Share via
Intent.ACTION_SENDwith the correct MIME type and read permission flag. - SAF save via
CreateDocument. - Export UI on trip detail with format and destination choice.
- Clear
cacheDir/exports/on app start. - 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
FileUriExposedExceptionis the failure mode ifFileProvideris skipped — it throws at share time, not build time.- Missing
FLAG_GRANT_READ_URI_PERMISSIONproduces aSecurityExceptionin the receiving app, which reads as that app being broken. - Authority must be unique —
${applicationId}.fileproviderguarantees 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.