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>
This commit is contained in:
109
rippr-src/docs/v2/16-export-ui.md
Normal file
109
rippr-src/docs/v2/16-export-ui.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user