• Sep 17, 2026
  • 8 min read

Building a Native Android DJI Tello Controller with Kotlin

I wanted the useful parts of my desktop DJI Tello controller on the device that is normally beside me when I fly: an Android phone. The result is a fully native Kotlin application with touch controls, telemetry, live video, photo capture, recording, and signed updates delivered outside the Play Store.

The application has now been built locally, released through GitHub Actions, installed with Obtainium, and tested on a Pixel 9 Pro with the real drone. This article covers the engineering decisions that mattered and the boundaries of what a desktop test can prove.

The source and APK releases are available in the tello-kotlin repository.

What I kept—and what I deliberately removed

The starting point was a working Rust desktop controller. That prototype had already answered the difficult product questions: which controls felt useful, how to display telemetry, and how command and video traffic behaved.

The Android version retained the core flying experience:

  • Connection to the drone and entry into SDK command mode
  • Touch controls for translation, altitude, and yaw
  • Live battery and flight telemetry
  • Live H.264 video
  • Takeoff, landing, emergency stop, speed modes, and flips
  • Photo capture and MP4 recording

I did not port face detection or route planning. I also avoided Tauri, a WebView, an embedded web frontend, and FFmpeg. Narrowing the scope kept the first mobile release focused on manual control and video—the parts I would actually use.

The native Android stack

The application uses Kotlin and Jetpack Compose for the interface, coroutines for asynchronous loops, Android networking APIs for the drone connection, MediaCodec for hardware H.264 decoding, and MediaStore for photos and recordings.

Jetpack Compose UI
       │ control state and telemetry
       ▼
Kotlin controller + coroutines
       │
       ├── UDP commands/responses ── 192.168.10.1:8889
       ├── UDP telemetry ◀────────── port 8890
       └── UDP H.264 video ◀──────── port 11111
                                      │
                                      ▼
                             MediaCodec → Surface

Kotlin Android builds still require a JDK because Gradle, the Kotlin compiler, and the Android Gradle Plugin run on the JVM. JDK 17 is part of the build toolchain; it does not turn the shipped application into a desktop Java app or make the UI less native.

Talking directly to the Tello over UDP

The Tello exposes a small text-command SDK on its own Wi-Fi network. Commands and replies use UDP port 8889, state telemetry arrives on 8890, and the video stream arrives on 11111.

That sounds simple, but mobile routing introduces a complication. A Tello Wi-Fi network usually has no internet access, so Android may prefer mobile data or another validated network. Opening an ordinary socket and hoping Android sends it through Wi-Fi is not sufficiently deterministic for flight control.

The controller therefore finds the TELLO-* Wi-Fi network and binds its drone sockets to that specific Android Network. The binding belongs on the sockets used for the drone; globally forcing every process request onto that network would unnecessarily affect unrelated traffic.

The initial handshake is the Tello command instruction. Once command mode is confirmed, independent coroutine loops handle command replies, telemetry, video, and periodic RC updates.

Continuous controls need a state loop

A touch joystick should not emit one movement command only when a finger first lands. It represents current intent while the finger remains down. The app stores that intent as control state and sends RC values at 20 Hz.

touch input → desired axes → safety normalization → 20 Hz RC packet

This design also gives safety rules one central place to operate:

  • Opposing directions cancel instead of fighting each other.
  • Slow and fast modes constrain the output range.
  • Acceleration avoids an unnecessarily sharp first movement.
  • A stale-input watchdog clears control values that are no longer refreshed.
  • Disconnecting or backgrounding triggers conservative landing behavior.
  • Takeoff is blocked when reported battery is below the configured threshold.

Drone-control software is physical software. A stale button is not merely an annoying UI state; it can keep an aircraft moving. That changes how seemingly small lifecycle and input decisions should be reviewed.

Decoding the native H.264 stream

The Tello sends a raw H.264 Annex-B byte stream over UDP. UDP packet boundaries are not video-frame boundaries, so packets cannot simply be passed one by one to a decoder.

The application accumulates bytes, identifies Annex-B start codes, reconstructs NAL units, and queues valid units into Android’s H.264 MediaCodec. The decoder renders directly to a Surface beneath the Compose controls.

UDP datagrams
      ↓
byte accumulator
      ↓ detect 00 00 01 / 00 00 00 01
H.264 NAL units
      ↓
MediaCodec input buffers
      ↓
hardware-decoded Surface

