Skip to content

alvinvinalon/android-command-deck

Android Command Deck Remote

A USB and LAN remote-control button pad for Windows 11. An Android app communicates with a small Windows companion over a local WebSocket connection.


Feature Overview

Feature Status
Volume up / down (5 % steps) ✅ implemented
System audio mute toggle ✅ implemented
Microphone mute toggle ✅ implemented
Configurable hotkeys (media keys, Fn keys) ✅ implemented
App launch by logical name ✅ implemented
Button grid (10 buttons, long-press to configure) ✅ implemented
Two built-in button pages (Media + Productivity) ✅ implemented
Persistent IP / port + button config ✅ implemented
USB wired connection via ADB reverse tunnel ✅ implemented
Automatic LAN discovery ❌ not in v1 — manual IP entry

Default Button Pages

The app ships with two built-in pages, each with 10 buttons. Swipe left or right on the control screen to switch pages; paging wraps around continuously between Media and Productivity. Long-press any button to reconfigure it, and that change is persisted for its page.

Media page

  1. Play/Pause
  2. Next Track
  3. Prev Track
  4. Vol +
  5. Vol -
  6. Mute
  7. Spotify
  8. Chrome
  9. Screenshot
  10. Mic Mute

Productivity page

  1. Screenshot
  2. Teams Mute
  3. Teams Camera
  4. YouTube
  5. GitHub
  6. NotebookLM
  7. Mute
  8. Gemini
  9. Rename
  10. Show Desktop

App Screenshots

Media button page screenshot Productivity button page screenshot

Left: Media page. Right: Productivity page.


Repo Layout

repo-root/
├── README.md                  ← this file
├── start-companion.ps1        ← launches the Windows companion
├── firewall-setup.ps1         ← one-time firewall setup (requires Admin)
├── setup-usb.ps1              ← sets up ADB reverse tunnel for USB mode
│
├── windows-companion/         ← .NET 8 ASP.NET Core WebSocket server
│   ├── WindowsCompanion.csproj
│   ├── Program.cs
│   ├── appsettings.json       ← port, hotkey map, app map
│   ├── WebSocketHandler.cs
│   ├── Actions/
│   │   ├── ActionMessage.cs   ← JSON message model
│   │   ├── ActionRegistry.cs  ← dictionary-backed action dispatch
│   │   └── CompanionOptions.cs
│   ├── Audio/
│   │   ├── AudioController.cs ← NAudio CoreAudio volume/mute
│   │   └── MicController.cs   ← NAudio CoreAudio mic mute
│   └── Input/
│       ├── HotkeySimulator.cs ← Win32 SendInput P/Invoke
│       └── AppLauncher.cs     ← Process.Start with config paths
│
└── android-client/            ← Kotlin + Jetpack Compose Android app
    ├── settings.gradle.kts
    ├── build.gradle.kts
    ├── gradle.properties
    ├── gradle/wrapper/gradle-wrapper.properties
    └── app/
        ├── build.gradle.kts
        └── src/main/
            ├── AndroidManifest.xml
            ├── java/com/streamdeckremote/android/
            │   ├── MainActivity.kt
            │   ├── model/          ActionMessage.kt, ButtonConfig.kt
            │   ├── viewmodel/      MainViewModel.kt, ConnectionState.kt
            │   ├── websocket/      WebSocketClient.kt
            │   ├── ui/             ConnectScreen.kt, ControlScreen.kt,
            │   │                   ButtonConfigSheet.kt, theme/
            │   └── persistence/    PreferencesRepository.kt
            └── res/values/         strings.xml, themes.xml

Prerequisite Matrix

If you have access to an AI coding assistant like GitHub Copilot, you can delegate much of the local setup, build, and iteration work in this repository. This project has also been co-developed with GitHub Copilot Coding Agents, so parts of the workflow and documentation assume an assistant can help execute commands, inspect files, and apply targeted changes.

Can be installed autonomously (no human approval required)

| Tool | Install command | | .NET 8 SDK | winget install Microsoft.DotNet.SDK.8 | | Git | winget install Git.Git | | JDK 17 | winget install EclipseAdoptium.Temurin.17.JDK | | Node.js (optional tooling) | winget install OpenJS.NodeJS.LTS |

Requires human setup, approval, or physical action

