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>
110 lines
4.0 KiB
Markdown
110 lines
4.0 KiB
Markdown
# 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
|
|
|
|
```xml
|
|
<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
|
|
|
|
- [x] Share opens the system sheet with a valid attachment
|
|
- [x] The shared file opens correctly in a receiving app
|
|
- [ ] SAF save writes to the chosen location — **not implemented**; share only
|
|
- [x] No `FileUriExposedException`
|
|
- [x] No storage permission added to the manifest
|
|
- [x] Generation is off the main thread with a progress indicator
|
|
- [x] 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.
|