This avoided packaging a general-purpose FFmpeg runtime. MediaCodec provides the native hardware path already present on Android, although decoder behavior still has to be tested on real devices.

Photo capture and recording use Android’s media APIs. Files are written through MediaStore, so they appear in ordinary gallery locations without requesting broad filesystem access.

A landscape-first Compose interface

Video is the primary surface, not a decorative panel. The UI therefore uses a landscape-first layout with the live stream behind two touch-control regions. Telemetry and high-value actions remain visible without covering the centre of the image.

Compose worked well for deriving visuals from controller state: connected, command-ready, airborne, recording, speed mode, and current telemetry. The network and decoder code remain outside composables, while lifecycle-aware state connects them to the screen.

That separation made pure logic independently testable. Telemetry parsing, Annex-B framing, acceleration, opposing inputs, and the stale-input watchdog all have JVM unit tests that do not need an Android phone or drone.

What I could test on the PC

The local validation command was intentionally broader than “the APK builds”:

./gradlew testDebugUnitTest lintDebug assembleDebug

It checks Kotlin compilation, unit tests, Android lint, and debug packaging. A separate release build was signed with the permanent production key and checked with apksigner.

That gives good evidence for parsing, control math, lifecycle logic, manifest configuration, signing, and packaging. It cannot prove:

  • Android will route real packets correctly on the Tello network.
  • UDP timing will remain stable on a particular phone.
  • Its H.264 decoder accepts the live stream.
  • The touch directions match real physical movement.
  • Landing and failsafe behavior work under flight conditions.

An emulator is useful for an ordinary application, but it cannot emulate the entire radio, decoder, touch, and aircraft system.

The real-device safety ladder

I installed the production-signed build on a Pixel 9 Pro and validated it with the real Tello. I did not begin with aggressive flight commands. A safer order is:

  1. Verify the APK signature and visible version.
  2. Connect while the drone remains landed.
  3. Confirm command replies and telemetry.
  4. Confirm the video stream and decoder stability.
  5. Check STOP and landing controls before takeoff.
  6. Test basic movement at low altitude in a clear area.
  7. Leave flips and fast mode until the basic axes are proven.

The app worked successfully on the Pixel, including live control and video. That is meaningful evidence for this device, not a universal Android hardware certification.

Distribution was part of the product

A useful personal app still needs a repeatable update path. Each release is signed with one permanent Android key, built by GitHub Actions, attached to a public GitHub Release, and discovered by Obtainium.

The first successful public release installed normally. The next release made only a low-risk visible version change. Obtainium detected it, Android accepted it over the existing installation, and the new version appeared in the UI. That small test proved five systems together: package ID, signing identity, monotonic version code, release discovery, and in-place installation.

Lessons I would reuse

The most reusable lesson was to port behavior, not framework structure. The desktop application was evidence about protocol and product behavior; it was not a template that Android needed to imitate internally.

Other decisions paid off quickly:

  • Reduce the first scope to the features that define the product.
  • Bind local-device sockets explicitly on multi-network mobile systems.
  • Put continuous control behind a periodic state loop and watchdog.
  • Prefer platform media primitives when they cover the required codec.
  • Keep pure protocol and control logic outside the UI and unit-test it.
  • Treat physical-device testing as a distinct stage, not the last build step.
  • Prove the update channel with a harmless visible change before relying on it.
  • Back up the Android signing key before publishing the first permanent APK.

The result is not a universal drone platform. It is a deliberately small, native controller that does the important jobs well—and can now be maintained and updated like a real Android application.

Frequently asked questions

Does the DJI Tello work with Android without the DJI Mobile SDK?

Yes. The Tello SDK exposes command, telemetry, and video endpoints over its local Wi-Fi network. This app communicates with those endpoints directly.

Why not use a WebView or cross-platform framework?

The project needed precise network routing, low-latency touch state, lifecycle handling, and native video decoding. A Kotlin/Compose implementation provides direct access to those Android primitives without an extra UI bridge.

Can a PC completely test an Android Tello controller?

No. It can validate compilation, unit logic, lint, APK packaging, and signing. The Wi-Fi path, hardware H.264 decoder, touch interaction, and physical flight behavior require a real Android device and drone.

Is the APK available without Google Play?

Yes. Signed APKs are published on GitHub Releases and can be installed and updated through Obtainium. Android still verifies that every update carries the same signing identity.