Item Why human action is needed
Android Studio GUI installer; license acceptance required. Download from developer.android.com/studio.
Android SDK / ADB Installed through Android Studio's SDK Manager. Requires accepting Android SDK licenses.
Physical Android device or emulator Running an AVD or connecting a phone over USB requires manual device setup.
Windows Firewall rule firewall-setup.ps1 needs an elevated (Administrator) PowerShell session.
LAN IP address The user must read their PC's LAN IP and type it into the Android app.
Steam / app paths in config appsettings.json Companion:Apps paths must match the actual install locations on the target machine.

Android Studio itself can usually be installed with winget install Google.AndroidStudio, but the first-launch SDK Manager flow, emulator/device setup, and license acceptance still require a human in the GUI.


Windows Companion: How to Run

  • .NET 8 SDK installed (see above)
  • Windows 10 or 11

First-time firewall setup (one-off, requires Admin)

Open an elevated PowerShell session and run:

# From the repository root:
.\firewall-setup.ps1

Equivalent manual netsh command if you prefer:

netsh advfirewall firewall add rule ^
    name="CommandDeckRemoteCompanion" ^
  dir=in action=allow protocol=TCP localport=8765 ^
  profile=any

The helper script and the equivalent manual rule above open the companion port on all Windows network profiles by default. That keeps the app usable on machines whose network category is forced to Public, but it also increases reachability; narrow the rule yourself if you only want Private/Domain access.

Starting the companion

# From the repository root (no Admin needed):
.\start-companion.ps1

By default, the launcher runs in Auto mode:

  • it uses windows-companion\publish\WindowsCompanion.exe when that output is current
  • it switches to dotnet run --configuration Release when the companion source is newer and a .NET SDK is installed
  • it still falls back to the published binary on no-SDK machines, but warns if that output looks stale

Optional overrides:

.\start-companion.ps1 -Mode Published
.\start-companion.ps1 -Mode Source

Or manually:

cd windows-companion
dotnet run --configuration Release

The companion prints its IP addresses and WebSocket URL on startup:

Command Deck Remote Companion listening on port 8765
  WebSocket : ws://<your-ip>:8765/ws
  Health    : http://<your-ip>:8765/health

Configuring hotkeys and app paths

Edit windows-companion/appsettings.json:

{
  "Companion": {
    "Port": 8765,
    "Hotkeys": {
      "play_pause": "VK_MEDIA_PLAY_PAUSE",
      "next_track": "VK_MEDIA_NEXT_TRACK",
      "prev_track": "VK_MEDIA_PREV_TRACK",
      "screenshot": "VK_SNAPSHOT"
    },
    "Apps": {
      "steam": "C:\\Program Files (x86)\\Steam\\steam.exe",
      "spotify": "spotify:",
      "chrome": "chrome.exe",
      "youtube": "chrome.exe https://www.youtube.com",
      "github": "chrome.exe https://github.com",
      "notebooklm": "chrome.exe https://notebooklm.google.com/",
      "gemini": "chrome.exe https://gemini.google.com/"
    }
  }
}

Spotify note: the value "spotify:" is a URI scheme handled by Windows Shell. With UseShellExecute = true, Windows routes it to the registered Spotify protocol handler and foregrounds the already-running Spotify window if one exists. No path configuration is needed.

Chrome note: chrome.exe relies on UseShellExecute = true so Windows resolves it via the App Paths registry key set by the Chrome installer. If Chrome is not on PATH, substitute the full path (e.g. C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe).

The key field in an ActionMessage must match one of the Hotkeys keys, or be a raw VK name (e.g. "VK_MEDIA_PLAY_PAUSE", "VK_SNAPSHOT", "F2", "WIN+D", "0xB3"). The app field must match one of the Apps keys. The shipped Productivity page expects youtube, github, notebooklm, and gemini to be present.


Windows Companion: Packaging for Another Machine

Use this flow to deploy the companion to a Windows gaming PC that does not have the .NET SDK installed. The output is a self-contained single-file executable — no SDK or runtime needed on the target machine.

1. Build the published package (development machine, SDK required)

Run from the repository root:

