Installing a mobile operating system on a single-board computer bridges the gap between rapid UI development and bare-metal hardware control. If you want to install Android OS on Raspberry Pi for an embedded kiosk, digital signage, or IoT dashboard, the standard Raspberry Pi OS won't cut it. You need a build with hardware-accelerated graphics and touch support. The most reliable path in 2026 is flashing Konstakang’s LineageOS builds (Android 14/15), which provide full V3D GPU acceleration and DSI touchscreen support.
However, native Android GPIO support (the deprecated Android Things) is dead. To interface with physical sensors, the professional approach is to bridge a microcontroller like an ESP32 to the Pi via the 40-pin header's UART. Below is the complete bench-tested procedure for building an Android-Pi environmental kiosk, including the exact UART pinouts, Kotlin serial integration, and the specific boot errors you will encounter.
Hardware Matrix and Required Parts
Not all Raspberry Pi boards handle Android equally. The GPU memory allocation and DRM/KMS (Direct Rendering Manager / Kernel Mode Setting) drivers vary wildly between generations. Here is the compatibility matrix for current LineageOS builds.
| Board Variant | RAM | Recommended Android Build | GPU Acceleration | Max Stable Resolution |
|---|---|---|---|---|
| Raspberry Pi 5 | 8GB | LineageOS 21 (Android 14) | Hardware (V3D) | 4K @ 60Hz |
| Raspberry Pi 4 Model B | 4GB | LineageOS 20 (Android 13) | Hardware (V3D) | 1080p @ 60Hz |
| Raspberry Pi 3 Model B+ | 1GB | LineageOS 18.1 (Android 11) | Software / Limited | 1080p @ 30Hz |
| Compute Module 4 | 4GB+ | LineageOS 20 (Custom DTBO) | Hardware (V3D) | Varies by carrier board |
Exact Parts List for the Kiosk Build
The code and pinouts in this guide target the Raspberry Pi 5 (8GB variant). Do not substitute the power supply; the Pi 5 will throttle USB and GPIO peripherals if it does not negotiate a 27W USB-C PD contract.
- SBC: Raspberry Pi 5 (8GB) — ~$80
- Power: Official Raspberry Pi 27W USB-C PD Power Supply — $12
- Thermal: Official Pi 5 Active Cooler — $5
- Display: Waveshare 7-inch DSI Touchscreen (800x480) — $45
- Sensor Bridge: ESP32-WROOM-32 DevKit v1 — $6
- Storage: Samsung EVO Plus 64GB microSD (A2 rated for random I/O) — $10
Flashing LineageOS and UART Pin Mapping
Download the LineageOS 21 `.img` file for the Pi 5 from the Konstakang archive. Use the official Raspberry Pi Imager to flash it to your A2-rated microSD card. Do not use Etcher; the Pi Imager correctly handles the FAT32 boot partition formatting required for the Pi's bootloader.
Before booting, mount the `boot` partition on your PC and edit the `config.txt` file. You must disable Bluetooth to free up the hardware PL011 UART for the GPIO header, and force the DSI display initialization.
# Add to the bottom of /boot/config.txt
dtoverlay=disable-bt
dtoverlay=vc4-kms-v3d
ignore_lcd=0
dtparam=audio=on
UART Pin Mapping Table
The Pi 5 operates at 3.3V logic, and the ESP32-WROOM-32 also operates at 3.3V logic. You can wire them directly without a logic level shifter, provided you do not accidentally connect the ESP32's 5V (VIN) pin to the Pi's GPIO.
| Pi 5 GPIO (40-Pin Header) | Function | ESP32 DevKit v1 Pin | Wire Color |
|---|---|---|---|
| Pin 8 (GPIO 14 / TXD) | Transmit Data | GPIO 16 (RX2) | Yellow |
| Pin 10 (GPIO 15 / RXD) | Receive Data | GPIO 17 (TX2) | Orange |
| Pin 6 | Ground | GND | Black |
/dev/ttyAMA0 only when the disable-bt overlay is active. If you skip the config.txt edit, the OS will route UART to the mini-UART (/dev/ttyS0), which lacks a stable baud rate clock and will drop sensor packets at 115200 baud.
Kotlin Serial Integration for the Pi 5
Because standard Android restricts direct access to `/dev/` nodes, we use the android-serialport-api library (requires root, which LineageOS supports via the su binary). Add implementation 'com.github.licheedev:Android-SerialPort-API:2.0.0' to your app's build.gradle.
The following Kotlin class targets the Pi 5's PL011 UART, handling the input stream via Coroutines and parsing JSON payloads sent by the ESP32.
package com.electricalflux.pi5kiosk
import android.serialport.SerialPort
import kotlinx.coroutines.*
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.security.AccessControlException
/**
* Manages UART communication on Raspberry Pi 5 (LineageOS 21).
* Pin Mapping: Pi GPIO 14 (TX) -> ESP32 RX | Pi GPIO 15 (RX) -> ESP32 TX
* Target Device: /dev/ttyAMA0 @ 115200 baud
*/
class Pi5UartManager(
private val scope: CoroutineScope,
private val onDataReceived: (String) -> Unit
) {
// Pi 5 PL011 UART path (requires disable-bt in config.txt)
private val uartFile = File("/dev/ttyAMA0")
private val baudRate = 115200
private var serialPort: SerialPort? = null
private var inputStream: InputStream? = null
private var readJob: Job? = null
fun connect() {
try {
// Requires 'su' binary and SELinux permissive or proper policy
serialPort = SerialPort(uartFile, baudRate, 0)
inputStream = serialPort!!.inputStream
startReading()
} catch (e: SecurityException) {
// Catch SELinux DAC denials
throw IOException("SELinux blocked /dev/ttyAMA0. Run 'setenforce 0' via ADB root.", e)
} catch (e: IOException) {
throw IOException("Failed to open UART port. Check config.txt disable-bt overlay.", e)
}
}
private fun startReading() {
readJob = scope.launch(Dispatchers.IO) {
val buffer = ByteArray(1024)
try {
while (isActive) {
val available = inputStream?.available() ?: 0
if (available > 0) {
val size = inputStream!!.read(buffer)
if (size > 0) {
val payload = String(buffer, 0, size).trim()
// ESP32 sends JSON like: {"temp":22.5,"hum":45}
withContext(Dispatchers.Main) {
onDataReceived(payload)
}
}
} else {
delay(50) // Prevent CPU spinning on Pi 5
}
}
} catch (e: IOException) {
// Handle physical disconnects or UART resets
cancel("UART stream interrupted", e)
}
}
}
fun disconnect() {
readJob?.cancel()
try {
inputStream?.close()
serialPort?.close()
} catch (e: IOException) {
// Safe to ignore on teardown
}
}
}
Debugging Bootloops and Permission Errors
Embedded Android is notoriously fragile when interacting with hardware. When your kiosk fails to boot or the app crashes on serial connect, check these exact error strings in your adb logcat output.
Ranked Error Causes and Fixes
1. Error String: "java.io.IOException: Permission denied" when calling SerialPort().
Cause: SELinux is enforcing and blocking the untrusted app domain from accessing the character device.
Fix: Connect via ADB, escalate to root (adb root), and temporarily set SELinux to permissive: adb shell setenforce 0. For production, you must compile a custom SELinux .te policy allowing untrusted_app to rw_chr_file on ttyAMA_device.
2. Error String: "SurfaceFlinger: Failed to allocate display" followed by a black screen or bootloop.
Cause: The DRM/KMS overlay failed to initialize the DSI touchscreen, usually because the ribbon cable is seated backwards or ignore_lcd=1 is lingering in the config.
Fix: Verify ignore_lcd=0 in /boot/config.txt. Reseat the DSI ribbon cable, ensuring the metal contacts face the correct direction (towards the board edge on the Pi 5).
3. Error String: "adb: device unauthorized. Please check the confirmation dialog on your device."
Cause: The Pi's Android OS has not accepted your PC's RSA key, common on headless setups where you cannot tap "Allow" on the touchscreen.
Fix: Boot the Pi with a USB mouse plugged in. Use the mouse cursor to click "Always allow from this computer" on the RSA prompt, or push your PC's adbkey.pub directly to /data/misc/adb/adb_keys via a root shell.
- Power Delivery Contract: Use a multimeter or USB-C PD sniffer to verify the Pi 5 is pulling 5V at 5A (27W). If it defaults to 5V/3A, the OS will hard-disable the USB ports and throttle the CPU.
- UART Muxing: Run
cat /proc/tty/driver/serialvia ADB. If/dev/ttyAMA0is missing, yourdisable-btoverlay failed to apply. - SELinux State: Run
getenforce. If it returnsEnforcing, your serial code will always throw a permission exception until you patch the policy.
Simplifying for Kiosks vs. Extending for IoT
Writing a custom Android app just to display sensor data on a screen is often overkill for a production deployment. You have two distinct paths depending on your project scope.
How to Simplify the Build
If your goal is strictly digital signage or a web-based dashboard, abandon the custom Kotlin serial code. Instead, install Fully Kiosk Browser (available via sideloaded APK). Fully Kiosk allows you to lock the Pi into a single-URL web view, disable the status bar, and automatically wake the screen on motion. You can push sensor data to a lightweight Node-RED dashboard hosted on a local server, letting the Pi act purely as a dumb display terminal rather than a serial-processing hub.
How to Extend the Build
If you need to integrate the Pi into a larger industrial or smart-home network, extend the ESP32's role. Instead of just passing raw UART strings to the Pi, program the ESP32 to connect to local WiFi and publish the BME280 sensor readings to an MQTT broker (like Mosquitto running on a Home Assistant server). The Pi's Android app can then subscribe to the MQTT topic using the Eclipse Paho Android library. This decouples the hardware layer from the UI layer, meaning you can swap the Pi for a cheaper Android tablet later without rewiring the UART pins.
For deeper hardware integration, consult the official Raspberry Pi 5 hardware documentation regarding the PCIe 2.0 lane on the board. If your Android kiosk requires NVMe storage for heavy local caching (e.g., offline video playback), you can bypass the microSD bottleneck entirely by wiring an M.2 HAT to the Pi 5's PCIe connector, though this requires editing the bootloader EEPROM configuration to set BOOT_ORDER=0xf416.






