Android multi-display management app for Android Automotive systems. Mirrors the primary display to secondary displays, launches apps on any connected display, force-stops running apps, and provides a system-wide edge swipe gesture to open the app from anywhere.
Built and tested on a Qualcomm Gen4 (SA8775p) based Android Automotive 14 head unit (Chery/Autolink BSP) with 5 physical displays.
- App Launcher — Launch any installed app on any connected display
- Split-Screen Launch — Launch two apps side-by-side (left/right split) on any display using freeform windowing
- Screen Mirroring — Mirror Display 0 to any secondary display using MediaProjection
- Force Stop Apps — Kill running apps directly via ADB protocol with shell privileges (no root needed)
- Floating Overlay Button — Draggable circle overlay for quick access to DisplayMirror from anywhere
- Edge Swipe Gesture — Swipe from the left edge of the screen to open DisplayMirror from any app (always-on)
- Default Target Display — Set a default target display from the display list, persisted across restarts
- Auto-start on Boot — Edge swipe service and overlay button start automatically after device reboot
- Self-Update from GitHub — App checks for new versions on launch and can download & install updates directly on the device (no PC needed)
- Permissions Dashboard — View and grant required permissions from within the app, including "Install Unknown Apps" with ADB auto-grant
- Check for Updates — Manual update check button in the Permissions dialog
- Restart App — Quick restart button in the permissions dialog
Connect your device via ADB and run the install script for your platform.
curl -sL https://github.com/Baghdady92/DisplayMirror/releases/latest/download/install.sh -o install.sh
chmod +x install.sh
./install.shOpen Command Prompt (cmd) — not PowerShell — and run:
curl -sL https://github.com/Baghdady92/DisplayMirror/releases/latest/download/install.bat -o install.bat
install.batOr in PowerShell:
Invoke-WebRequest -Uri "https://github.com/Baghdady92/DisplayMirror/releases/latest/download/install.bat" -OutFile "install.bat"
.\install.bat- Download the latest APK from GitHub Releases
- Install it on the connected device
- Grant all required permissions
- Push ADB keys for force-stop & split-screen
- Launch the app
You can also specify a device serial: ./install.sh <serial> (or install.bat <serial> on Windows)
Note: Requires
adbandcurlon your computer. The Windows script also uses PowerShell (included in Windows 10+) to parse the GitHub API response. After initial setup, the app can update itself from GitHub — no PC needed.
| Display ID | Type | Resolution | Name |
|---|---|---|---|
| 0 | Internal | 2560x1440 | DSI_1 (Main) |
| 2 | External | 4032x284 | DP_2 (HDMI) |
| 3 | External | 4032x284 | DP_3 (HDMI) |
| 4 | External | 1920x480 | DP_4 (HDMI) |
| 5 | External | 2880x1620 | DP_5 (HDMI) |
- Select a target display from the dropdown (defaults to the saved target)
- Tap any app icon to launch it on the selected display via
ActivityOptions.setLaunchDisplayId() - Launched apps are tracked in the "Launched Apps" section — tap to force stop via ADB protocol
- Select "Split Left | Right" from the Launch Mode dropdown
- Tap the first app — it highlights with a teal border (this will be the left app)
- Tap the second app — both apps launch side-by-side on the target display
- Tap the same app again to deselect, or switch back to "Fullscreen" to cancel
How it works:
- Enables freeform windowing via ADB:
settings put global enable_freeform_support 1 - Launches both apps in freeform mode (
--windowingMode 5) on the target display - Queries
am stack listto find the task IDs of the newly launched apps - Resizes each task to fill exactly half the display:
am task resize <ID> <L> <T> <R> <B>
Note: Requires ADB keys to be installed (see Manual Install & Setup). The split uses Android's freeform windowing mode which may not be supported on all devices.
The app communicates with the local ADB daemon on 127.0.0.1:5555 using the ADB wire protocol to execute am force-stop with shell (uid=2000) privileges. This bypasses the limitation that normal apps cannot force-stop other apps.
How it works:
- App opens a TCP connection to the local ADB daemon (port 5555)
- Performs RSA authentication using a pre-installed ADB private key
- Opens a shell session and runs
am force-stop <package> - Closes the session
One-time setup: Push your computer's ADB key to the device (see Manual Install & Setup), or use ./install.sh which does this automatically.
The app checks GitHub Releases for new versions on every launch. When an update is available:
- An "Update Available" dialog shows the new version number
- Tap "Update" to download the APK directly to the device
- A progress dialog shows download progress
- Once downloaded, the system package installer launches automatically
The "Install Unknown Apps" permission is required and can be granted from the Permissions dialog via ADB auto-grant. You can also manually check for updates from Permissions > "Check for Updates".
- Select a target display from the mirror dropdown (Display 0 is excluded since it's always the source)
- Tap Start to begin mirroring — approve the screen capture consent dialog
- A
MirrorPresentationis created on the target display with a full-screenSurfaceView - A
VirtualDisplayfromMediaProjectionwithVIRTUAL_DISPLAY_FLAG_AUTO_MIRRORrenders Display 0's content onto the target - Tap Stop to end the session
Note: Android's MediaProjection API always captures Display 0 (the default display). Mirroring from other source displays is not supported.
- A foreground service (
SwipeService) places a narrow invisible overlay (20dp wide, center 40% of screen height) on the left edge of the screen - Swipe detection: horizontal swipe >= 80dp, vertical drift <= 60dp, duration <= 500ms
- On valid swipe: launches
MainActivitywithFLAG_ACTIVITY_REORDER_TO_FRONT - Starts automatically when the app opens and on device boot
- A draggable 56dp circle overlay (teal accent) that floats above all apps
- Tap to open DisplayMirror, drag to reposition
- Toggle on/off from the Permissions dialog
- Persists across reboots when enabled
Display 0 (Main) Target Display
┌──────────────────┐ ┌──────────────────┐
│ │ MediaProject │ │
│ Main Screen │ ──────────► │ MirrorPresent. │
│ │ VirtualDisp. │ (SurfaceView) │
│ [MainActivity] │ │ Full Screen │
└──────────────────┘ └──────────────────┘
│
├─ starts MirrorService (foreground, screen capture)
├─ starts SwipeService (foreground, edge overlay)
├─ starts OverlayButtonService (foreground, floating button)
├─ uses AdbShellHelper (ADB protocol for force-stop)
└─ uses UpdateChecker (GitHub releases API for self-update)
BootReceiver ──► SwipeService + OverlayButtonService (auto-start on boot)
- Title Row — "Display Manager" header with "by Baghdady92 · GitHub" subtitle, plus Permissions and Show All Displays buttons
- App Launcher Card — Target display selector + launch mode (Fullscreen / Split) + app grid (12 columns)
- Launched Apps Card — Running apps grid, tap to force stop
- Screen Mirror Card — Target display selector (excludes Display 0) + Start/Stop buttons + status
- Android 11+ (API 30)
- Multi-display hardware (Android Automotive or similar)
- ADB over TCP enabled (the app auto-detects the port; see Manual Install & Setup if not pre-configured)
- ADB access for initial permission and key setup
- JDK 17+ (Android Studio or Homebrew
openjdk@17) - Android SDK Platform 34
- Android Build Tools 34.0.0+
make help # Show all available commands
make build # Build debug APK
make setup # Build, install, grant permissions, push ADB keys, launch
make run # Build, install, and launch
make release # Build signed release APK
make release-github # Build and publish to GitHub Releases
make deploy # Bump version + release to GitHubexport JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home"
# or on macOS with Homebrew:
# export JAVA_HOME=/opt/homebrew/opt/openjdk@17
./gradlew assembleDebugOutput APK: app/build/outputs/apk/debug/app-debug.apk
If you prefer not to use install.sh, you can set up manually:
# Install APK
adb install -r app/build/outputs/apk/debug/app-debug.apk
# Enable plain TCP ADB (required on devices using wireless/TLS ADB)
# Skip this if the device already has persist.adb.tcp.port=5555.
adb tcpip 5555
# Grant required permissions
adb shell appops set com.example.displaymirror SYSTEM_ALERT_WINDOW allow
adb shell appops set com.example.displaymirror PROJECT_MEDIA allow
adb shell appops set com.example.displaymirror REQUEST_INSTALL_PACKAGES allow
# Push ADB keys for force-stop and split-screen functionality (one-time setup)
adb push ~/.android/adbkey /data/local/tmp/adbkey
adb push ~/.android/adbkey.pub /data/local/tmp/adbkey.pub
adb shell "run-as com.example.displaymirror mkdir -p ./files"
adb shell "run-as com.example.displaymirror cp /data/local/tmp/adbkey ./files/adbkey"
adb shell "run-as com.example.displaymirror cp /data/local/tmp/adbkey.pub ./files/adbkey.pub"
# Launch
adb shell am start -n com.example.displaymirror/.MainActivityNote: The ADB key push is a one-time setup. After this, the device can force-stop apps and launch split-screen without a PC connection — even after reboots.
Note: On devices connected via wireless ADB (TLS), the
adb tcpip 5555step enables a plain TCP listener that the app can connect to. This setting may reset on reboot — if ADB features stop working after a reboot, runadb tcpip 5555again. Devices withpersist.adb.tcp.port=5555in their build properties don't need this step.Note: The
SYSTEM_ALERT_WINDOWpermission can also be granted from within the app via the Permissions dialog ("Grant Overlay via ADB" button), as long as ADB keys are installed.
- Open the app on Display 0 (main screen)
- Set default target display: Tap "Show All Displays" and tap "Set Target" on the desired display
- Launch apps: Select a target display, tap an app icon — it opens on that display
- Split-screen: Select "Split Left | Right" from Launch Mode, tap first app (left), tap second app (right) — both launch side-by-side
- Force stop apps: Tap a running app in the "Launched Apps" section, then "Force Stop"
- Start mirroring: Select a target display in the Screen Mirror card, tap "Start", approve capture
- Edge swipe: From any app, swipe right from the left-center edge of the screen to return to DisplayMirror
- Overlay button: Enable from Permissions dialog — a floating circle appears for quick access
- Update: The app checks for updates on launch. You can also check manually from Permissions > "Check for Updates"
| Permission | Purpose |
|---|---|
FOREGROUND_SERVICE |
Required for foreground services |
FOREGROUND_SERVICE_MEDIA_PROJECTION |
Declares the service type for screen capture |
SYSTEM_ALERT_WINDOW |
Required for edge swipe overlay, floating button, and Presentation from Service |
RECEIVE_BOOT_COMPLETED |
Auto-start services after reboot |
QUERY_ALL_PACKAGES |
List all installed apps for the app launcher |
KILL_BACKGROUND_PROCESSES |
Force stop launched apps |
REQUEST_INSTALL_PACKAGES |
Self-update: install downloaded APK updates |
INTERNET |
Self-update: check GitHub releases and download APK |
WRITE_EXTERNAL_STORAGE |
Write files to storage |
READ_EXTERNAL_STORAGE |
Read files from storage |
Use the Permissions button in the app to check which permissions are granted and grant missing ones via ADB.
| Decision | Reason |
|---|---|
targetSdk 33 |
Avoids Android 14's strict FOREGROUND_SERVICE_MEDIA_PROJECTION permission check which requires system-level CAPTURE_VIDEO_OUTPUT on non-rooted devices |
launchMode="singleTask" |
Prevents duplicate Activity instances when re-opening from launcher or swipe gesture |
VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR |
Mirrors the default display content to the VirtualDisplay |
| VirtualDisplay uses target display resolution | Ensures edge-to-edge rendering without black bars |
FLAG_LAYOUT_NO_LIMITS + immersive sticky |
Forces the Presentation to cover the entire display including system bar areas |
Presentation API (not WindowManager) |
Standard Android approach for rendering on secondary displays |
Static isRunning() flags |
Allows Activity to sync UI state with services after recreation |
| Mirror dropdown excludes Display 0 | Prevents crash from showing Presentation on the default display |
START_STICKY for SwipeService |
System restarts the service if killed |
| SharedPreferences for defaults | Persists target display selection and overlay button state across app restarts |
ADB am task resize for split-screen |
am start --launch-bounds is not available on Android Automotive 14; instead launch in freeform mode then resize the task by ID |
| Auto-detect ADB port | Different devices use different ports (5555, 55556, etc.); the app probes system properties and common ports at runtime |
| Grant overlay via ADB in-app | Permissions dialog can self-grant SYSTEM_ALERT_WINDOW via the ADB client, avoiding the need for manual adb shell commands |
| Direct ADB protocol (not daemon) | Each force-stop opens a fresh ADB session — no background process to keep alive |
| RSA key in app internal storage | Survives reinstalls, not accessible by other apps, avoids scoped storage restrictions |
| Self-update via GitHub API | No extra dependencies — uses HttpURLConnection + org.json (built into Android); FileProvider serves APK to system installer |
| Release APK signed with debug key | Allows seamless self-update without signature mismatch on development devices |
adb logcat -s MirrorService AndroidRuntimeCommon causes:
- Missing
PROJECT_MEDIAappop — run:adb shell appops set com.example.displaymirror PROJECT_MEDIA allow - Missing
SYSTEM_ALERT_WINDOW— run:adb shell appops set com.example.displaymirror SYSTEM_ALERT_WINDOW allow
Check that ADB keys are installed:
adb shell "run-as com.example.displaymirror ls files/adbkey"Check ADB over TCP is enabled:
adb shell getprop persist.adb.tcp.port
# Should return a port number (e.g. 5555)
# If empty, enable it with:
adb tcpip 5555Check which port the app detected:
adb logcat -s AdbShellHelper | grep "port"Check logs:
adb logcat -s AdbShellHelperCheck that the install permission is granted:
adb shell appops get com.example.displaymirror REQUEST_INSTALL_PACKAGES
# Should return "allow"
# If not, grant it:
adb shell appops set com.example.displaymirror REQUEST_INSTALL_PACKAGES allowCheck update logs:
adb logcat -s UpdateCheckerVerify the display is connected and active:
adb shell dumpsys display | grep "Display Id="Check that the overlay permission is granted:
adb shell appops get com.example.displaymirror SYSTEM_ALERT_WINDOWCheck that SwipeService is running:
adb shell dumpsys activity services com.example.displaymirror/.SwipeServiceEnsure the VirtualDisplay is created with the target display's resolution. Check MirrorService.onSurfaceReady().
Force stop and relaunch:
adb shell am force-stop com.example.displaymirror
adb shell am start -n com.example.displaymirror/.MainActivityDisplayMirror/
├── build.gradle # Project-level Gradle config
├── Makefile # Build, install, release automation
├── install.sh # One-command install script (macOS/Linux)
├── install.bat # One-command install script (Windows)
├── settings.gradle
├── gradle.properties
├── gradle/wrapper/
│ └── gradle-wrapper.properties # Gradle 8.10.2
└── app/
├── build.gradle # App-level config (compileSdk 34, targetSdk 33)
└── src/main/
├── AndroidManifest.xml
├── java/com/example/displaymirror/
│ ├── MainActivity.java # Main UI — app launcher, mirror controls, permissions, update check
│ ├── UpdateChecker.java # GitHub releases API client for self-update
│ ├── MirrorService.java # Foreground service managing MediaProjection
│ ├── MirrorPresentation.java # Full-screen Presentation on target display
│ ├── SwipeService.java # Foreground service with edge swipe overlay
│ ├── OverlayButtonService.java # Floating draggable overlay button service
│ ├── BootReceiver.java # Starts services on boot
│ ├── AdbShellHelper.java # ADB protocol client for shell commands
│ ├── PermissionManager.java # Permission querying and management helper
│ ├── CloseActivity.java # Helper to force-stop apps
│ ├── AppInfo.java # Data class for installed apps
│ ├── AppListAdapter.java # GridView adapter for app icons
│ ├── RunningAppInfo.java # Data class for launched apps
│ ├── RunningAppAdapter.java # GridView adapter for running apps
│ └── ExpandableGridView.java # Custom GridView that expands to fit content
└── res/
├── layout/
│ ├── activity_main.xml
│ ├── presentation_mirror.xml
│ ├── item_app.xml
│ └── item_running_app.xml
├── drawable/
│ ├── btn_start.xml
│ ├── btn_stop.xml
│ ├── card_bg.xml
│ ├── overlay_button.xml
│ └── spinner_bg.xml
├── xml/
│ └── file_paths.xml # FileProvider paths for APK install
└── values/
├── colors.xml
├── strings.xml
└── styles.xml
Internal use.