dotnet publish .\windows-companion\WindowsCompanion.csproj `
    -p:PublishProfile=win-x64-selfcontained

Or from within the windows-companion\ directory:

dotnet publish -p:PublishProfile=win-x64-selfcontained

Output lands in windows-companion\publish\. The folder contains:

  • WindowsCompanion.exe — self-contained, no runtime dependency
  • appsettings.jsonexternal and editable (not bundled into the exe)

The publish profile lives at windows-companion\Properties\PublishProfiles\win-x64-selfcontained.pubxml. It targets win-x64, self-contained, with single-file compression enabled.

2. Transfer to the target machine

Create a small staging folder for the target machine (zip it for convenience) with this layout:

CommandDeckRemoteCompanion-win-x64\
├── publish\
│   ├── WindowsCompanion.exe
│   └── appsettings.json
├── install-companion.ps1
└── firewall-setup.ps1

Populate it from the repo like this:

Item Where
windows-companion\publish\ (entire folder) As publish\ inside the staging folder
install-companion.ps1 Staging folder root
firewall-setup.ps1 Staging folder root

3. Run the installer on the target machine

Open a PowerShell window in the staging location and run:

.\install-companion.ps1

The installer will:

  1. Copy all files from publish\ to %LOCALAPPDATA%\CommandDeckRemoteCompanion\ (default).
  2. Preserve an existing target-machine appsettings.json by default so machine-specific app paths are not clobbered during reinstall.
  3. Prompt to run firewall-setup.ps1; if you accept and the shell is not already elevated, it launches an elevated sub-process automatically and uses the configured Companion:Port value from appsettings.json.

Optional parameters:

# Custom install directory
.\install-companion.ps1 -InstallDir "D:\Tools\CommandDeck"

# Skip firewall (configure manually later)
.\install-companion.ps1 -SkipFirewall

# Replace an existing appsettings.json during reinstall
.\install-companion.ps1 -OverwriteConfig

If the companion is already running from the destination, the installer warns and exits — stop it first.

install-companion.ps1 also tolerates two alternate layouts if you prefer them: windows-companion\publish\ next to the script, or WindowsCompanion.exe directly next to the script.

4. Configure app paths on the target machine

Open appsettings.json in the install directory and adjust Companion:Apps to match this machine:

"Apps": {
  "steam":   "C:\\Program Files (x86)\\Steam\\steam.exe",
  "spotify": "spotify:",
  "chrome":  "chrome.exe"
}

App paths vary per machine. The spotify: URI and chrome.exe shell-launch shortcuts often work without changes, but Steam and other game-launcher paths typically need updating.

5. Launch after install

& "$env:LOCALAPPDATA\CommandDeckRemoteCompanion\WindowsCompanion.exe"

Or use start-companion.ps1 from this repo directory — it auto-selects the freshest viable launch mode, preferring current published output when appropriate and switching to dotnet run when local source changes are newer.


Android Client: How to Build and Run

Prerequisites

  • Android Studio Hedgehog or later
  • Project is currently configured with minSdk 26 (Android 8.0), but versions below Android 13 are unverified
  • JDK 17 (bundled with Android Studio)
  • A physical Android device or an AVD

Tested Android Devices

The app has only been tested on Android 13+ devices, specifically:

  • Samsung S20 FE
  • Samsung S24 Ultra

Button sizing, spacing, and overall layout can differ on other screen sizes, aspect ratios, display-density settings, or Android vendor skins. Expect to make UI adjustments for other mobile devices.

Steps

  1. Install Android Studio from developer.android.com/studio.
  2. Accept Android SDK licenses in the SDK Manager (Android Studio → Settings → SDK Manager → SDK Licenses).
  3. Open the project: File → Open → navigate to android-client/.
  4. Let Gradle sync complete.
  5. Run on device or emulator via the ▶ Run button.

Testing with Android Emulator (AVD)

The Windows companion binds to 0.0.0.0:8765, so it accepts connections from the emulator out of the box — no additional companion config is needed.

  1. Start the companion first (see above). Confirm it is healthy:
    Invoke-RestMethod 'http://localhost:8765/health'
  2. Open the project in Android Studio, create or start an AVD via the Device Manager, and run the app.
  3. In the app's connect screen, enter:
    • IP address: 10.0.2.2 — the standard Android Emulator alias for the host machine's loopback.
    • Port: 8765
  4. Tap Connect. The emulator routes 10.0.2.2:8765 straight to the Windows host without any firewall rule or LAN pairing.

The firewall rule from firewall-setup.ps1 opens the configured port on all Windows network profiles by default. It is not required for emulator traffic, which arrives over the loopback interface.

Troubleshooting

  • The Android app intentionally uses ws:// cleartext traffic for trusted LAN demos only. The manifest opts into cleartext traffic so emulator and device connections can succeed without TLS.
  • If the app cannot connect, open http://<your-ip>:8765/health from another device on the same network first. If that fails, check the firewall rule and confirm both devices are on the same LAN without client isolation.
  • If a hotkey button does nothing, verify the button's key matches either a raw virtual-key name such as F13 or a logical alias defined under Companion:Hotkeys in windows-companion/appsettings.json.
  • If a Productivity page launcher button does nothing, verify the corresponding Companion:Apps entry exists for youtube, github, notebooklm, or gemini.

First use on device (Wi-Fi)

  1. Start the Windows companion first.
  2. Ensure phone and PC are on the same Wi-Fi network.
  3. Open the app → enter the PC's LAN IP and port 8765 → tap Connect.
  4. Use the button grid. Long-press any button to reconfigure it.

Wi-Fi connection setup screen

First use on device (USB)

  1. Enable USB Debugging on the device (Settings → Developer Options → USB Debugging).
  2. Connect the phone to the PC with a USB cable.
  3. Run .\setup-usb.ps1 from the repo root to set up the ADB reverse tunnel.
  4. Start the Windows companion.
  5. Open the app → tap the USB transport tab → confirm port 8765 → tap Establish Link.
  6. Use the button grid normally. See the USB Wired Connection section for full details.

USB connection setup screen

USB Wired Connection (Fallback)

Use this mode when Wi-Fi is unavailable, unreliable, or not allowed. The app connects to localhost on the device, and ADB routes that traffic to the Windows companion over the USB cable.

Prerequisites

  • USB cable between the Android device and the Windows PC.
  • USB Debugging enabled on the device: Settings → Developer Options → USB Debugging.
  • ADB (Android Debug Bridge) on the PC's PATH. Install via Android SDK Platform-Tools and add the folder to PATH.

1. Set up the ADB reverse tunnel

Run the helper script once per cable session (re-run after unplugging):

# From the repository root:
.\setup-usb.ps1

To use a non-default port:

.\setup-usb.ps1 -Port 9000

The script checks that adb is available, verifies a device is connected, and runs:

adb reverse tcp:<port> tcp:<port>

This tells the device to forward any connection it makes to localhost:<port> through the USB cable to localhost:<port> on the Windows PC.

2. Use the USB tab in the app

  1. On the Connect screen, tap the USB segment of the transport selector (top of the form).
  2. The host field automatically shows localhost (non-editable).
  3. Confirm the port matches your companion's configured port (default 8765).
  4. Tap Establish Link.

The connection badge in the top bar will read USB · ONLINE when the USB tunnel is active.

How it works

adb reverse creates a socket relay on the device: any TCP connection the Android app opens to 127.0.0.1:<port> is tunnelled over the USB debug channel to 127.0.0.1:<port> on the Windows host. No Wi-Fi, no LAN IP address, and no firewall rule change is required for USB mode. The companion currently listens on all host interfaces by default, but the USB tunnel itself still targets the host's loopback endpoint.

The tunnel is session-scoped. It is reset when the USB cable is disconnected or adb kill-server is run. Re-run setup-usb.ps1 to restore it.


JSON Message Protocol

All messages are UTF-8 text WebSocket frames:

{ "action": "volume_up" }
{ "action": "volume_down" }
{ "action": "volume_mute" }
{ "action": "mic_mute" }
{ "action": "teams_mute" }
{ "action": "teams_camera" }
{ "action": "hotkey", "key": "VK_MEDIA_PLAY_PAUSE" }
{ "action": "app_launch", "app": "steam" }

The server returns no response frames for action messages. Unknown actions are logged and silently ignored.


Architecture Notes

  • Transport: plain TCP WebSocket (ws://) over Wi-Fi (LAN) or USB (adb reverse tunnel). TLS is not required on a trusted LAN; add it for production use.
  • Pairing: manual IP + port in v1. mDNS/Bonjour discovery is a natural v2 addition.
  • Action dispatch: ActionRegistry uses a Dictionary<string, Func<ActionMessage, Task>> — adding a new action is a single dictionary entry, not a switch case.
  • Audio: NAudio CoreAudioApi (MMDeviceEnumerator) targets the system default render/capture device.
  • Input: Win32 SendInput P/Invoke with an explicit VK map; raw hex codes also accepted as fallback.
  • Android state: MainViewModel owns all state as StateFlows. WebSocketClient calls back into the ViewModel; UI only reads flows — transport and UI are fully decoupled.

About

Android remote-control button pad for Windows 11 that connects to a small local companion app over LAN or USB, letting you trigger hotkeys, media controls, app launches, and productivity actions from your phone.

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors