Initial commit
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
632
README.md
Normal file
632
README.md
Normal file
@@ -0,0 +1,632 @@
|
|||||||
|
Pamietnik ist eine Lebens/Reisejournal (Android +Webapp + Go Server) — Single Source of Truth
|
||||||
|
|
||||||
|
1. Zielbild
|
||||||
|
Eine Android-App oder Webui erstellt ein Reisejournal, loggt Standortdaten im Background, cached offline, und lädt bevorzugt per HTTP zu einem Go-Server hoch; zusätzlich ist ein Datei-Export aus der App möglich.
|
||||||
|
|
||||||
|
Der Server stellt zusätzlich eine Website bereit (Login erforderlich), auf der pro Tag die gespeicherten Punkte angezeigt werden.
|
||||||
|
|
||||||
|
Das Backend soll basierend auf Logdaten Vorschläge für bestimmte Standorte machen (z. B. wenn man sich länger an einem Ort aufhält). Die Standortinformationen (Reverse-Geocoding/Place-Info) sollen über eine kostenlose API bezogen werden.
|
||||||
|
|
||||||
|
In der App soll es möglich sein, manuell Punkte hinzuzufügen (ohne GPS).
|
||||||
|
|
||||||
|
2. Festlegungen (Decisions)
|
||||||
|
DEC-CLIENT-01: Android (Kotlin), UI mit Jetpack Compose.
|
||||||
|
|
||||||
|
DEC-CLIENT-LANG-01: Android wird in Kotlin implementiert (Android „Kotlin-first").
|
||||||
|
|
||||||
|
DEC-LOC-01: Standortlogging im Background ist erforderlich.
|
||||||
|
|
||||||
|
DEC-API-01: Server-Schnittstelle ist HTTP/REST (MQTT verworfen).
|
||||||
|
|
||||||
|
DEC-DOC-01: Alle wesentlichen Inhalte werden in Markdown gepflegt; Diagramme in Mermaid.
|
||||||
|
|
||||||
|
DEC-DB-01: Android lokal/offline: SQLite (über Room); Backend: PostgreSQL.
|
||||||
|
|
||||||
|
DEC-WEB-01: Server bietet Website mit Login; Anzeige der Trackpoints pro Tag.
|
||||||
|
|
||||||
|
DEC-AUTH-01: Für die Website wird Session-basierte Auth (Session Cookie) bevorzugt; JWT ist optional für spätere Erweiterungen (z. B. API-first/SSO).
|
||||||
|
|
||||||
|
DEC-MAP-01: Für Kartenanzeige wird OpenStreetMap-Datenbasis genutzt, mit MapLibre als SDK/Renderer (Android) bzw. Web-Kartenbibliothek nach Wahl; Tile-Quelle wird konfigurierbar gehalten.
|
||||||
|
|
||||||
|
DEC-GEO-01: Für Reverse-Geocoding (kostenlos) wird initial Nominatim (OSM) eingeplant, aber nur mit striktem Caching/Rate-Limiting und „Provider austauschbar"-Design.
|
||||||
|
|
||||||
|
DEC-OPENAPI-01: Die Backend-HTTP-Schnittstelle wird als OpenAPI-Dokument gepflegt (OpenAPI 3.1, YAML oder JSON).
|
||||||
|
|
||||||
|
3. Produktanforderungen (PRD Requirements)
|
||||||
|
3.1 Background-Logging & Permissions
|
||||||
|
REQ-LOC-01: Die App MUSS Standort im Vordergrund und im Hintergrund erfassen können.
|
||||||
|
|
||||||
|
REQ-LOC-02: Manifest & Runtime: COARSE (und optional FINE) MUSS deklariert und zur Laufzeit angefragt werden; die App MUSS mit „approximate" funktionieren.
|
||||||
|
|
||||||
|
REQ-LOC-03: Background-Location: Für Hintergrundzugriff MUSS Background-Location berücksichtigt und korrekt angefordert werden (wenn Background-Logging aktiviert ist).
|
||||||
|
|
||||||
|
REQ-LOC-04: Falls Tracking als Foreground Service umgesetzt wird, MUSS beim Service der Location-Typ gesetzt werden (foregroundServiceType="location").
|
||||||
|
|
||||||
|
3.2 Offline-First, Upload bevorzugt
|
||||||
|
REQ-SYNC-01: Trackpoints MÜSSEN zuerst lokal persistiert werden (keine rein RAM-basierte Queue).
|
||||||
|
|
||||||
|
REQ-SYNC-02: Upload ist bevorzugter Weg; bei Server-Nichterreichbarkeit MUSS gecached werden und später automatisch nachgesendet werden (Retry).
|
||||||
|
|
||||||
|
REQ-SYNC-03: Hintergrund-Upload SOLL über WorkManager erfolgen, mit Constraints „Network connected" und Retry/Backoff.
|
||||||
|
|
||||||
|
REQ-SYNC-04: Retries dürfen keine Duplikate erzeugen; es MUSS eine Idempotenz-Strategie geben (event_id).
|
||||||
|
|
||||||
|
3.3 Manuelle Punkte
|
||||||
|
REQ-MAN-01: Die App MUSS es erlauben, manuell einen Punkt hinzuzufügen (mindestens: lat, lon, timestamp; optional: Name/Notiz).
|
||||||
|
|
||||||
|
REQ-MAN-02: Manuelle Punkte MÜSSEN lokal in SQLite/Room gespeichert werden, genauso wie GPS-Punkte.
|
||||||
|
|
||||||
|
REQ-MAN-03: Manuelle Punkte MÜSSEN in den Upload-Flow (Queue) integriert sein und zum Server hochgeladen werden (inkl. event_id).
|
||||||
|
|
||||||
|
REQ-MAN-04: Manuelle Punkte MÜSSEN validiert werden (z. B. Pflichtfelder, Wertebereich) und bei Fehlern im UI eine verständliche Fehlermeldung anzeigen; Validierung kann „as the user types" erfolgen.
|
||||||
|
|
||||||
|
REQ-MAN-05: Der Nutzer SOLL optional „aktuellen Zeitpunkt" und/oder „aktuelle Position" als Vorschlag übernehmen können, aber die Eingabe bleibt manuell editierbar.
|
||||||
|
|
||||||
|
3.4 Export (sekundär)
|
||||||
|
REQ-EXP-01: Nutzer MUSS Logs/Trips in eine Datei exportieren können.
|
||||||
|
|
||||||
|
REQ-EXP-02: Export MUSS über Storage Access Framework erfolgen (z. B. ACTION_CREATE_DOCUMENT; Nutzer wählt Speicherort, App schreibt in URI).
|
||||||
|
|
||||||
|
3.5 Server Website
|
||||||
|
REQ-WEB-01: Der Server MUSS eine Website bereitstellen, die nur nach Login zugänglich ist (Auth erforderlich).
|
||||||
|
|
||||||
|
REQ-WEB-02: Nach Login MUSS die Website pro Tag die gespeicherten Punkte anzeigen können (z. B. Tagesliste + Detailansicht eines Tages).
|
||||||
|
|
||||||
|
REQ-WEB-03: Die Website MUSS die Daten nutzerspezifisch anzeigen (ein Nutzer sieht nur seine eigenen Daten / Devices/Trips innerhalb seiner Berechtigung).
|
||||||
|
|
||||||
|
REQ-WEB-04: Die Website SOLL eine Kartenansicht bieten (zusätzlich zur Liste), um Punkte/Stops pro Tag visuell darzustellen.
|
||||||
|
|
||||||
|
3.6 Security / Auth
|
||||||
|
REQ-AUTH-01: Passwörter DÜRFEN nicht im Klartext gespeichert werden; Password Hashing MUSS sicher erfolgen (Argon2id bevorzugt).
|
||||||
|
|
||||||
|
REQ-AUTH-02: Session-basierte Auth (Cookie) ist erlaubt; Sessions SOLLEN serverseitig verwaltet werden, damit Sessions invalidierbar sind (Logout/Expire).
|
||||||
|
|
||||||
|
REQ-AUTH-03: Auth muss auch für Web-Query-Endpoints gelten (Tagesliste/Tagesdetail sind geschützt).
|
||||||
|
|
||||||
|
3.7 Vorschläge/Stops
|
||||||
|
REQ-SUG-01: Das Backend MUSS aus Trackpoints „Stops" erkennen können (Aufenthalte an ungefähr gleicher Position über eine Mindestdauer).
|
||||||
|
|
||||||
|
REQ-SUG-02: Das Backend MUSS aus Stops „Vorschläge" ableiten können.
|
||||||
|
|
||||||
|
REQ-SUG-03: Stops/Vorschläge MÜSSEN pro Nutzer/Device/Trip zuordenbar sein und in der Website pro Tag sichtbar sein (Liste + Karte).
|
||||||
|
|
||||||
|
3.8 Kostenlose Standortinfo-API
|
||||||
|
REQ-GEO-01: Reverse-Geocoding/Place-Info MUSS über einen kostenlosen Dienst erfolgen (initial Nominatim), mit Caching und klaren Limits/Retry-Strategie.
|
||||||
|
|
||||||
|
REQ-GEO-02: Geocoding-Provider MUSS austauschbar sein (z. B. via Server-Config/Proxy), ohne App-Update.
|
||||||
|
|
||||||
|
REQ-GEO-03: Bulk/periodische Reverse-Geocoding Requests SOLLEN vermieden werden; Geocoding wird bevorzugt ereignisbasiert (z. B. pro erkanntem Stop) und gecached.
|
||||||
|
|
||||||
|
3.9 Karten-Dienst
|
||||||
|
REQ-MAP-01: Kartenanzeige SOLL auf OpenStreetMap-Daten basieren; Rendering/SDK via MapLibre (Android) oder äquivalente Open-Source Web-Kartenkomponente.
|
||||||
|
|
||||||
|
REQ-MAP-02: Tile-Quelle MUSS konfigurierbar sein (später Self-hosting oder Provider-Wechsel möglich).
|
||||||
|
|
||||||
|
3.10 OpenAPI
|
||||||
|
REQ-OPENAPI-01: Die Backend-HTTP-Schnittstelle MUSS im OpenAPI-Format dokumentiert werden (OAS 3.1).
|
||||||
|
|
||||||
|
REQ-OPENAPI-02: Das OpenAPI-Dokument MUSS als YAML oder JSON im Repository versioniert werden (Entry-Point: openapi.yaml oder openapi.json).
|
||||||
|
|
||||||
|
REQ-OPENAPI-03: Das OpenAPI-Dokument MUSS alle implementierten Endpoints, Schemas (Request/Response), Fehlerformate und Security-Schemes enthalten.
|
||||||
|
|
||||||
|
REQ-OPENAPI-04: Cookie-basierte Auth für Web-Endpoints SOLL im OpenAPI über type: apiKey, in: cookie beschrieben werden (CookieAuth).
|
||||||
|
|
||||||
|
3.11 Google Play / Transparenz
|
||||||
|
REQ-PRIV-01: Bei Standortzugriff im Hintergrund MUSS eine klare In-App-Erklärung/Disclosure vorhanden sein; Background-Location ist zu begründen und sauber zu deklarieren.
|
||||||
|
|
||||||
|
3.12 Dokumentationsstil
|
||||||
|
REQ-DOC-STYLE-01: Alle wesentlichen Anforderungen, Architekturentscheidungen, API-Contracts, Testanforderungen und Tasks MÜSSEN in diesem Markdown gepflegt werden.
|
||||||
|
|
||||||
|
REQ-DOC-STYLE-02: Im Chat wird nur minimal kommentiert; Details stehen im Markdown.
|
||||||
|
|
||||||
|
REQ-DOC-STYLE-03: Änderungen erfolgen als fortlaufende Updates dieses Dokuments.
|
||||||
|
|
||||||
|
4. Architektur-Dokumentation (arc42-orientiert)
|
||||||
|
Die Architekturdokumentation MUSS Kontextsicht, Bausteinsicht, Laufzeitsicht und Verteilungssicht enthalten.
|
||||||
|
Alle Diagramme MÜSSEN Mermaid sein.
|
||||||
|
Architektur wird zuerst erstellt und abgestimmt, bevor Implementierung beginnt.
|
||||||
|
|
||||||
|
4.1 Kontextsicht
|
||||||
|
Fachlicher Kontext (Kommunikationspartner)
|
||||||
|
Benutzer mobil (startet/stopt Trips, sieht Status, exportiert, fügt manuell Punkte hinzu).
|
||||||
|
|
||||||
|
Benutzer web (loggt sich ein, sieht pro Tag Punkte/Stops/Vorschläge).
|
||||||
|
|
||||||
|
Android OS / Location Services (liefert Standortupdates).
|
||||||
|
|
||||||
|
Webserver (nimmt Trackpoints entgegen, berechnet Stops, stellt Website bereit).
|
||||||
|
|
||||||
|
Geocoding-Dienst (liefert Adress-/Place-Infos).
|
||||||
|
|
||||||
|
Dateisystem via SAF (Export-Ziel).
|
||||||
|
|
||||||
|
Technischer Kontext (Schnittstellen)
|
||||||
|
Android → Server: HTTPS/REST (JSON oder Protobuf, Entscheidung offen).
|
||||||
|
|
||||||
|
Browser → Server: HTTPS (Web UI, Login, Views).
|
||||||
|
|
||||||
|
Server → Geocoding: HTTPS (Reverse-Geocoding; Provider austauschbar, gecached).
|
||||||
|
|
||||||
|
Android → SAF: ACTION_CREATE_DOCUMENT und Schreiben in content:// URI.
|
||||||
|
|
||||||
|
text
|
||||||
|
flowchart LR
|
||||||
|
UserM[User Mobile] -->|Start/Stop Trip, Export, Manual point| App[Android App]
|
||||||
|
OS[Android Location Services] -->|Location updates| App
|
||||||
|
App -->|HTTPS REST: trackpoints| API[Go REST API]
|
||||||
|
API --> DB[(PostgreSQL)]
|
||||||
|
API -->|HTTPS reverse-geocode cached| GEO[Geocoding Provider]
|
||||||
|
UserW[User Web] -->|HTTPS| Web[Go Web UI]
|
||||||
|
Web --> API
|
||||||
|
App -->|SAF create document| SAF[Android Storage Access Framework]
|
||||||
|
SAF --> File[(Exported file)]
|
||||||
|
4.2 Bausteinsicht
|
||||||
|
Bausteine (initial):
|
||||||
|
|
||||||
|
Android App: UI (Compose), Domain/Use-Cases, Background Location Logging, Manuelle Punkt-Eingabe, Lokale DB + Upload-Queue, Upload Worker, Export, Map View (optional).
|
||||||
|
|
||||||
|
Go Server: REST Router/Handler, Validation, Idempotenz/Dedupe, Persistenz, Observability.
|
||||||
|
|
||||||
|
Go Web UI: Login, Tagesübersicht, Tagesdetail (Punktliste + Karte), Vorschläge.
|
||||||
|
|
||||||
|
Suggestion Engine: Stop Detection + Reverse-Geocoding + Vorschlagslogik.
|
||||||
|
|
||||||
|
Geocoding Adapter: Provider/Proxy + Cache/RateLimit.
|
||||||
|
|
||||||
|
text
|
||||||
|
flowchart TB
|
||||||
|
subgraph Mobile[Android App]
|
||||||
|
UI[Compose UI]
|
||||||
|
Domain[Domain / Use-Cases]
|
||||||
|
Loc[Background Location Logging]
|
||||||
|
Manual[Manual Point Entry + Validation]
|
||||||
|
Store[(SQLite/Room: Local DB + Upload Queue)]
|
||||||
|
Worker[Upload Worker]
|
||||||
|
Export[Export SAF]
|
||||||
|
UI --> Domain
|
||||||
|
Domain --> Loc
|
||||||
|
Domain --> Manual
|
||||||
|
Loc --> Store
|
||||||
|
Manual --> Store
|
||||||
|
Store --> Worker
|
||||||
|
Domain --> Export
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Server[Go Server]
|
||||||
|
API[HTTP REST API]
|
||||||
|
Val[Validation]
|
||||||
|
Idem[Idempotency/Dedupe]
|
||||||
|
Auth[Auth users sessions]
|
||||||
|
Web[Web UI login day view map]
|
||||||
|
Suggest[Stop Detection + Suggestions]
|
||||||
|
Geo[Geocoding Adapter + Cache]
|
||||||
|
SDB[(PostgreSQL)]
|
||||||
|
API --> Val --> Idem --> SDB
|
||||||
|
API --> Suggest --> SDB
|
||||||
|
Suggest --> Geo
|
||||||
|
Geo -->|HTTPS| Provider[Geocoding Provider]
|
||||||
|
Web --> Auth
|
||||||
|
Web --> API
|
||||||
|
Auth --> SDB
|
||||||
|
end
|
||||||
|
|
||||||
|
Worker -->|HTTPS Batch Upload| API
|
||||||
|
4.3 Laufzeitsicht (Szenarien)
|
||||||
|
Szenarien:
|
||||||
|
|
||||||
|
R1: Trip starten → Background-Logging → Trackpoints in lokale DB.
|
||||||
|
|
||||||
|
R2: Offline → Queue wächst → Online → Batch Upload → Ack → Queue markiert als sent.
|
||||||
|
|
||||||
|
R3: Retry/Duplicate → gleiche event_id erneut → Server erkennt Duplicate → kein doppelter Insert.
|
||||||
|
|
||||||
|
R4: Export → Nutzer wählt Datei → App schreibt Export.
|
||||||
|
|
||||||
|
R5: Web Login → Tagesübersicht → Tagesdetail (Punktliste + Karte).
|
||||||
|
|
||||||
|
R6: Stop erkannt → Reverse-Geocoding (cached) → Vorschlag wird gespeichert → Web zeigt Vorschlag.
|
||||||
|
|
||||||
|
R7: Nutzer fügt Punkt manuell hinzu → Validierung → SQLite/Room persistiert → Queue pending → später Upload.
|
||||||
|
|
||||||
|
text
|
||||||
|
sequenceDiagram
|
||||||
|
participant U as User
|
||||||
|
participant A as Android App
|
||||||
|
participant L as Location Services
|
||||||
|
participant D as Local DB Queue
|
||||||
|
|
||||||
|
U->>A: Start Trip
|
||||||
|
A->>L: Request location updates
|
||||||
|
L-->>A: Location update trackpoint
|
||||||
|
A->>D: Persist trackpoint pending
|
||||||
|
text
|
||||||
|
sequenceDiagram
|
||||||
|
participant A as Android App
|
||||||
|
participant W as WorkManager Worker
|
||||||
|
participant D as Local DB Queue
|
||||||
|
participant S as Go REST API
|
||||||
|
|
||||||
|
W->>D: Fetch pending batch
|
||||||
|
W->>S: POST /v1/trackpoints:batch
|
||||||
|
alt Server unreachable timeout
|
||||||
|
S--xW: Network error
|
||||||
|
W->>D: Keep pending; schedule retry
|
||||||
|
else Success
|
||||||
|
S-->>W: Ack accepted_ids rejected[]
|
||||||
|
W->>D: Mark accepted as sent; keep rejected with reason
|
||||||
|
end
|
||||||
|
text
|
||||||
|
sequenceDiagram
|
||||||
|
participant B as Browser
|
||||||
|
participant W as Go Web UI
|
||||||
|
participant A as Auth sessions
|
||||||
|
participant API as Go REST API
|
||||||
|
participant DB as PostgreSQL
|
||||||
|
|
||||||
|
B->>W: GET /login
|
||||||
|
B->>W: POST /login username + password
|
||||||
|
W->>A: Verify credentials + create session
|
||||||
|
A->>DB: Load user + verify hash
|
||||||
|
A-->>W: Session created
|
||||||
|
W-->>B: Set-Cookie session + redirect
|
||||||
|
|
||||||
|
B->>W: GET /days
|
||||||
|
W->>API: GET /v1/days auth
|
||||||
|
API->>DB: Query days group filter by date
|
||||||
|
API-->>W: days[]
|
||||||
|
W-->>B: Render days view
|
||||||
|
|
||||||
|
B->>W: GET /days/yyyy-mm-dd
|
||||||
|
W->>API: GET /v1/trackpoints?date=YYYY-MM-DD
|
||||||
|
API->>DB: Query trackpoints for date
|
||||||
|
API-->>W: trackpoints[]
|
||||||
|
W-->>B: Render day detail
|
||||||
|
text
|
||||||
|
sequenceDiagram
|
||||||
|
participant W as WorkManager Worker
|
||||||
|
participant S as Go REST API
|
||||||
|
participant DB as PostgreSQL
|
||||||
|
participant SE as Suggestion Engine
|
||||||
|
participant G as Geocoding Adapter Cache
|
||||||
|
participant P as Geocoding Provider
|
||||||
|
|
||||||
|
W->>S: POST /v1/trackpoints:batch
|
||||||
|
S->>DB: Persist trackpoints
|
||||||
|
S->>SE: Trigger stop detection async
|
||||||
|
SE->>DB: Read recent points
|
||||||
|
SE->>G: Reverse-geocode stop if not cached
|
||||||
|
alt Cache hit
|
||||||
|
G-->>SE: Place info cached
|
||||||
|
else Cache miss
|
||||||
|
G->>P: HTTPS reverse-geocode
|
||||||
|
P-->>G: Place info
|
||||||
|
G-->>SE: Place info
|
||||||
|
end
|
||||||
|
SE->>DB: Persist stop + suggestion
|
||||||
|
text
|
||||||
|
sequenceDiagram
|
||||||
|
participant U as User
|
||||||
|
participant A as Android App
|
||||||
|
participant V as Validation ViewModel
|
||||||
|
participant D as SQLite Room Queue
|
||||||
|
participant W as Upload Worker
|
||||||
|
participant S as Go REST API
|
||||||
|
|
||||||
|
U->>A: Add manual point lat/lon/time/note
|
||||||
|
A->>V: Update fields as user types
|
||||||
|
V-->>A: Show validation errors or OK
|
||||||
|
A->>D: Persist trackpoint source=manual pending
|
||||||
|
W->>D: Fetch pending batch
|
||||||
|
W->>S: POST /v1/trackpoints:batch
|
||||||
|
S-->>W: Ack
|
||||||
|
W->>D: Mark sent
|
||||||
|
4.4 Verteilungssicht
|
||||||
|
text
|
||||||
|
flowchart LR
|
||||||
|
Phone[Android Device] -->|Internet HTTPS| Edge[Reverse Proxy LB optional]
|
||||||
|
Browser[Web Browser] -->|Internet HTTPS| Edge
|
||||||
|
Edge --> API[Go API Service REST + Web UI]
|
||||||
|
API --> DB[(PostgreSQL)]
|
||||||
|
API --> GEO[Geocoding Adapter Proxy + Cache]
|
||||||
|
GEO -->|HTTPS| Provider[Geocoding Provider]
|
||||||
|
5. HTTP API (REST) — Vertrag (Draft)
|
||||||
|
5.1 Ziele
|
||||||
|
Single-Point ingest und Batch ingest.
|
||||||
|
|
||||||
|
Idempotenz über event_id (mind. unique pro device).
|
||||||
|
|
||||||
|
Klare Acks für Queue-Weiterdrehen.
|
||||||
|
|
||||||
|
Web UI benötigt Tagesaggregation und Tagesdetail.
|
||||||
|
|
||||||
|
Vorschläge/Stops müssen abfragbar sein.
|
||||||
|
|
||||||
|
5.2 Endpoints (Vorschlag)
|
||||||
|
Ingest:
|
||||||
|
|
||||||
|
POST /v1/trackpoints (single)
|
||||||
|
|
||||||
|
POST /v1/trackpoints:batch (batch)
|
||||||
|
|
||||||
|
GET /healthz
|
||||||
|
|
||||||
|
GET /readyz
|
||||||
|
|
||||||
|
Web/Query:
|
||||||
|
|
||||||
|
GET /v1/days?from=YYYY-MM-DD&to=YYYY-MM-DD
|
||||||
|
|
||||||
|
GET /v1/trackpoints?date=YYYY-MM-DD
|
||||||
|
|
||||||
|
GET /v1/stops?date=YYYY-MM-DD
|
||||||
|
|
||||||
|
GET /v1/suggestions?date=YYYY-MM-DD
|
||||||
|
|
||||||
|
Web UI Routes (serverseitig gerendert):
|
||||||
|
|
||||||
|
GET /login
|
||||||
|
|
||||||
|
POST /login
|
||||||
|
|
||||||
|
POST /logout
|
||||||
|
|
||||||
|
GET /days
|
||||||
|
|
||||||
|
GET /days/{yyyy-mm-dd}
|
||||||
|
|
||||||
|
5.3 Datenmodell (Draft)
|
||||||
|
Trackpoint:
|
||||||
|
|
||||||
|
event_id (string, client-generated; UUID empfohlen)
|
||||||
|
|
||||||
|
device_id (string)
|
||||||
|
|
||||||
|
trip_id (string)
|
||||||
|
|
||||||
|
timestamp (RFC3339 oder epochMillis, offen)
|
||||||
|
|
||||||
|
lat, lon (float)
|
||||||
|
|
||||||
|
source: "gps" | "manual" (optional; Default: gps)
|
||||||
|
|
||||||
|
note (string, optional; nur bei manuellen Punkten)
|
||||||
|
|
||||||
|
optional: accuracy_m, speed_mps, bearing_deg, altitude_m
|
||||||
|
|
||||||
|
Stop:
|
||||||
|
|
||||||
|
stop_id
|
||||||
|
|
||||||
|
device_id, trip_id
|
||||||
|
|
||||||
|
start_ts, end_ts
|
||||||
|
|
||||||
|
center_lat, center_lon
|
||||||
|
|
||||||
|
duration_s
|
||||||
|
|
||||||
|
place_label (optional, aus reverse-geocode)
|
||||||
|
|
||||||
|
place_details (optional JSON)
|
||||||
|
|
||||||
|
Suggestion:
|
||||||
|
|
||||||
|
suggestion_id
|
||||||
|
|
||||||
|
stop_id
|
||||||
|
|
||||||
|
type (z. B. "highlight", "name_place", "add_note")
|
||||||
|
|
||||||
|
title/text
|
||||||
|
|
||||||
|
created_at
|
||||||
|
|
||||||
|
dismissed_at (optional)
|
||||||
|
|
||||||
|
Batch response:
|
||||||
|
|
||||||
|
server_time
|
||||||
|
|
||||||
|
accepted_ids (string[])
|
||||||
|
|
||||||
|
rejected (array of {event_id, code, message})
|
||||||
|
|
||||||
|
Day summary:
|
||||||
|
|
||||||
|
date (YYYY-MM-DD)
|
||||||
|
|
||||||
|
count (int)
|
||||||
|
|
||||||
|
first_ts, last_ts (optional)
|
||||||
|
|
||||||
|
5.4 Idempotenz-Regel
|
||||||
|
Unique Key: (device_id, event_id)
|
||||||
|
|
||||||
|
Duplicate: Server antwortet erfolgreich (z. B. 200) und behandelt es als „already processed".
|
||||||
|
|
||||||
|
5.5 Auth (Draft)
|
||||||
|
Website-Zugriff MUSS Login erfordern (Session Cookie).
|
||||||
|
|
||||||
|
API-Calls für Web UI müssen Auth prüfen (Session).
|
||||||
|
|
||||||
|
Android Upload Auth ist separat zu entscheiden (API-Key vs Bearer/JWT).
|
||||||
|
|
||||||
|
6. Auth: Session Cookie vs JWT
|
||||||
|
Session Cookie (session-basiert, stateful)
|
||||||
|
Vorteile:
|
||||||
|
|
||||||
|
Browser sendet Cookies automatisch mit; serverseitige Sessions sind gut kontrollierbar.
|
||||||
|
|
||||||
|
Sofortiger Widerruf möglich (Logout = Session löschen).
|
||||||
|
|
||||||
|
Nachteile:
|
||||||
|
|
||||||
|
Server muss Session-State speichern und über Instanzen verfügbar machen (z. B. Postgres/Redis).
|
||||||
|
|
||||||
|
JWT (token-basiert, stateless)
|
||||||
|
Vorteile:
|
||||||
|
|
||||||
|
Stateless pro Request; skaliert oft leichter für reine APIs/mehrere Services.
|
||||||
|
|
||||||
|
Gut für API-first/SSO/Verteilung.
|
||||||
|
|
||||||
|
Nachteile:
|
||||||
|
|
||||||
|
Widerruf/Revocation, Rotation und Lebensdauer-Management erhöhen Komplexität.
|
||||||
|
|
||||||
|
Empfehlung für RALPH
|
||||||
|
Website: Session Cookie.
|
||||||
|
|
||||||
|
JWT optional später, falls API-first/SSO nötig wird.
|
||||||
|
|
||||||
|
7. Testing (Requirements)
|
||||||
|
7.1 Use-Case Coverage
|
||||||
|
Alle wichtigen Use Cases (R1–R7, plus Duplicate/Retry und Validierungsfehler) MÜSSEN automatisiert getestet werden.
|
||||||
|
|
||||||
|
7.2 Android Unit Tests
|
||||||
|
REQ-TEST-A01: Lokale JVM-Unit-Tests für Domain/Queue/Serializer/Retry-Entscheidungen.
|
||||||
|
|
||||||
|
REQ-TEST-A02: Unit Tests für Validierungslogik manueller Punkte.
|
||||||
|
|
||||||
|
7.3 Android Integration/Instrumentation (WorkManager)
|
||||||
|
REQ-TEST-A03: Integrationstests für Worker/Constraints/Delays mit WorkManager-Testunterstützung.
|
||||||
|
|
||||||
|
7.4 Go Unit & HTTP Handler Tests
|
||||||
|
REQ-TEST-S01: Go Unit Tests via testing (go test ./...).
|
||||||
|
|
||||||
|
REQ-TEST-S02: REST Handler Tests (inkl. /v1/days, /v1/trackpoints?date=…, /v1/stops, /v1/suggestions) mit net/http/httptest.
|
||||||
|
|
||||||
|
7.5 System-/Integrationstests (App ↔ Server)
|
||||||
|
REQ-TEST-I01: Integrationstest: Pending Queue → Batch Upload → Ack → Mark sent.
|
||||||
|
|
||||||
|
REQ-TEST-I02: Offline→Online-Wechsel: zuerst Fehler, später Erfolg.
|
||||||
|
|
||||||
|
REQ-TEST-I03: Duplicate event_id: gleicher Batch zweimal → keine Duplikate.
|
||||||
|
|
||||||
|
REQ-TEST-I04: Manual point → Queue → Upload → Ack (End-to-End).
|
||||||
|
|
||||||
|
7.6 Web UI Tests
|
||||||
|
REQ-TEST-W01: Login-Flow muss getestet werden (gültig/ungültig, Session created, Zugriffsschutz).
|
||||||
|
|
||||||
|
REQ-TEST-W02: Tagesansicht muss getestet werden (korrekte Tage, korrekte Punkte/Stops/Vorschläge pro Tag, AuthZ).
|
||||||
|
|
||||||
|
7.7 Geocoding/RateLimit Tests
|
||||||
|
REQ-TEST-GEO-01: Tests müssen sicherstellen, dass Reverse-Geocoding gecached wird und keine Bulk/periodischen Calls entstehen.
|
||||||
|
|
||||||
|
REQ-TEST-GEO-02: Provider-Wechsel muss testbar sein (Config umstellen → anderer Endpoint, ohne App-Update).
|
||||||
|
|
||||||
|
8. Backlog (Tasks)
|
||||||
|
8.1 Reihenfolge (wichtig)
|
||||||
|
T000: Architektur in diesem Dokument finalisieren (Kontext, Bausteine, Laufzeit, Verteilung) und abnehmen, bevor Implementierung startet.
|
||||||
|
|
||||||
|
T000e: Architektur-Review/Abnahme-Checkpoint (Issue/PR), erst danach Implementierungs-Tasks starten.
|
||||||
|
|
||||||
|
8.2 Android (Client)
|
||||||
|
T001: Compose-Projekt anlegen (Kotlin, minSdk festlegen).
|
||||||
|
|
||||||
|
T001a: Kotlin-Standards festlegen (Coroutines-Usage, KTX/Extensions, Code Style).
|
||||||
|
|
||||||
|
T004: Permissions im Manifest + Runtime-Flow (COARSE/FINE + Background).
|
||||||
|
|
||||||
|
T006: Background-Tracking Architektur (Provider/Manager/Service falls nötig).
|
||||||
|
|
||||||
|
T033: Android Persistenz: Room-Setup (Entities/DAOs/Migrations) auf SQLite-Basis.
|
||||||
|
|
||||||
|
T008: Lokale DB + Upload-Queue (pending/sent/failed, retryCount).
|
||||||
|
|
||||||
|
T012: Upload-Worker (Constraints Network connected, Retry/Backoff).
|
||||||
|
|
||||||
|
T018: Export via SAF ACTION_CREATE_DOCUMENT + Schreiben in URI.
|
||||||
|
|
||||||
|
T060: App-Kartenansicht (Track/Stops lokal anzeigen), SDK nach DEC-MAP-01 (optional).
|
||||||
|
|
||||||
|
T080: UI Screen/Flow „Manuellen Punkt hinzufügen" (lat/lon, timestamp, note optional).
|
||||||
|
|
||||||
|
T081: Eingabevalidierung in Compose (as the user types) + Fehlermeldungen.
|
||||||
|
|
||||||
|
T082: Persistenz: Manual-Point als Trackpoint in Room speichern, inkl. source=manual.
|
||||||
|
|
||||||
|
T083: Upload: Manual-Points in Batch/Single Upload einbeziehen (idempotent über event_id).
|
||||||
|
|
||||||
|
T084: Tests: Unit Tests für Validierung + Mapping; Integrationstest: Manual point → Queue → Upload → Ack.
|
||||||
|
|
||||||
|
8.3 Server (Go, HTTP + Web UI)
|
||||||
|
API / Persistenz:
|
||||||
|
|
||||||
|
T024: REST API finalisieren (Single + Batch, Fehlerformat, Limits).
|
||||||
|
|
||||||
|
T027: Persistenz: Postgres Schema + Migrationen + Indizes + Retention.
|
||||||
|
|
||||||
|
T028: Server-Idempotenz (unique event_id pro device).
|
||||||
|
|
||||||
|
T029: Observability (Logs/Metrics), Health/Ready.
|
||||||
|
|
||||||
|
T030: E2E lokal: docker compose (API + Postgres) + Minimal-Client zum Senden.
|
||||||
|
|
||||||
|
Auth / Website:
|
||||||
|
|
||||||
|
T050: User-/Auth-Konzept festlegen (User, Devices, Ownership, Session-Lifetime).
|
||||||
|
|
||||||
|
T051: Passwort-Hashing implementieren (Argon2id bevorzugt).
|
||||||
|
|
||||||
|
T052: Session-Management implementieren (Session-Store in Postgres, Cookie setzen, Logout/Invalidate).
|
||||||
|
|
||||||
|
T053: Web UI: Login-Seite (GET/POST), Zugriffsschutz (Middleware).
|
||||||
|
|
||||||
|
T054: Web UI: Tagesübersicht (GET /days) + Tagesdetail (GET /days/{date}) inkl. Karte.
|
||||||
|
|
||||||
|
T055: API: GET /v1/days und GET /v1/trackpoints?date=YYYY-MM-DD (Auth required).
|
||||||
|
|
||||||
|
T061: Stop Detection implementieren (Parameter: minDuration, radiusMeters; konfigurierbar).
|
||||||
|
|
||||||
|
T062: Suggestions implementieren (aus Stops ableiten; Storage + API).
|
||||||
|
|
||||||
|
T063: Geocoding Adapter + Cache + RateLimit + Provider-Wechsel implementieren.
|
||||||
|
|
||||||
|
T064: Web UI: Stops/Vorschläge pro Tag anzeigen (Liste + Karte).
|
||||||
|
|
||||||
|
OpenAPI:
|
||||||
|
|
||||||
|
T070: OpenAPI-Dokument anlegen (openapi.yaml als OAS 3.1) und im Repo versionieren.
|
||||||
|
|
||||||
|
T071: OpenAPI vollständig halten: alle Endpoints/Schemas/Responses/Errors/Security abbilden, inkl. CookieAuth für Web-API.
|
||||||
|
|
||||||
|
T072: OpenAPI in CI validieren (Spec-Validation) und Änderungen nur via PR.
|
||||||
|
|
||||||
|
8.4 Tests
|
||||||
|
T040: Use-Case-Matrix (Use Case → Testart).
|
||||||
|
|
||||||
|
T041: Android Unit Tests (Queue/Serializer/Retry/Validation).
|
||||||
|
|
||||||
|
T042: Android WorkManager Integrationstests.
|
||||||
|
|
||||||
|
T043: Go Unit Tests (testing).
|
||||||
|
|
||||||
|
T044: Go HTTP Handler Tests (httptest).
|
||||||
|
|
||||||
|
T045: Integrationstests (E2E ingest + Ack + Dedupe).
|
||||||
|
|
||||||
|
T057: Web Login Tests (happy path + invalid creds + session required).
|
||||||
|
|
||||||
|
T058: Web Tagesansicht Tests (korrekte Tage, korrekte Punkte pro Tag, AuthZ).
|
||||||
|
|
||||||
|
T065: Stop/Suggestion Tests (synthetische Trackpoints → erwartete Stops/Vorschläge).
|
||||||
|
|
||||||
|
T066: Geocoding Cache/RateLimit Tests (keine Bulk Calls; Provider switch).
|
||||||
|
|
||||||
|
9. Offene Entscheidungen (Checklist)
|
||||||
|
timestamp Format: epochMillis vs RFC3339.
|
||||||
|
|
||||||
|
Android Upload Auth: X-API-Key vs Bearer/JWT (Web bleibt Session Cookie).
|
||||||
|
|
||||||
|
Payload: JSON vs Protobuf über HTTP.
|
||||||
|
|
||||||
|
Batch limits (max items, max bytes).
|
||||||
|
|
||||||
|
Server Retention-Policy (wie lange Trackpoints gespeichert werden).
|
||||||
|
|
||||||
|
Stop-Detection Parameter (min Dauer, Radius).
|
||||||
|
|
||||||
|
Karten-Tile-Provider (konkreter Anbieter/Hosting; muss austauschbar bleiben).
|
||||||
|
|
||||||
|
Geocoding Provider (konkret: Nominatim public vs self-hosted Nominatim vs alternative freie Provider); Nutzung muss Policy-konform sein.
|
||||||
16
app/.gitignore
vendored
Normal file
16
app/.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
*.iml
|
||||||
|
.gradle/
|
||||||
|
.idea/
|
||||||
|
/local.properties
|
||||||
|
/.idea/workspace.xml
|
||||||
|
/.idea/libraries
|
||||||
|
.DS_Store
|
||||||
|
/build/
|
||||||
|
/captures/
|
||||||
|
.externalNativeBuild/
|
||||||
|
.cxx/
|
||||||
|
*.keystore
|
||||||
|
!debug.keystore
|
||||||
|
local.properties
|
||||||
|
app/build/
|
||||||
|
app/.cxx/
|
||||||
65
app/CLAUDE.md
Normal file
65
app/CLAUDE.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# CLAUDE.md — RALPH Android Frontend
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
Language: Kotlin
|
||||||
|
UI: Jetpack Compose
|
||||||
|
DB: Room (SQLite)
|
||||||
|
Worker: WorkManager
|
||||||
|
Maps: MapLibre + OpenStreetMap
|
||||||
|
HTTP: Retrofit oder Ktor Client (TBD)
|
||||||
|
Auth: TBD (API-Key vs JWT für Upload)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Kern-Features (Android)
|
||||||
|
|
||||||
|
1. Background GPS-Tracking (COARSE/FINE + Background permission)
|
||||||
|
2. Offline Queue (Room) + WorkManager Upload (Idempotenz via event_id)
|
||||||
|
3. Manuelle Punkte hinzufügen (lat/lon/timestamp/notiz + Validierung)
|
||||||
|
4. Kartenansicht (MapLibre, Tile-Quelle konfigurierbar)
|
||||||
|
5. Datei-Export (SAF ACTION_CREATE_DOCUMENT)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architektur-Prinzipien
|
||||||
|
|
||||||
|
- Offline-First: Jeder Punkt wird zuerst lokal in Room gespeichert
|
||||||
|
- Upload via WorkManager mit Constraint: Network connected + Retry/Backoff
|
||||||
|
- Idempotenz: Jeder Punkt erhält eine client-seitige event_id (UUID)
|
||||||
|
- source-Feld: "gps" | "manual" unterscheidet Punkt-Typ
|
||||||
|
- Validierung manueller Punkte: as the user types im ViewModel
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Offene Entscheidungen (TBD)
|
||||||
|
|
||||||
|
- HTTP Client: Retrofit vs Ktor
|
||||||
|
- timestamp Format: epochMillis vs RFC3339
|
||||||
|
- Android Upload Auth: X-API-Key vs JWT
|
||||||
|
- minSdk festlegen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nächste Tasks (Reihenfolge)
|
||||||
|
|
||||||
|
- [ ] T001 Compose-Projekt anlegen (Kotlin, minSdk festlegen)
|
||||||
|
- [ ] T001a Kotlin-Standards festlegen (Coroutines, KTX, Code Style)
|
||||||
|
- [ ] T004 Permissions Manifest + Runtime-Flow (COARSE/FINE + Background)
|
||||||
|
- [ ] T006 Background-Tracking Architektur (Foreground Service)
|
||||||
|
- [ ] T033 Room-Setup (Entities/DAOs/Migrations)
|
||||||
|
- [ ] T008 Upload-Queue (pending/sent/failed, retryCount)
|
||||||
|
- [ ] T012 Upload-Worker (WorkManager, Retry/Backoff)
|
||||||
|
- [ ] T080 UI: Manuellen Punkt hinzufügen
|
||||||
|
- [ ] T081 Eingabevalidierung Compose (as the user types)
|
||||||
|
- [ ] T018 Export via SAF
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Antwort-Stil
|
||||||
|
|
||||||
|
- Kotlin-Code immer mit Coroutines/Flow
|
||||||
|
- Compose-Code: State Hoisting, ViewModel pattern
|
||||||
|
- Kein direkter DB/Network-Zugriff in Composables
|
||||||
|
- Tests: JVM Unit Tests für Domain/Queue; Instrumentation für Room/Worker
|
||||||
103
app/app/build.gradle.kts
Normal file
103
app/app/build.gradle.kts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application)
|
||||||
|
alias(libs.plugins.kotlin.android)
|
||||||
|
alias(libs.plugins.kotlin.compose)
|
||||||
|
alias(libs.plugins.ksp)
|
||||||
|
alias(libs.plugins.hilt)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "de.jacek.reisejournal"
|
||||||
|
compileSdk = 35
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "de.jacek.reisejournal"
|
||||||
|
minSdk = 26
|
||||||
|
targetSdk = 35
|
||||||
|
versionCode = 1
|
||||||
|
versionName = "1.0"
|
||||||
|
|
||||||
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isMinifyEnabled = true
|
||||||
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
compose = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Compose
|
||||||
|
val composeBom = platform(libs.compose.bom)
|
||||||
|
implementation(composeBom)
|
||||||
|
androidTestImplementation(composeBom)
|
||||||
|
|
||||||
|
implementation(libs.compose.ui)
|
||||||
|
implementation(libs.compose.ui.graphics)
|
||||||
|
implementation(libs.compose.ui.tooling.preview)
|
||||||
|
implementation(libs.compose.material3)
|
||||||
|
implementation(libs.compose.activity)
|
||||||
|
implementation(libs.navigation.compose)
|
||||||
|
implementation(libs.lifecycle.viewmodel.compose)
|
||||||
|
debugImplementation(libs.compose.ui.tooling)
|
||||||
|
debugImplementation(libs.compose.ui.test.manifest)
|
||||||
|
|
||||||
|
// Hilt
|
||||||
|
implementation(libs.hilt.android)
|
||||||
|
ksp(libs.hilt.compiler)
|
||||||
|
implementation(libs.hilt.navigation.compose)
|
||||||
|
|
||||||
|
// Room
|
||||||
|
implementation(libs.room.runtime)
|
||||||
|
implementation(libs.room.ktx)
|
||||||
|
ksp(libs.room.compiler)
|
||||||
|
|
||||||
|
// WorkManager
|
||||||
|
implementation(libs.workmanager)
|
||||||
|
implementation(libs.hilt.work)
|
||||||
|
ksp(libs.hilt.work.compiler)
|
||||||
|
|
||||||
|
// Retrofit + Moshi
|
||||||
|
implementation(libs.retrofit)
|
||||||
|
implementation(libs.retrofit.moshi)
|
||||||
|
implementation(libs.moshi.kotlin)
|
||||||
|
ksp(libs.moshi.codegen)
|
||||||
|
implementation(libs.okhttp.logging)
|
||||||
|
|
||||||
|
// Location
|
||||||
|
implementation(libs.play.services.location)
|
||||||
|
|
||||||
|
// DataStore
|
||||||
|
implementation(libs.datastore.preferences)
|
||||||
|
|
||||||
|
// MapLibre
|
||||||
|
implementation(libs.maplibre)
|
||||||
|
|
||||||
|
// Coroutines
|
||||||
|
implementation(libs.coroutines.core)
|
||||||
|
implementation(libs.coroutines.android)
|
||||||
|
|
||||||
|
// Tests
|
||||||
|
testImplementation(libs.junit)
|
||||||
|
androidTestImplementation(libs.junit.ext)
|
||||||
|
androidTestImplementation(libs.espresso.core)
|
||||||
|
androidTestImplementation(libs.compose.ui.test.junit4)
|
||||||
|
}
|
||||||
25
app/app/proguard-rules.pro
vendored
Normal file
25
app/app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
|
||||||
|
# Moshi
|
||||||
|
-keepclassmembers class * {
|
||||||
|
@com.squareup.moshi.FromJson *;
|
||||||
|
@com.squareup.moshi.ToJson *;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Retrofit
|
||||||
|
-keepattributes Signature, InnerClasses, EnclosingMethod
|
||||||
|
-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations
|
||||||
|
-keepclassmembers,allowshrinking,allowobfuscation interface * {
|
||||||
|
@retrofit2.http.* <methods>;
|
||||||
|
}
|
||||||
|
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
|
||||||
|
-dontwarn javax.annotation.**
|
||||||
|
-dontwarn kotlin.Unit
|
||||||
|
-dontwarn retrofit2.KotlinExtensions
|
||||||
|
-dontwarn retrofit2.KotlinExtensions$*
|
||||||
|
|
||||||
|
# OkHttp
|
||||||
|
-dontwarn okhttp3.**
|
||||||
|
-dontwarn okio.**
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package de.jacek.reisejournal
|
||||||
|
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class ExampleInstrumentedTest {
|
||||||
|
@Test
|
||||||
|
fun useAppContext() {
|
||||||
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
assertEquals("de.jacek.reisejournal", appContext.packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
42
app/app/src/main/AndroidManifest.xml
Normal file
42
app/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<!-- Location permissions -->
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
<!-- Background location: requested at runtime separately (T004) -->
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||||
|
|
||||||
|
<!-- Foreground service (GPS tracking) -->
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||||
|
|
||||||
|
<!-- Network -->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|
||||||
|
<!-- WorkManager auto-start after reboot -->
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name=".RalphApp"
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.Reisejournal">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:theme="@style/Theme.Reisejournal">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package de.jacek.reisejournal
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import de.jacek.reisejournal.ui.navigation.NavGraph
|
||||||
|
import de.jacek.reisejournal.ui.theme.RalphTheme
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class MainActivity : ComponentActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enableEdgeToEdge()
|
||||||
|
setContent {
|
||||||
|
RalphTheme {
|
||||||
|
NavGraph()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
app/app/src/main/kotlin/de/jacek/reisejournal/RalphApp.kt
Normal file
19
app/app/src/main/kotlin/de/jacek/reisejournal/RalphApp.kt
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package de.jacek.reisejournal
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.hilt.work.HiltWorkerFactory
|
||||||
|
import androidx.work.Configuration
|
||||||
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltAndroidApp
|
||||||
|
class RalphApp : Application(), Configuration.Provider {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var workerFactory: HiltWorkerFactory
|
||||||
|
|
||||||
|
override val workManagerConfiguration: Configuration
|
||||||
|
get() = Configuration.Builder()
|
||||||
|
.setWorkerFactory(workerFactory)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package de.jacek.reisejournal.domain
|
||||||
|
|
||||||
|
data class Trackpoint(
|
||||||
|
val eventId: String, // UUID, client-generated
|
||||||
|
val deviceId: String,
|
||||||
|
val tripId: String?,
|
||||||
|
val timestamp: String, // RFC3339, e.g. "2024-01-15T10:30:00Z"
|
||||||
|
val lat: Double,
|
||||||
|
val lon: Double,
|
||||||
|
val source: String, // "gps" | "manual"
|
||||||
|
val note: String?,
|
||||||
|
val accuracy: Float?,
|
||||||
|
val speed: Float?,
|
||||||
|
val bearing: Float?,
|
||||||
|
val altitude: Double?,
|
||||||
|
)
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package de.jacek.reisejournal.ui.home
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import de.jacek.reisejournal.R
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun HomeScreen(
|
||||||
|
viewModel: HomeViewModel = hiltViewModel()
|
||||||
|
) {
|
||||||
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text(stringResource(R.string.app_name)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.app_name),
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Punkte: ${uiState.trackpointCount}",
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package de.jacek.reisejournal.ui.home
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
data class HomeUiState(
|
||||||
|
val isTracking: Boolean = false,
|
||||||
|
val trackpointCount: Int = 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class HomeViewModel @Inject constructor() : ViewModel() {
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow(HomeUiState())
|
||||||
|
val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package de.jacek.reisejournal.ui.navigation
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import de.jacek.reisejournal.ui.home.HomeScreen
|
||||||
|
|
||||||
|
const val HOME_ROUTE = "home"
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NavGraph() {
|
||||||
|
val navController = rememberNavController()
|
||||||
|
|
||||||
|
NavHost(
|
||||||
|
navController = navController,
|
||||||
|
startDestination = HOME_ROUTE,
|
||||||
|
) {
|
||||||
|
composable(HOME_ROUTE) {
|
||||||
|
HomeScreen()
|
||||||
|
}
|
||||||
|
// Future routes: map, add_point, settings, export
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package de.jacek.reisejournal.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
val Primary = Color(0xFF2E7D32) // Forest green
|
||||||
|
val OnPrimary = Color(0xFFFFFFFF)
|
||||||
|
val PrimaryContainer = Color(0xFFA5D6A7)
|
||||||
|
val OnPrimaryContainer = Color(0xFF003909)
|
||||||
|
|
||||||
|
val Secondary = Color(0xFF558B2F)
|
||||||
|
val OnSecondary = Color(0xFFFFFFFF)
|
||||||
|
val SecondaryContainer = Color(0xFFCCFF90)
|
||||||
|
val OnSecondaryContainer = Color(0xFF1B5E20)
|
||||||
|
|
||||||
|
val Background = Color(0xFFF8FFF8)
|
||||||
|
val OnBackground = Color(0xFF1A1C1A)
|
||||||
|
val Surface = Color(0xFFF8FFF8)
|
||||||
|
val OnSurface = Color(0xFF1A1C1A)
|
||||||
|
val SurfaceVariant = Color(0xFFDCE5DC)
|
||||||
|
val OnSurfaceVariant = Color(0xFF404940)
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package de.jacek.reisejournal.ui.theme
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
|
import androidx.compose.material3.lightColorScheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|
||||||
|
private val LightColorScheme = lightColorScheme(
|
||||||
|
primary = Primary,
|
||||||
|
onPrimary = OnPrimary,
|
||||||
|
primaryContainer = PrimaryContainer,
|
||||||
|
onPrimaryContainer = OnPrimaryContainer,
|
||||||
|
secondary = Secondary,
|
||||||
|
onSecondary = OnSecondary,
|
||||||
|
secondaryContainer = SecondaryContainer,
|
||||||
|
onSecondaryContainer = OnSecondaryContainer,
|
||||||
|
background = Background,
|
||||||
|
onBackground = OnBackground,
|
||||||
|
surface = Surface,
|
||||||
|
onSurface = OnSurface,
|
||||||
|
surfaceVariant = SurfaceVariant,
|
||||||
|
onSurfaceVariant = OnSurfaceVariant,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val DarkColorScheme = darkColorScheme(
|
||||||
|
primary = PrimaryContainer,
|
||||||
|
onPrimary = OnPrimaryContainer,
|
||||||
|
primaryContainer = Primary,
|
||||||
|
onPrimaryContainer = OnPrimary,
|
||||||
|
secondary = SecondaryContainer,
|
||||||
|
onSecondary = OnSecondaryContainer,
|
||||||
|
secondaryContainer = Secondary,
|
||||||
|
onSecondaryContainer = OnSecondary,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RalphTheme(
|
||||||
|
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
|
dynamicColor: Boolean = true,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val colorScheme = when {
|
||||||
|
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||||
|
val context = LocalContext.current
|
||||||
|
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||||
|
}
|
||||||
|
darkTheme -> DarkColorScheme
|
||||||
|
else -> LightColorScheme
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = colorScheme,
|
||||||
|
typography = Typography,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package de.jacek.reisejournal.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.material3.Typography
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
val Typography = Typography(
|
||||||
|
bodyLarge = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
lineHeight = 24.sp,
|
||||||
|
letterSpacing = 0.5.sp
|
||||||
|
),
|
||||||
|
titleLarge = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 22.sp,
|
||||||
|
lineHeight = 28.sp,
|
||||||
|
letterSpacing = 0.sp
|
||||||
|
),
|
||||||
|
labelSmall = TextStyle(
|
||||||
|
fontFamily = FontFamily.Default,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
lineHeight = 16.sp,
|
||||||
|
letterSpacing = 0.5.sp
|
||||||
|
)
|
||||||
|
)
|
||||||
4
app/app/src/main/res/values/strings.xml
Normal file
4
app/app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Reisejournal</string>
|
||||||
|
</resources>
|
||||||
5
app/app/src/main/res/values/themes.xml
Normal file
5
app/app/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="Theme.Reisejournal" parent="android:Theme.Material.Light.NoActionBar" />
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package de.jacek.reisejournal
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
class ExampleUnitTest {
|
||||||
|
@Test
|
||||||
|
fun addition_isCorrect() {
|
||||||
|
assertEquals(4, 2 + 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
8
app/build.gradle.kts
Normal file
8
app/build.gradle.kts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application) apply false
|
||||||
|
alias(libs.plugins.kotlin.android) apply false
|
||||||
|
alias(libs.plugins.kotlin.compose) apply false
|
||||||
|
alias(libs.plugins.ksp) apply false
|
||||||
|
alias(libs.plugins.hilt) apply false
|
||||||
|
}
|
||||||
6
app/gradle.properties
Normal file
6
app/gradle.properties
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
org.gradle.configuration-cache=true
|
||||||
|
org.gradle.parallel=true
|
||||||
|
android.useAndroidX=true
|
||||||
|
kotlin.code.style=official
|
||||||
|
android.nonTransitiveRClass=true
|
||||||
85
app/gradle/libs.versions.toml
Normal file
85
app/gradle/libs.versions.toml
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
[versions]
|
||||||
|
kotlin = "2.0.21"
|
||||||
|
ksp = "2.0.21-1.0.27"
|
||||||
|
agp = "8.7.3"
|
||||||
|
|
||||||
|
composeBom = "2024.12.01"
|
||||||
|
navigationCompose = "2.8.5"
|
||||||
|
hilt = "2.52"
|
||||||
|
hiltNavigationCompose = "1.2.0"
|
||||||
|
room = "2.6.1"
|
||||||
|
workManager = "2.10.0"
|
||||||
|
retrofit = "2.11.0"
|
||||||
|
moshi = "1.15.2"
|
||||||
|
okhttp = "4.12.0"
|
||||||
|
playServicesLocation = "21.3.0"
|
||||||
|
datastorePreferences = "1.1.2"
|
||||||
|
lifecycleViewmodelCompose = "2.8.7"
|
||||||
|
maplibre = "11.7.1"
|
||||||
|
coroutines = "1.9.0"
|
||||||
|
|
||||||
|
junit = "4.13.2"
|
||||||
|
junitExt = "1.2.1"
|
||||||
|
espresso = "3.6.1"
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
# Compose
|
||||||
|
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||||
|
compose-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||||
|
compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
||||||
|
compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||||
|
compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
||||||
|
compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||||
|
compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||||
|
compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||||
|
compose-activity = { group = "androidx.activity", name = "activity-compose", version = "1.9.3" }
|
||||||
|
navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
|
||||||
|
lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" }
|
||||||
|
|
||||||
|
# Hilt
|
||||||
|
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
|
||||||
|
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
|
||||||
|
hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
|
||||||
|
|
||||||
|
# Room
|
||||||
|
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
|
||||||
|
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
|
||||||
|
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
|
||||||
|
|
||||||
|
# WorkManager
|
||||||
|
workmanager = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "workManager" }
|
||||||
|
hilt-work = { group = "androidx.hilt", name = "hilt-work", version = "1.2.0" }
|
||||||
|
hilt-work-compiler = { group = "androidx.hilt", name = "hilt-compiler", version = "1.2.0" }
|
||||||
|
|
||||||
|
# Retrofit + Moshi
|
||||||
|
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
|
||||||
|
retrofit-moshi = { group = "com.squareup.retrofit2", name = "converter-moshi", version.ref = "retrofit" }
|
||||||
|
moshi-kotlin = { group = "com.squareup.moshi", name = "moshi-kotlin", version.ref = "moshi" }
|
||||||
|
moshi-codegen = { group = "com.squareup.moshi", name = "moshi-kotlin-codegen", version.ref = "moshi" }
|
||||||
|
okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
|
||||||
|
|
||||||
|
# Location
|
||||||
|
play-services-location = { group = "com.google.android.gms", name = "play-services-location", version.ref = "playServicesLocation" }
|
||||||
|
|
||||||
|
# DataStore
|
||||||
|
datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastorePreferences" }
|
||||||
|
|
||||||
|
# MapLibre
|
||||||
|
maplibre = { group = "org.maplibre.gl", name = "android-sdk", version.ref = "maplibre" }
|
||||||
|
|
||||||
|
# Coroutines
|
||||||
|
coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" }
|
||||||
|
coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" }
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||||
|
junit-ext = { group = "androidx.test.ext", name = "junit", version.ref = "junitExt" }
|
||||||
|
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso" }
|
||||||
|
|
||||||
|
[plugins]
|
||||||
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
android-library = { id = "com.android.library", version.ref = "agp" }
|
||||||
|
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
|
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||||
|
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||||
|
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
|
||||||
BIN
app/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
app/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
app/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
app/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
187
app/gradlew
vendored
Executable file
187
app/gradlew
vendored
Executable file
@@ -0,0 +1,187 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# Copyright © 2015-2021 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by "Gradle init".
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other POSIX-compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for distributing:
|
||||||
|
#
|
||||||
|
# (2) This script is not designed to run as an executable binary. It is
|
||||||
|
# designed to be run by the shell interpreter.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #( absolute
|
||||||
|
*) app_path=$APP_HOME$link ;; #( relative
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is reliable if the script itself doesn't have a relative symlink
|
||||||
|
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command;
|
||||||
|
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||||
|
# shell script including quotes and variable substitutions, so put them in
|
||||||
|
# double quotes to make sure that they get re-expanded; and
|
||||||
|
# * put everything else in single quotes, so that it's not re-expanded.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution,
|
||||||
|
# so instead we have to hack around them using a temporary file.
|
||||||
|
#
|
||||||
|
# We also fail if there are not enough args to parse the input.
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
91
app/gradlew.bat
vendored
Normal file
91
app/gradlew.bat
vendored
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%"=="" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /C_ return code!
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
29
app/settings.gradle.kts
Normal file
29
app/settings.gradle.kts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
google {
|
||||||
|
content {
|
||||||
|
includeGroupByRegex("com\\.android.*")
|
||||||
|
includeGroupByRegex("com\\.google.*")
|
||||||
|
includeGroupByRegex("androidx.*")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
versionCatalogs {
|
||||||
|
create("libs") {
|
||||||
|
from(files("gradle/libs.versions.toml"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = "reisejournal"
|
||||||
|
include(":app")
|
||||||
1
backend
Submodule
1
backend
Submodule
Submodule backend added at 55d2ffc203
Reference in New Issue
Block a user