Skip to content

Commit ee03b3d

Browse files
areeyanclaude
andcommitted
AKL Live: Android app for live 27H buses and the post-CRL train map
Kotlin + Jetpack Compose, everything drawn with Canvas: - Buses: animated scene per direction (time-of-day sky, Sky Tower skyline, bus driving to the stop by ETA), live delay, stops away, GPS distance/speed/occupancy, next five buses, live 27H route map - Trains: zoomable schematic after AT's Nga Tereina map, every train gliding between GPS fixes with heading and late markers; tap a train for its destination, speed and next stops, a station for live platform departures; line filters and network overview - CI builds a signed release APK and publishes it to Releases Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
0 parents  commit ee03b3d

32 files changed

Lines changed: 3764 additions & 0 deletions

‎.github/workflows/build.yml‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: Build APK
2+
3+
on:
4+
push:
5+
branches: [main]
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
build:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- uses: actions/setup-java@v4
18+
with:
19+
distribution: temurin
20+
java-version: 17
21+
22+
- uses: gradle/actions/setup-gradle@v4
23+
with:
24+
gradle-version: "8.11.1"
25+
26+
- name: Signing key
27+
env:
28+
KEYSTORE_B64: ${{ secrets.KEYSTORE_B64 }}
29+
run: |
30+
if [ -n "$KEYSTORE_B64" ]; then echo "$KEYSTORE_B64" | base64 -d > app/release.p12; fi
31+
32+
- name: Build
33+
env:
34+
AT_API_KEY: ${{ secrets.AT_API_KEY }}
35+
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
36+
run: gradle :app:assembleRelease --no-daemon --stacktrace
37+
38+
- name: Name the APK
39+
run: cp app/build/outputs/apk/release/app-release.apk AKL-Live.apk
40+
41+
- uses: actions/upload-artifact@v4
42+
with:
43+
name: AKL-Live
44+
path: AKL-Live.apk
45+
46+
- name: Publish to the "latest" release
47+
env:
48+
GH_TOKEN: ${{ github.token }}
49+
run: |
50+
gh release delete latest --yes --cleanup-tag || true
51+
gh release create latest AKL-Live.apk \
52+
--title "AKL Live (build ${{ github.run_number }})" \
53+
--notes "Built from ${{ github.sha }}. Download AKL-Live.apk on your phone and open it to install or update."

‎.gitignore‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.gradle/
2+
build/
3+
app/build/
4+
local.properties
5+
*.p12
6+
*.jks
7+
signing/
8+
tools/stops.json
9+
__pycache__/
10+
.idea/
11+
*.iml

