A real-time transit app for the Kelantan public bus network — native Android, built with Jetpack Compose.
Package:
com.taqisystems.bus.android
Min SDK: 24 (Android 7.0) · Target SDK: 36
Build status:
| Feature | Description |
|---|---|
| Live map | Google Map centred on user location; zoom-adaptive stop markers |
| Stop arrivals | Real-time + scheduled arrivals from OneBusAway API with 30 s auto-refresh |
| Arrival reminders | Push reminder N minutes before your ride arrives (OneSignal + sidecar) |
| Destination alerts | GPS foreground service: TTS voice + vibration as bus approaches your stop |
| Trip planner | Origin → Destination routing via OpenTripPlanner |
| Itinerary view | Leg-by-leg breakdown with a Destination Alert FAB |
| Route timeline | Live vehicle position overlaid on stop timeline |
| Saved stops | Persist favourite stops across sessions |
| Multi-region | 11 Malaysian regions selectable at runtime |
| Localisation | English + Bahasa Malaysia UI strings |
| Layer | Technology |
|---|---|
| Language | Kotlin 2.2.0 |
| UI | Jetpack Compose (BOM 2025.04.01), Material 3 |
| Navigation | androidx.navigation:navigation-compose 2.9.0 |
| Maps | maps-compose 6.4.4 · play-services-maps 19.2.0 |
| Networking | OkHttp 4.12.0 · Retrofit 2.11.0 · Gson 2.13.1 |
| Transit data | OneBusAway Kotlin SDK 0.1.0-alpha.77 |
| Push notifications | OneSignal SDK 5.1.21 |
| Persistence | DataStore Preferences 1.1.4 |
| Build | AGP 9.0.1 · Gradle 9.1.0 |
- Android Studio Meerkat or later
- JDK 11+
- A Google Maps API key (Maps SDK for Android enabled)
- A OneSignal account and App ID
git clone <repo-url>
cd KelantanBuslocal.properties is gitignored. Create it in the project root with the following content:
# ── Android SDK ───────────────────────────────────────────────────────────────
sdk.dir=/path/to/Android/Sdk
# ── Release signing ───────────────────────────────────────────────────────────
STORE_FILE=/absolute/path/to/keystore.jks
STORE_PASSWORD=<keystore password>
KEY_ALIAS=<key alias>
KEY_PASSWORD=<key password>
# ── API keys ──────────────────────────────────────────────────────────────────
ONESIGNAL_APP_ID=<your OneSignal App ID>
GOOGLE_MAPS_API_KEY=<your Google Maps API key>
# ── OBA user-agent ────────────────────────────────────────────────────────────
OBA_USER_AGENT=KelantanBus
# ── Branding (optional — defaults shown) ──────────────────────────────────────
APP_NAME=Kelantan Bus
APP_PRIMARY_COLOR=#C62828
APP_SECONDARY_COLOR=#E53935
APP_TERTIARY_COLOR=#37474F
# ── Service URLs (required — Gradle sync fails if missing) ────────────────────
OBA_BASE_URL=https://api.kelantanbus.com
GEOCODING_BASE_URL=https://geocode.kelantanbus.com/v1
REGIONS_URL=https://cdn.unrealasia.net/onebusaway/regions.json
# ── Social / support URLs (required — Gradle sync fails if missing) ───────────
FACEBOOK_PAGE_URL=https://www.facebook.com/kelantanbus
WHATSAPP_PHONE=60109141767
STATUS_PAGE_URL=https://status.kelantanbus.comSecurity:
ONESIGNAL_APP_IDandGOOGLE_MAPS_API_KEYare validated at Gradle sync time and injected viaBuildConfig/manifestPlaceholders. They are never present in any committed file. If either key was ever accidentally committed, revoke and regenerate it immediately.
# Debug APK
./gradlew assembleKelantanbusDebug
# Signed release APK
./gradlew assembleKelantanbusRelease
# Install on connected device
./gradlew installKelantanbusDebugThe project is structured for multiple transit agency brands using Android product flavors. No Kotlin source files need to be touched to produce a new brand.
Branding
| Key | Default | Effect |
|---|---|---|
APP_NAME |
Kelantan Bus |
Launcher label, About screen, onboarding, notification titles |
APP_PRIMARY_COLOR |
#C62828 |
Buttons, toolbar, FAB, active nav icon |
APP_SECONDARY_COLOR |
#E53935 |
Accent chips, live-status badges, active tab |
APP_TERTIARY_COLOR |
#37474F |
Supporting text, icon tints |
Service URLs (required — Gradle sync fails if missing)
| Key | Effect |
|---|---|
OBA_BASE_URL |
Base URL for all OneBusAway API calls |
GEOCODING_BASE_URL |
Base URL for the Photon geocoding API |
REGIONS_URL |
OBA regions manifest URL |
Social / support URLs (required — Gradle sync fails if missing)
| Key | Effect |
|---|---|
FACEBOOK_PAGE_URL |
Facebook link in the Feedback screen |
WHATSAPP_PHONE |
WhatsApp number (E.164 without +) in the Feedback screen |
STATUS_PAGE_URL |
Service-status link in the More screen |
Place files under app/src/<flavourName>/res/ — the flavor resource always
shadows the equivalent file in app/src/main/res/:
| Asset | Path within res/ |
|---|---|
| In-app / onboarding logo | drawable/logo.png |
| Launcher icon | mipmap-{mdpi,hdpi,xhdpi,xxhdpi,xxxhdpi}/ic_launcher*.png |
| Adaptive icon foreground | mipmap-{density}/ic_launcher_foreground.png |
| Notification alert sound | raw/alert.wav (WAV / OGG / MP3, under 1 MB) |
-
Add a flavor block to
app/build.gradle.kts:create("johor") { dimension = "brand" applicationId = "com.yourcompany.johorbus" }
-
Create
app/src/johor/res/drawable/logo.pngand mipmap launcher icons. -
Optionally add
app/src/johor/res/raw/alert.wavfor a custom notification sound. -
Set your branding, API keys, and service URLs in
local.properties:APP_NAME=Johor Bus APP_PRIMARY_COLOR=#0057B8 APP_SECONDARY_COLOR=#003F84 APP_TERTIARY_COLOR=#1A1A2E ONESIGNAL_APP_ID=<johor onesignal app id> GOOGLE_MAPS_API_KEY=<johor maps api key> OBA_BASE_URL=https://api.johorbus.com GEOCODING_BASE_URL=https://geocode.johorbus.com/v1 REGIONS_URL=https://cdn.example.com/onebusaway/regions.json FACEBOOK_PAGE_URL=https://www.facebook.com/johorbus WHATSAPP_PHONE=601XXXXXXXXX STATUS_PAGE_URL=https://status.johorbus.com
-
Build:
./gradlew assembleJohorRelease
Notification sound note: Channel IDs are scoped to
packageName(e.g.com.yourcompany.johorbus_reminders). This prevents Android from locking in a previous brand's sound, since the OS caches a channel's sound on first creation and it cannot be changed programmatically afterward.
KelantanBus/
├── app/
│ ├── build.gradle.kts
│ └── src/
│ ├── kelantanbus/res/ ← KelantanBus brand resources
│ ├── generic/res/ ← White-label template
│ └── main/
│ ├── AndroidManifest.xml
│ ├── res/
│ │ ├── drawable/ ← default logo + icons
│ │ ├── mipmap-*/ ← adaptive launcher icons
│ │ ├── raw/alert.wav ← default notification sound
│ │ ├── values/ ← English strings + themes
│ │ └── values-ms/ ← Bahasa Malaysia strings
│ └── java/com/taqisystems/bus/android/
│ ├── KelantanBusApplication.kt
│ ├── MainActivity.kt
│ ├── ServiceLocator.kt
│ ├── data/
│ │ ├── AppPreferences.kt
│ │ ├── model/Models.kt
│ │ └── repository/
│ │ ├── ObaRepository.kt
│ │ ├── OtpRepository.kt
│ │ ├── GeocodingRepository.kt
│ │ └── RegionsRepository.kt
│ ├── service/
│ │ └── DestinationAlertService.kt
│ └── ui/
│ ├── map/VehicleMarkerFactory.kt
│ ├── navigation/AppNavigation.kt
│ ├── screens/
│ ├── theme/
│ │ ├── BrandConfig.kt ← runtime brand color parser
│ │ ├── Color.kt
│ │ ├── Theme.kt
│ │ └── Type.kt
│ └── viewmodel/
├── gradle/libs.versions.toml
├── local.properties ← gitignored, never committed
├── DOCUMENTATION.md ← full technical reference
└── README.md ← this file
The app follows a single-Activity, MVVM + Repository pattern:
Compose UI ──▶ ViewModel ──▶ Repository ──▶ OBA SDK / OkHttp / DataStore
│
ServiceLocator (singleton DI)
- ServiceLocator initialises all repositories once in
Application.onCreateand exposes them as singletons. - ViewModels hold
StateFlows consumed by Composables. - Repositories wrap all I/O: OneBusAway SDK, OpenTripPlanner REST, Pelias geocoding, DataStore preferences, and the OBA sidecar.
| Service | Base URL | Auth |
|---|---|---|
| OneBusAway | https://api.kelantanbus.com |
key=TEST query param |
| OpenTripPlanner | https://otp.kelantanbus.com/otp/routers/default/ |
None |
| Pelias Geocoding | https://geocode.kelantanbus.com/v1/ |
None |
| Regions JSON | https://cdn.unrealasia.net/onebusaway/regions.json |
None |
| OBA Sidecar | https://sidecar.kelantanbus.com |
None |
The OBA and OTP base URLs can be overridden at runtime via the Settings screen and are persisted to DataStore.
| Permission | Reason |
|---|---|
INTERNET |
API calls |
ACCESS_FINE_LOCATION / ACCESS_COARSE_LOCATION |
Live map, destination alerts |
RECORD_AUDIO |
Voice search on trip planner |
POST_NOTIFICATIONS |
Arrival reminders, destination alert notifications |
FOREGROUND_SERVICE / FOREGROUND_SERVICE_LOCATION |
GPS tracking while on the bus |
VIBRATE |
Haptic feedback on destination alert |
Two notification channels are registered on app start, both scoped to
packageName to support white-label isolation:
| Channel | Importance | Sound |
|---|---|---|
{pkg}_reminders |
HIGH | res/raw/alert.wav |
{pkg}_destination |
LOW | Silent (ongoing foreground service) |
Arrival reminders are registered through the sidecar service and delivered via
OneSignal. The KelantanBusNotificationExtension intercepts each push, suppresses
OneSignal's default display, and re-posts via NotificationCompat to ensure the
custom sound plays.
See DOCUMENTATION.md for the complete technical reference, including:
- All domain models and repository method signatures
- AppPreferences DataStore schema
- Screen-by-screen UI breakdown and ViewModel state descriptions
- Map marker drawing specifications
- Known SDK quirks and workarounds
- Unit test reference
Copyright 2026 Taqi Systems
Licensed under the Apache License, Version 2.0. You may not use this project except in compliance with the License. A copy of the License is included in the LICENSE file.
All source files carry the following SPDX identifier:
SPDX-FileCopyrightText: 2026 Taqi Systems
SPDX-License-Identifier: Apache-2.0