BigBrainParking/README.md
Erik ad55559f55
All checks were successful
build-apk / build (push) Successful in 28m55s
v0.5.0: persistent parking notification — foreground service, End/Extend
The ongoing countdown now appears whenever a car is parked, paid or free,
and sticks around the way ntfy's does on GrapheneOS.

Why it wasn't showing at all on a paid park: StartSessionScreen carried its
own stricter copy of parseApiTime that demanded MM-DD-YYYY + AM/PM. The
dual-format fix from deb78bf only landed in sessionStatus.ts, so when the
API returned the other format the parser returned null and the notification
was simply never posted. There is now one parser (api/parseTime.ts), and the
failure logs instead of going silent.

Persistence: a plain notify() was never enough — Android 14+ lets the user
swipe an ongoing notification away, and nothing brought it back after a
reboot. BbpSessionService is a real foreground service (type specialUse) that
owns the notification, plus a BOOT_COMPLETED receiver to restore it; specialUse
is one of the types Android 14/15 still allow to start from BOOT_COMPLETED.
The service's life is exactly the session's life: it stops itself, removing
the notification, on End, at expiry, or when there's no session to show.

Buttons: paid sessions previously got no actions at all (only free check-ins
did). Both now get End and Extend/Pay. End stops tracking — honest about the
fact that ParkSmarter has no stop-session endpoint, so bought time keeps
running at the meter. Extend opens the purchase screen for that exact zone.

The active session (with its Zone) is persisted locally, so the countdown and
Extend survive reboot, offline, and Anonymous Mode instead of depending on a
round-trip. sessionStatus.ts + features/checkin collapse into one owner,
features/session/activeParking — one record, one notification.

Verified: :bbp-notify:compileDebugKotlin passes, and the merged app manifest
carries the service (specialUse + FGS subtype property), both receivers, and
the FOREGROUND_SERVICE/SPECIAL_USE/RECEIVE_BOOT_COMPLETED permissions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 20:10:54 +00:00

113 lines
5.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<p align="center">
<img src="app/assets/icon.png" alt="BigBrainParking — big-brain driver" width="220">
</p>
# 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) |
| 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 |
## 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.