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:
2026-08-11 08:30:25 -05:00
parent 4debdfa518
commit 280fd7f988
106 changed files with 10377 additions and 0 deletions

View 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.