diff --git a/android/BlastoiseNative/.gitignore b/android/BlastoiseNative/.gitignore
new file mode 100644
index 0000000..5b3641b
--- /dev/null
+++ b/android/BlastoiseNative/.gitignore
@@ -0,0 +1,5 @@
+.gradle/
+build/
+app/build/
+local.properties
+*.iml
diff --git a/android/BlastoiseNative/DESIGN.md b/android/BlastoiseNative/DESIGN.md
new file mode 100644
index 0000000..03a6454
--- /dev/null
+++ b/android/BlastoiseNative/DESIGN.md
@@ -0,0 +1,34 @@
+# Blastoise Native Android Design Brief
+
+Design direction: broadcast-console music app, not generic AI dashboard.
+
+## Principles
+
+- Put the listening task first: current channel, current track, transport, progress, channel switcher, and queue preview must stay visible on the main screen.
+- Use explicit states: connected, loading, disconnected, guest session, drift, buffering, and playback state are visible without opening logs.
+- Avoid generic AI decoration: no random blobs, ornamental gradients, nested cards, oversized empty hero areas, or decorative metrics.
+- Keep controls direct and tactile: large touch targets, clear labels, predictable placement, no hidden primary actions.
+- Keep diagnostics subordinate: useful for development, but visually quieter than playback.
+
+## Theme
+
+- Personality: late-night radio console, compact and technical.
+- Display/body font: Rajdhani.
+- Diagnostics font: JetBrains Mono.
+- Background: near-black.
+- Surfaces: layered blue-black panels.
+- Accents: safety orange for primary actions, level-meter green for live sync, brass for warnings/session state, restrained red for failures.
+- Avoid blue, cyan, and purple as dominant interface colors.
+- Corners: squared receiver hardware. Use small 2-4dp radii, never bubbly cards.
+
+## Layout
+
+- Header: app identity plus connection status.
+- Server row: compact editable endpoint and reconnect action.
+- Mode switch: RADIO and LIBRARY are first-class modes.
+- Main deck: station/library identity, track title, progress, transport, session/sync health.
+- Transport controls: use a real player strip with previous, seek back, large play/pause, seek forward, and next. Avoid generic equal-width text buttons for playback.
+- Radio channels: station cards that feel like playlists/radio rooms the user can join.
+- Library: dense track rows for direct local-style MP3 playback from the server library.
+- Queue: next few tracks for Radio mode, not the whole library.
+- Diagnostics: compact, monospaced, last few events.
diff --git a/android/BlastoiseNative/README.md b/android/BlastoiseNative/README.md
new file mode 100644
index 0000000..dcadea8
--- /dev/null
+++ b/android/BlastoiseNative/README.md
@@ -0,0 +1,45 @@
+# Blastoise Native Android
+
+Native Kotlin listener app for the Blastoise/MusicRoom server.
+
+## Build
+
+From this directory:
+
+```bat
+gradlew.bat :app:assembleDebug
+```
+
+The debug APK is written to:
+
+```text
+app\build\outputs\apk\debug\app-debug.apk
+```
+
+## Run
+
+Use the root Android helper:
+
+```bat
+..\run.bat
+```
+
+Or install manually:
+
+```bat
+gradlew.bat :app:installDebug
+adb shell am start -n com.peterino.blastoise/.MainActivity
+```
+
+## MVP scope
+
+- Defaults to `http://mhsgroove.peterino.com:3001` and auto-connects on launch.
+- Saves a server URL locally.
+- Uses `/api/channels` to establish an authenticated or guest session.
+- Supports `/api/auth/login` for named users.
+- Connects to `/api/channels/:id/ws`.
+- Plays `/api/tracks/:id` through AndroidX Media3.
+- Corrects playback drift when the server timestamp differs by 2 seconds or more.
+- Shows channel switching, queue preview, sync health, drift, session state, and recent WebSocket events on the main screen.
+
+Offline track caching is intentionally not included yet.
diff --git a/android/BlastoiseNative/app/build.gradle.kts b/android/BlastoiseNative/app/build.gradle.kts
new file mode 100644
index 0000000..2382ab5
--- /dev/null
+++ b/android/BlastoiseNative/app/build.gradle.kts
@@ -0,0 +1,36 @@
+plugins {
+ id("com.android.application")
+}
+
+android {
+ namespace = "com.peterino.blastoise"
+ compileSdk = 36
+
+ defaultConfig {
+ applicationId = "com.peterino.blastoise"
+ minSdk = 26
+ targetSdk = 36
+ versionCode = 1
+ versionName = "0.1.0"
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlin {
+ compilerOptions {
+ jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
+ }
+ }
+}
+
+dependencies {
+ val media3Version = "1.10.1"
+
+ implementation("androidx.media3:media3-exoplayer:$media3Version")
+ implementation("androidx.media3:media3-session:$media3Version")
+ implementation("androidx.media3:media3-common:$media3Version")
+ implementation("com.squareup.okhttp3:okhttp:5.3.0")
+}
diff --git a/android/BlastoiseNative/app/src/main/AndroidManifest.xml b/android/BlastoiseNative/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..d4dcb17
--- /dev/null
+++ b/android/BlastoiseNative/app/src/main/AndroidManifest.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/BlastoiseNative/app/src/main/java/com/peterino/blastoise/MainActivity.kt b/android/BlastoiseNative/app/src/main/java/com/peterino/blastoise/MainActivity.kt
new file mode 100644
index 0000000..5f9a767
--- /dev/null
+++ b/android/BlastoiseNative/app/src/main/java/com/peterino/blastoise/MainActivity.kt
@@ -0,0 +1,1507 @@
+package com.peterino.blastoise
+
+import android.Manifest
+import android.app.Activity
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.PackageManager
+import android.content.res.ColorStateList
+import android.graphics.Canvas
+import android.graphics.Typeface
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.GradientDrawable
+import android.os.Build
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.os.SystemClock
+import android.text.TextUtils
+import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup
+import android.widget.EditText
+import android.widget.LinearLayout
+import android.widget.ScrollView
+import android.widget.SeekBar
+import android.widget.TextView
+import androidx.media3.common.MediaItem
+import androidx.media3.common.MediaMetadata
+import androidx.media3.common.Player
+import androidx.media3.session.MediaController
+import androidx.media3.session.SessionToken
+import java.util.concurrent.Executor
+import kotlin.math.abs
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.random.Random
+
+private class CenteredIconButton(context: Context) : View(context) {
+ private var icon: Drawable? = null
+ private var iconSizePx = 0
+
+ init {
+ isClickable = true
+ isFocusable = true
+ }
+
+ fun setIcon(drawable: Drawable?, color: Int, sizePx: Int) {
+ icon = drawable?.mutate()?.apply { setTint(color) }
+ iconSizePx = sizePx
+ invalidate()
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ super.onDraw(canvas)
+ val drawable = icon ?: return
+ val availableWidth = width - paddingLeft - paddingRight
+ val availableHeight = height - paddingTop - paddingBottom
+ val size = min(iconSizePx, min(availableWidth, availableHeight)).coerceAtLeast(0)
+ val left = paddingLeft + (availableWidth - size) / 2
+ val top = paddingTop + (availableHeight - size) / 2
+ drawable.setBounds(left, top, left + size, top + size)
+ drawable.draw(canvas)
+ }
+}
+
+class MainActivity : Activity(), MusicRoomClient.Listener {
+ private enum class ScreenMode { RADIO, LIBRARY, PLAYLISTS }
+
+ private lateinit var client: MusicRoomClient
+ private val mainHandler = Handler(Looper.getMainLooper())
+ private var controllerFuture: com.google.common.util.concurrent.ListenableFuture? = null
+ private var controller: MediaController? = null
+
+ private lateinit var displayFont: Typeface
+ private lateinit var bodyFont: Typeface
+ private lateinit var bodyStrongFont: Typeface
+ private lateinit var monoFont: Typeface
+
+ private lateinit var serverInput: EditText
+ private lateinit var statusBadge: TextView
+ private lateinit var radioTab: TextView
+ private lateinit var libraryTab: TextView
+ private lateinit var playlistTab: TextView
+ private lateinit var modeLabel: TextView
+ private lateinit var sourceLabel: TextView
+ private lateinit var titleText: TextView
+ private lateinit var metaText: TextView
+ private lateinit var elapsedText: TextView
+ private lateinit var durationText: TextView
+ private lateinit var progressSeek: SeekBar
+ private lateinit var prevButton: CenteredIconButton
+ private lateinit var rewindButton: CenteredIconButton
+ private lateinit var playButton: CenteredIconButton
+ private lateinit var forwardButton: CenteredIconButton
+ private lateinit var nextButton: CenteredIconButton
+ private lateinit var playbackModeButton: TextView
+ private lateinit var queueCurrentButton: TextView
+ private lateinit var playNextCurrentButton: TextView
+ private lateinit var syncBadge: TextView
+ private lateinit var driftBadge: TextView
+ private lateinit var sessionBadge: TextView
+ private lateinit var radioContent: LinearLayout
+ private lateinit var libraryContent: LinearLayout
+ private lateinit var playlistContent: LinearLayout
+ private lateinit var debugText: TextView
+
+ private var screenMode = ScreenMode.RADIO
+ private var currentChannelId: String? = null
+ private var currentTrackId: String? = null
+ private var localLibraryIndex = -1
+ private var lastPaused = true
+ private var lastStatus = "Starting"
+ private var lastChannels: List = emptyList()
+ private var libraryTracks: List