BigBrainParking — big-brain driver

# BigBrainParking An unofficial, de-Googled client for the ParkSmarter (IPS Group) parking system, built to run on **GrapheneOS** with **UnifiedPush** notifications and distributed via **Obtainium** — no Google Play Services, no Firebase, no app store. ## Install it (users) BigBrainParking installs and updates through **Obtainium** — no Google Play, no account. Works on GrapheneOS. On your phone (with Obtainium installed), tap: **➕ [Add BigBrainParking to Obtainium](obtainium://add/https://git.mowden.top/hank/BigBrainParking)** or follow **[INSTALL.md](INSTALL.md)** for the manual steps. ## Monorepo layout ``` bigbrainparking/ ├── parksmarter-client/ # Zero-dep TypeScript API client (reverse-engineered, live-verified) ├── app/ # Expo / React Native app (BigBrainParking) └── .gitea/workflows/ # CI: build signed APK -> publish release for Obtainium ``` - **`parksmarter-client`** — the API layer. All ~40 endpoints, the custom-header auth model, and response models (most verified against production). Runs anywhere; the app imports it directly. See its own README for the API details. - **`app`** — the phone app. React Native (Expo prebuild), MapLibre maps, VisionCamera QR scanning, expo-secure-store token storage, local session-expiry reminders, and UnifiedPush wiring. ## Features | Feature | Status | Notes | | --- | --- | --- | | Phone + password login | ✅ wired | tokens in OS keystore | | Map of nearby meters (clickable, zoom, live GPS) | ✅ wired | MapLibre + OpenFreeMap tiles (no key) | | City parking map overlay (2h/3h/4h/no-limit/lots) | ✅ wired | the city's printed map, georeferenced — **never touches the IPS API** | | "Park here" pin + time tracking on any city area | ✅ wired | GPS or hand-placed pin, auto-detects the area, local countdown | | Last-lot + My-location search | ✅ wired | opens on your last session's lot; GPS sent only via explicit "My location" | | QR kiosk scan → meter lookup | ✅ wired | on-device VisionCamera | | Save / share kiosks | ✅ wired | local (no server favorites API exists) | | Active / past sessions | ✅ wired | list views + tap for full receipt | | Start a paid session | ✅ works | confirmed live end-to-end (real $0.10 DL-zone charge); declines surfaced | | Ongoing parking countdown | ✅ wired | foreground service; ticks down, **End** / **Extend** buttons, survives reboot | | Session-expiry reminders | ✅ wired | **local** on-device notifications — no server, no push | | UnifiedPush (ntfy) | ⚪ optional | not needed for reminders; stub for future server-initiated msgs | ## The city parking map The **City map** layer on the Map tab is the City of Sandpoint's printed *Downtown & Waterfront Public Parking* map, georeferenced and drawn in the same colours as the legend: 2-hour free, 3-hour, 4-hour, no time limit, and the paid city lots. 49 areas in all. **The free areas never touch ParkSmarter.** They live in the local database (bundled with the app, refreshed from the zone-labels server, cached on-device), the countdown is the phone's own clock, and the notification is the same foreground service every other session uses. So tracking your time on a free city spot works with no account, no signal, no payment, and in Anonymous Mode. The **green city lots are the exception** — they're the map's only paid category, and paying for them means ParkSmarter. They're hidden entirely when you're not signed in, since parking you can't actually buy is worse than no parking at all. (Standing in one and tapping "Park here" says so rather than reporting nothing nearby.) A single lot can be flipped back via the server's `requiresAccount` field if it turns out to take payment another way. Two ways to start: - **Park here** — pins your car from GPS and works out which area you're in. No GPS fix (garage, indoors, radio off)? It asks you to tap the spot instead and pins that. The pin stays on the map until you end the session, because "where did I leave the car" is half the point. - **Tap a coloured segment** — pick the block directly, no pin needed. Either way you choose how long to track, capped at the posted limit (a 2-hour space won't offer to run a 4-hour timer — that's just scheduling a ticket). The ongoing notification's second button reads **+1 hr** here rather than *Extend*: there is nothing to buy, so it edits the local timer and says so. The **Sessions** tab shows and manages these under *Tracking on this phone* — add an hour, end it, and see recent ones — with no account and no network, because that is the only place they exist. ParkSmarter's own sessions are layered on top when you're signed in, and failing to reach them (offline, or signed out) never hides the local half. The georeference was fitted to OpenStreetMap street centrelines and lands within ~4 m (see [`tools/citymap/`](tools/citymap/) to regenerate it from a new edition of the PDF). Because a few metres is the difference between two sides of a street, **Account → Align city map** lets you nudge the whole overlay against a live GPS fix and save it — on the phone, or published to the server for every device if you hold the admin token. ## Privacy BigBrainParking sends your location to ParkSmarter **only** when you explicitly tap "My location" and search; it opens on your last parking lot instead of your GPS, and ships **no** analytics/tracking (no Segment/Amplitude/Firebase/Sentry, no ad-ID, no Google services). For a plain-language comparison with the official app — which auto-sends your GPS on map open and bundles that tracking stack — see **[docs/OFFICIAL_APP_PRIVACY.md](docs/OFFICIAL_APP_PRIVACY.md)**. What reaches the API and what never does is also spelled out in the app's **About** page. ## Build & run (dev) Requires Node 20, JDK 17, Android SDK, and a GrapheneOS device (or any Android device) with USB debugging. ```bash npm install # installs both workspaces npm run build --workspace parksmarter-client # compile the client cd app npx expo prebuild --platform android # generate native project npx expo run:android # build + install a dev client ``` The app talks to prod (`apiv2.parksmarter.com`) by default — change `extra.psEnvironment` in `app/app.json` to point elsewhere. ## Notifications on GrapheneOS Everything here is **entirely on-device** — no server, no push, no FCM, no Play Services — so it works fully offline. UnifiedPush (ntfy) is wired only as an optional, no-op stub for any *future* server-initiated messages; nothing time-based needs it. **The ongoing parking countdown.** Whenever a session is active — a paid one you bought or a free check-in — a persistent notification shows the time left and ticks down, with **End** and **Extend** buttons. It is held up by a real **foreground service** (`modules/bbp-notify`, type `specialUse`), which is what makes it stick on GrapheneOS the way ntfy's does: it survives the app being killed, can't be swiped away, and a `BOOT_COMPLETED` receiver brings it back after a reboot. The service's life is exactly the session's life — it stops itself, removing the notification, on **End**, when the meter runs out, or whenever there's no session to show. The countdown itself costs no battery: the end time is handed to Android as a [chronometer](https://developer.android.com/reference/android/app/Notification.Builder#setChronometerCountDown(boolean)), and the system redraws the ticking text with the app closed and no timer of its own. - **End** stops tracking and clears the notification. On a *free check-in* that genuinely ends it. On a *paid* session it only stops the display — ParkSmarter has no stop-session endpoint, so time you already bought keeps running at the meter either way. - **Extend** (labeled **Pay** on a free check-in) opens the purchase screen for that exact zone. The active session is stored locally *with its zone*, so this works offline, in Anonymous Mode, and after a reboot. Extending doesn't end anything until the purchase actually goes through. **Expiry reminders** fire a configurable lead time (default 15 min) before the end, via `AlarmManager`. Configure them in **Account → Notifications**, where a **"Send a test reminder"** button lets you confirm they fire on your phone; the same screen has a toggle for the ongoing countdown. ## Distribution via Obtainium (self-hosted) Tag a release and CI builds a signed APK and publishes it to this repo's releases; your phone's Obtainium tracks the repo and offers updates. See [`docs/DISTRIBUTION.md`](docs/DISTRIBUTION.md) for the full server + CI + keystore setup.