‎README.md‎

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# AKL Live
2+
3+
An Android app for live Auckland Transport buses and trains, built for the
4+
Galaxy S26. It's the phone version of
5+
[akl-departure-board](https://github.com/intelarc/akl-departure-board), the
6+
ESP32 desk display.
7+
8+
## Install
9+
10+
Open **Releases → latest** on the phone, download `AKL-Live.apk` and open it.
11+
Android asks once to allow installs from your browser. Every push to `main`
12+
builds a new signed APK there. Installing it updates the app in place.
13+
14+
## Buses
15+
16+
The 27H at Aldersgate Road, Hillsborough, both directions: stop 8669 to
17+
Britomart and 8664 to Waikowhai. Stops and route can be changed in Settings.
18+
19+
- **A live scene per direction.** The bus drives toward the stop as its arrival
20+
counts down, and its wheels turn while it's moving. The sky follows the real
21+
time of day: sunrise, the sun's arc, sunset, then stars and a crescent moon,
22+
with lit windows and the Sky Tower's beacon. The city lane has the Auckland
23+
skyline; the Waikowhai lane has houses and pōhutukawa in flower.
24+
- **The next bus in detail.** It shows the expected arrival against the
25+
timetable, on time or minutes late, and how many stops away the bus is. From
26+
its GPS it also shows how far away it is in km, its speed, how full it is,
27+
and its fleet number.
28+
- **The next five buses** with live and scheduled times.
29+
- **A live map of the whole 27H.** Both directions are drawn from the stops'
30+
real positions, with every bus on its way to you pointing where it's
31+
heading, tagged with its countdown.
32+
33+
## Trains
34+
35+
A live map of the post-CRL network, drawn after AT's official "Ngā Tereina"
36+
map. It has the red City Rail Link loop (Waitematā, Te Waihorotiu,
37+
Karanga-a-Hape, Grafton, Newmarket, Parnell), and E-W and O-W run side by side
38+
out west. Harbours and volcanic cones sit behind it.
39+
40+
- **Every train, live.** Markers glide between GPS fixes, show their direction
41+
of travel, and carry an orange or red dot when a train is running 2 or 5+
42+
minutes late.
43+
- **Pinch, pan and double-tap to zoom.** All 44 station names appear as you
44+
zoom in, placed so they never overlap.
45+
- **Tap a train** for its destination, speed, delay, how full it is and its
46+
carriage number. You also get its next ten stops with expected times.
47+
- **Tap a station** for live departures from every platform, with line,
48+
destination, delay and countdown.
49+
- **Line filters** (E-W, S-C, O-W) with live counts, and a network overview
50+
showing how many trains on each line are running late.
51+
- Light and dark map, following the system theme. Landscape puts the map
52+
beside the panel.
53+
54+
## How it's built
55+
56+
Kotlin + Jetpack Compose. Everything is drawn with Compose Canvas, with no
57+
map or chart libraries. Data comes straight from the
58+
[AT developer API](https://dev-portal.at.govt.nz):
59+
60+
| What | Endpoint |
61+
|---|---|
62+
| Stop timetable | `gtfs/v3/stops/{id}/stoptrips` |
63+
| Live delays, stops away | `realtime/legacy/tripupdates?tripid=` |
64+
| Bus and train GPS | `realtime/legacy/vehiclelocations?tripid=` / `?vehicleid=` (all trains are 59xxx) |
65+
| Train destination and stops | `gtfs/v3/trips/{id}` and `.../stoptimes` |
66+
| Station departures | stoptrips for each platform (platforms from `parent_station`) |
67+
68+
Train GPS fixes are snapped onto the nearest station-to-station stretch of the
69+
train's own line on the schematic. `tools/gen_data.py` generates the map
70+
geometry, the station/platform table and the 27H's stop lists into
71+
`app/src/main/java/nz/aryan/akllive/data/`:
72+
73+
AT_API_KEY=... python tools/gen_data.py
74+
75+
The app polls only while it's on screen: buses every 30 s, trains every 15 s.
76+
77+
## Building
78+
79+
GitHub Actions builds `app-release.apk` on every push (`.github/workflows/build.yml`).
80+
It uses three repository secrets:
81+
82+
- `AT_API_KEY`: built into the APK as the default key (Settings can override it)
83+
- `KEYSTORE_B64`, `KEYSTORE_PASSWORD`: the signing key. It's kept locally in
84+
`signing/`, which is gitignored. Back it up: updates must be signed with
85+
the same key.
86+
87+
Local builds need JDK 17, the Android SDK and Gradle 8.11:
88+
`gradle :app:assembleRelease`. Without the secrets, you get a debug-signed
89+
APK with no built-in key.
90+
91+
Not affiliated with Auckland Transport.

‎app/build.gradle.kts‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
plugins {
2+
id("com.android.application")
3+
id("org.jetbrains.kotlin.android")
4+
id("org.jetbrains.kotlin.plugin.compose")
5+
}
6+
7+
// CI supplies these; local builds fall back to a debug signature and no
8+
// built-in API key (enter one in the app's settings instead).
9+
val runNumber = (System.getenv("GITHUB_RUN_NUMBER") ?: "1").toInt()
10+
val atKey = System.getenv("AT_API_KEY") ?: ""
11+
val keystore = file("release.p12")
12+
13+
android {
14+
namespace = "nz.aryan.akllive"
15+
compileSdk = 35
16+
17+
defaultConfig {
18+
applicationId = "nz.aryan.akllive"
19+
minSdk = 26
20+
targetSdk = 35
21+
versionCode = runNumber
22+
versionName = "1.0.$runNumber"
23+
buildConfigField("String", "AT_API_KEY", "\"$atKey\"")
24+
}
25+
26+
signingConfigs {
27+
create("release") {
28+
if (keystore.exists()) {
29+
storeFile = keystore
30+
storeType = "pkcs12"
31+
storePassword = System.getenv("KEYSTORE_PASSWORD")
32+
keyAlias = "release"
33+
keyPassword = System.getenv("KEYSTORE_PASSWORD")
34+
}
35+
}
36+
}
37+
38+
buildTypes {
39+
release {
40+
isMinifyEnabled = false
41+
signingConfig = if (keystore.exists()) signingConfigs.getByName("release")
42+
else signingConfigs.getByName("debug")
43+
}
44+
}
45+
46+
compileOptions {
47+
sourceCompatibility = JavaVersion.VERSION_17
48+
targetCompatibility = JavaVersion.VERSION_17
49+
}
50+
kotlinOptions {
51+
jvmTarget = "17"
52+
}
53+
buildFeatures {
54+
compose = true
55+
buildConfig = true
56+
}
57+
}
58+
59+
dependencies {
60+
val bom = platform("androidx.compose:compose-bom:2024.12.01")
61+
implementation(bom)
62+
implementation("androidx.compose.ui:ui")
63+
implementation("androidx.compose.ui:ui-graphics")
64+
implementation("androidx.compose.foundation:foundation")
65+
implementation("androidx.compose.material3:material3")
66+
implementation("androidx.activity:activity-compose:1.9.3")
67+
implementation("androidx.core:core-ktx:1.15.0")
68+
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
69+
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
70+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
71+
}

‎app/src/main/AndroidManifest.xml‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
3+
4+
<uses-permission android:name="android.permission.INTERNET" />
5+
6+
<application
7+
android:label="@string/app_name"
8+
android:icon="@mipmap/ic_launcher"
9+
android:roundIcon="@mipmap/ic_launcher_round"
10+
android:supportsRtl="true"
11+
android:theme="@style/Theme.AklLive">
12+
<activity
13+
android:name=".MainActivity"
14+
android:exported="true"
15+
android:theme="@style/Theme.AklLive">
16+
<intent-filter>
17+
<action android:name="android.intent.action.MAIN" />
18+
<category android:name="android.intent.category.LAUNCHER" />
19+
</intent-filter>
20+
</activity>
21+
</application>
22+
</manifest>

0 commit comments

Comments
 (0)