Inspired by WSA (Windows Subsystem for Android), macOS Subsystem for Android (MSA) runs the official Android Emulator as a VM backend and gives each Android app its own native macOS NSWindow.
There is no full AOSP checkout or build required: a single platform-signed privileged host agent is added on top of the stock Android 16 AOSP userdebug emulator image.
Status: Early-stage and actively developed. Core app streaming, input, and live window resizing all work today — see Known Limitations for details.
macOS / AppKit NSWindow
H.264 video ◀──── adb TCP forward ────▶ input events
│
Android Emulator (unmodified AOSP framework) │
MsaAgent priv-app │
VirtualDevice │
├─ VirtualDisplay → MediaCodec H.264 ──┘
├─ VirtualTouchscreen
├─ VirtualMouse
└─ VirtualKeyboard
└─ target Activity on that display
- Features
- Known Limitations
- System Requirements
- Repository Layout
- Disk Usage
- Quick Start
- Native macOS App Integration (
.app) - Google Play and GMS Setup (rootAVD)
- Verification & Diagnostics
- Troubleshooting
- Security Notes
- Contributing
- License
- Virtual Display & Device Isolation: Creates a
VirtualDeviceandVirtualDisplayper app via Android 16'sVirtualDeviceManager(1 connection → 1 VirtualDevice → 1NSWindow). - Target Activity Routing: Launches the target Activity directly on its assigned virtual display using
ActivityOptions.setLaunchDisplayId(). - Low-Latency Input Forwarding: Injects macOS mouse, touch, and keyboard input into Android's
VirtualTouchscreen,VirtualMouse, andVirtualKeyboard. - Hardware-Accelerated Streaming: Encodes video via
MediaCodec(H.264) and decodes/renders frames smoothly in AppKit. - Audio Routing: Routes Android playback and microphone input through macOS CoreAudio.
- Bidirectional Clipboard: Shares text and images bidirectionally between macOS and Android clipboards.
- Camera Bridging: Maps selectable macOS cameras to Android's front and back cameras.
- Live Window Resizing: Dynamically resizes the
VirtualDisplay, density, encoder, and touchscreen to trackNSWindowaspect ratio and size changes live. - Automated Companion Pairing: Sets up the Companion Device app-streaming association and role automatically.
- Secure Authentication Forwarding: Forwards
FLAG_SECUREauthentication screens to macOS using a secure VirtualDisplay. - Native App Bundling: Packages any installed Android app as a native, double-clickable macOS
.appbundle.
- Not Yet Implemented: System notifications, IME composition, and DRM-protected video streaming.
- Unauthenticated Wire Protocol: The wire protocol has no built-in authentication and must only run behind
adb forward's localhost tunnel (see Security Notes). - Development Key Signing: The priv-app is signed using an AOSP development test key, not a production signing key.
- Root-Induced GMS Constraints: Google Play / GMS setup requires a rooted emulator, which breaks Play Integrity, hardware DRM, and Wallet-style features (see Google Play and GMS Setup).
- Hardware: Apple Silicon Mac (M1/M2/M3/M4 series).
- Android SDK & Emulator: Android SDK with command-line tools and emulator (defaults to
~/Library/Android/sdk; override withANDROID_SDK_ROOT). - Developer Tools: Xcode Command Line Tools (providing Swift 6).
- Runtime: Java 17 (OpenJDK 17 or Android Studio bundled JDK).
-
Install command-line tools and Java:
xcode-select --install # Xcode Command Line Tools (provides Swift 6) brew install openjdk@17 # or use the JDK bundled with Android Studio
-
Install Android SDK components: Install Android SDK via Android Studio (simplest — default SDK path matches script expectations) or via command-line tools. Ensure the following packages are installed:
"$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" \ "platform-tools" "emulator" "platforms;android-36" "build-tools;36.0.0"
(Note:
scripts/install-system-image.shinstalls the actual emulator system image separately in the initial setup). -
Configure PATH: Add
platform-toolsto yourPATH(required because scripts likeforward.shandsmoke-test.shinvokeadbdirectly):echo 'export PATH="$HOME/Library/Android/sdk/platform-tools:$PATH"' >> ~/.zshrc source ~/.zshrc
| Directory / File | Description |
|---|---|
android-agent/ |
Gradle-built, platform-signed privileged app (MsaAgent). |
macos-host/ |
Swift Package / AppKit host application (no Xcode project required). |
scripts/ |
Shell scripts for SDK setup, AVD creation, priv-app installation, execution, and verification. |
docs/protocol.md |
Specification of the wire protocol between the macOS host and Android agent. |
docs/storage-resize.md |
In-depth architecture and recovery guide for data-preserving storage expansion. |
aosp/ |
Fallback configuration for building a full AOSP product; not needed for regular use. |
The primary storage footprint consists of the Android SDK's API 36 ARM64 system image, AVD image data, and Gradle build cache. Initial installation requires a few gigabytes (approximately 4 GB in testing).
Execute all commands from the repository root.
Download required prebuilts and compile both agent and host:
scripts/install-system-image.sh
scripts/fetch-android-prebuilts.sh
scripts/build-agent.sh
scripts/build-host.shNote
fetch-android-prebuilts.sh downloads only the Android 16 system API stubs and the AOSP test platform key — never a full AOSP source tree. This test key is for local development only and cannot sign production distribution builds.
-
Launch the emulator:
scripts/start-emulator.sh
This launches the emulator with
-gpu hostto utilize Apple Silicon GPU acceleration. The video stream runs at a fixed 60 fps. -
Install the privileged agent: In a separate terminal, once the guest finishes booting:
ANDROID_SERIAL=emulator-5556 scripts/install-priv-app.sh
This script runs
adb root/adb remounton theuserdebugimage, copies the APK and priv-app permission allowlist into/system/priv-app, reboots the guest, and provisions the Companion Device association, app-streaming role, and TCP port forwarding.
Establish the port forward and run the host client (e.g., launching Android Settings):
ANDROID_SERIAL=emulator-5556 scripts/forward.sh
scripts/run-host.sh com.android.settingsReplace com.android.settings with any installed Android package name. Launching multiple packages creates independent VirtualDisplays and NSWindows for each.
Convert any Android app installed on the emulator into a native macOS .app bundle launchable from Finder or Spotlight.
- Naming & Icons: Display names are resolved from the APK manifest. Icons are extracted via Android's
PackageManageras PNG and converted to.icns(supporting XML adaptive icons, vector drawables, and standard image assets). Bundles are installed into~/Applicationsby default. - Embedded Configuration: Generated bundles embed the target package name and emulator settings active for
ANDROID_SERIALat creation time (no APK or Google Play binaries are bundled). - Independent Host: The generated app contains the lightweight
MSAAppHost. Emulator VM lifecycle management is handled exclusively byMSA Emulator.app.
# Basic creation (installed to ~/Applications)
ANDROID_SERIAL=emulator-5558 scripts/create-macos-app.sh com.android.vending
# Custom display name and output directory
ANDROID_SERIAL=emulator-5558 scripts/create-macos-app.sh com.example.app "My Android App" "$HOME/Desktop"
# Update an existing bundle with fresh host binaries and icons
ANDROID_SERIAL=emulator-5558 scripts/create-macos-app.sh --replace com.android.vendingTo run the generated app:
open "$HOME/Applications/Google Play Store.app"Opening the .app instructs the manager to start the emulator headlessly if not already running, then displays the app window once boot completes.
Once the Android agent and MSA Emulator.app are active, newly installed launchable Android apps are converted automatically:
- The agent queues package installation events in device-protected storage across reboots.
- The manager converts each package, signs the
.app, places it alongside the manager app, and sends an acknowledgment (ACK). - Existing bundles for that package are replaced automatically.
MSA Emulator.app runs as a persistent menu-bar application controlling the VM lifecycle and hardware bridging:
- Emulator Lifecycle Control: Start, restart, or stop the emulator.
- Power Management: 30 seconds after the last Android app window closes, the manager pauses the VM to save CPU. After 5 minutes of inactivity, it shuts down the emulator, saving its Quick Boot state.
- Cold Boot Fallback: If booting from a saved snapshot fails, the manager automatically falls back to a cold boot.
- Location Sharing: Enabled by default (prompts for macOS Location permission on first use). Injects an initial location on boot and updates coordinates only while an active Android app declares
ACCESS_COARSE_LOCATIONorACCESS_FINE_LOCATIONin its manifest. Updates pause when the VM is paused/stopped; resuming does not unconditionally inject location. - Camera Selection: Select which macOS cameras map to Android's front and back cameras. Settings persist per emulator; modifying a selection restarts the emulator to attach the new device.
- Microphone & Audio: Android playback and microphone input route through macOS CoreAudio. Because the emulator's CoreAudio backend lacks per-process input selection, the manager sets the macOS default input device (prompting for user confirmation first). Select Refresh Media Devices after plugging/unplugging media hardware.
- Storage Expansion ("Increase Storage…"):
- Allows entering a larger data-partition maximum capacity (in GiB).
- Storage is allocated dynamically as Android writes data (thin provisioning).
- Performs verified offline AVD backup, expands QCOW2, and uses a temporary ramdisk hook before Android mounts
/data. User data, encryption keys, and UUID are preserved; snapshots are retired. Requires Android NDK and free host disk space. - See docs/storage-resize.md for full safety, backup, and recovery details.
Each app window provides an Android menu in the macOS menu bar:
- Navigation & Audio:
- Back (
Command-[) - Volume Up, Volume Down, Mute (controls Android guest volume, not macOS host volume).
- Back (
- Clipboard Paste:
Command-VsendsControl-Vinto the focused Android input field, pasting synchronized text or images from the macOS clipboard.
- Status Indicators: Displays VM states:
Idle,Paused,Saving Snapshot, andStopped.
Create or update the manager app independently:
ANDROID_SERIAL=emulator-5558 scripts/create-emulator-manager-app.sh
open "$HOME/Applications/MSA Emulator.app"ANDROID_SERIAL: Defaults to mappingemulator-5558to AVDmsa-gms-api36, and any other serial tomsa-api36.MSA_AVD_NAME: Override target AVD name.MSA_WRITABLE_SYSTEM: Toggle-writable-system(1or0).
ANDROID_SERIAL=emulator-5560 MSA_AVD_NAME=my-avd MSA_WRITABLE_SYSTEM=0 \
scripts/create-macos-app.sh com.example.appGoogle Play Store and Google Mobile Services (GMS) run in an isolated AVD named msa-gms-api36. The SDK's Google Play system image is cloned using an APFS copy-on-write duplicate; rootAVD/Magisk patches only that copy's ramdisk, leaving original system images and Pixel AVDs untouched.
-
Create and initialize the GMS AVD:
scripts/setup-gms-avd.sh GMS_EMULATOR_WINDOW=1 scripts/start-gms-emulator.sh
-
Root the emulator with rootAVD: In a separate terminal (the emulator exits automatically upon completion):
ANDROID_SERIAL=emulator-5558 scripts/root-gms-emulator.sh
-
Complete Magisk setup:
- Launch with window:
GMS_EMULATOR_WINDOW=1 scripts/start-gms-emulator.sh. - Open the Magisk app inside Android and accept the additional setup prompt.
- After the automatic reboot, navigate to Magisk's Superuser screen and enable [SharedUID] Shell.
- Launch with window:
-
Install the agent module:
ANDROID_SERIAL=emulator-5558 scripts/install-gms-agent.sh
-
Headless daily operation:
scripts/start-gms-emulator.sh ANDROID_SERIAL=emulator-5558 scripts/forward.sh scripts/run-host.sh com.android.settings scripts/run-host.sh com.android.vending
Warning
This setup is for local development only. Do not redistribute Google Play system images, GMS, Magisk, rootAVD, or the agent APK in build artifacts. Play Integrity, DRM, and Wallet features may fail on rooted emulators.
The agent uses role permission CAPTURE_SECURE_VIDEO_OUTPUT and a secure VirtualDisplay to mirror FLAG_SECURE authentication and password screens to macOS.
- A systemless Runtime Resource Overlay (RRO) transfers the
SYSTEM_AUTOMOTIVE_PROJECTIONrole holder from Android Auto toMsaAgent. Consequently, Android Auto is unusable inside the emulator while this override is active. - Video streams unauthenticated over localhost TCP. Do not run on multi-user systems where untrusted local processes could listen.
Toggle secure capture (triggers emulator reboot):
ANDROID_SERIAL=emulator-5558 scripts/set-secure-capture.sh disable
ANDROID_SERIAL=emulator-5558 scripts/set-secure-capture.sh enableTo completely roll back secure capture modifications: execute disable above, then check out commit 62be958.
Verify that virtual devices, displays, and inputs are operating properly:
# Run automated smoke test
ANDROID_SERIAL=emulator-5556 scripts/smoke-test.sh com.android.settings
# Inspect Android subsystem state
adb -s emulator-5556 shell dumpsys virtualdevice
adb -s emulator-5556 shell dumpsys display
adb -s emulator-5556 shell dumpsys input
adb -s emulator-5556 shell dumpsys activity activities
adb -s emulator-5556 logcat -s MsaAgentExpected Results:
dumpsys virtualdevicelists the active Virtual Device.dumpsys displaydisplays anMSA:<package>virtual display entry.dumpsys inputregisters Touchscreen, Mouse, and Keyboard virtual devices.dumpsys activityshows the target Activity assigned to the virtual display ID.- Closing the
NSWindowcleanly tears down the encoder, virtual inputs, display, and virtual device.
| Issue / Error Message | Cause | Resolution |
|---|---|---|
No CDM association; run scripts/provision-emulator.sh |
Agent app data was cleared or reset without reinstalling the priv-app. | Run ANDROID_SERIAL=<serial> scripts/provision-emulator.sh to recreate the Companion Device association and streaming role. |
adb: command not found (in forward.sh or smoke-test.sh) |
platform-tools directory is missing from your shell's PATH. |
Add SDK tools to PATH: export PATH="$HOME/Library/Android/sdk/platform-tools:$PATH" (see Prerequisites). |
aapt2 / apksigner / android.jar not found |
Required SDK platforms or build tools are missing. | Install them via sdkmanager: sdkmanager "platforms;android-36" "build-tools;36.0.0". Note that install-system-image.sh only installs the emulator system image. |
- Transport Authentication: The wire protocol currently has no authentication. TCP ports must never be exposed to external networks or LANs; use exclusively via
adb forwardlocalhost tunnels. - Signing Key: The priv-app is signed with an AOSP development test platform key, not a production signing key.
- Production Gaps: Authenticated IPC, verified production signing keys, secure update mechanisms, and fine-grained permission scoping are required before deployment outside trusted local development machines.
Contributions, bug reports, and pull requests are welcome! The project codebase is compact and modular:
- Android privileged agent:
android-agent/ - Swift macOS host:
macos-host/ - Wire protocol specification: docs/protocol.md
- Helper automation:
scripts/
Distributed under the MIT License. See LICENSE for details.