Skip to content

Repository files navigation

Droid Runtime

Droid Runtime is a portable Android host for packaged HTML, CSS, and JavaScript applications. Apps run from isolated WebView origins, can call Android features through a promise-based native bridge, and can optionally run an embedded Node.js entry point.

This repository is currently an MVP. Its npm workspaces are private development packages, so build the Android host from source and invoke the dependency-free CLI directly from the repository.

Install

Droid Runtime targets Android 7.0 or newer (API 24+).

Windows

From the repository root, run:

.\install-windows.cmd

The installer locates or installs JDK 17, uses an existing Node.js/npm installation or installs Node.js LTS, and installs the Android command-line tools, SDK Platform 35, Build Tools 35.0.0, NDK 25.2.9519653, CMake 3.22.1, and Gradle 8.9. If it needs to install JDK or Node.js, winget must be available.

The completed build is written to:

dist/droid-runtime-debug.apk
dist/device-lab.droid
dist/node-webserver.droid

Connect an authorized Android device or start an emulator, then install the host:

adb devices
adb install -r .\dist\droid-runtime-debug.apk

If adb was installed by the script and is not visible in the current shell, open a new terminal. Useful installer options are:

Option Effect
-SdkRoot <path> Use a specific Android SDK directory.
-WithNodeMobile Download the pinned Node-Mobile build and embed Node.js.
-NodeMobileTag <tag> With -WithNodeMobile, override the default tag, v18.20.4.
-SkipToolInstall Require JDK, Node.js, and Android command-line tools to exist already.
-SkipLicenses Skip Android SDK license acceptance.
-SkipBuild Install or verify the toolchain without building artifacts.

For example, to choose the SDK location and enable Node.js:

.\install-windows.cmd -SdkRoot F:\droidsdk -WithNodeMobile

Manual build

Install the toolchain versions listed above, including Gradle 8.9 or newer. Point ANDROID_HOME or ANDROID_SDK_ROOT at the SDK, or set sdk.dir in a root local.properties file. The checked-in gradlew scripts are launch shims for a system Gradle installation, not a self-contained Gradle wrapper.

Open the repository root in Android Studio, or build and install from a terminal:

./gradlew :android:app:assembleDebug
adb install -r android/app/build/outputs/apk/debug/app-debug.apk

On Windows, use .\gradlew.bat instead of ./gradlew.

The default build uses a native Node stub: WebView apps and the native API work, while calls to node.call() reject with NODE_UNAVAILABLE. To enable embedded Node.js on macOS or Linux, first install Node.js, curl, and unzip, then run:

bash scripts/fetch-node-mobile.sh
./gradlew :android:app:assembleDebug -PDROID_NODE_MOBILE=true

The pinned Node-Mobile release is v18.20.4 and supports armeabi-v7a, arm64-v8a, and x86_64 host builds. On Windows, .\install-windows.cmd -WithNodeMobile is the recommended equivalent.

Quickstart

After installing the debug host and connecting one authorized ADB device, validate and launch the included Hello Droid app from the repository root:

node packages/cli/bin/droid.mjs validate templates/hello-world
node packages/cli/bin/droid.mjs run templates/hello-world

run validates and packs the project, deploys the temporary .droid bundle through ADB, and launches it in Droid Runtime. No npm install is required.

To start your own app, copy the template and give it a globally unique id in app.json:

Copy-Item -Recurse templates/hello-world my-app
node packages/cli/bin/droid.mjs dev my-app

On macOS or Linux, use cp -R templates/hello-world my-app. The dev command performs the initial launch, watches for changes, redeploys the bundle, and streams filtered runtime logs. Stop it with Ctrl+C.

Every app contains an app.json and a WebView entry. A Node entry is optional:

my-app/
|-- app.json
|-- www/
|   |-- index.html
|   `-- main.js
`-- node/
    `-- main.js

A minimal manifest looks like this:

{
  "id": "com.example.my-app",
  "name": "My App",
  "version": "0.1.0",
  "web": { "entry": "www/index.html" },
  "permissions": [],
  "runtime": { "minVersion": "0.1.0" }
}

Add "node": { "entry": "node/main.js" } only when the app needs Node.js and will run on a Node-enabled host. Supported permission names are camera, location, sensors, vibration, clipboard, storage, share, notifications, and intents.

API

The trusted app origin receives window.native and window.node; remote or externally navigated pages do not. Bridge methods are asynchronous and return Promises. native.runtime.version and native.runtime.apiVersion are the two synchronous string properties.

Declare each permission-gated capability your app uses in app.json. Camera, location, and notifications can additionally require an Android runtime permission prompt:

const state = await native.permissions.request('camera');
if (state === 'granted') {
  const photo = await native.camera.takePhoto();
  console.log(photo.uri);
}

Native modules

API Required manifest permission Description
native.runtime.info() - Returns { version, apiVersion, nodeAvailable, nodeReady }.
native.app.metadata() - Returns normalized metadata for the current app manifest.
native.app.state() - Returns { foreground }.
native.app.restart(), native.app.exit() - Restarts or closes the current app session.
native.device.info() - Returns device, Android, ABI, battery, and screen information.
native.permissions.get(name), request(name), all() request names must be declared Returns granted, denied, or blocked, or a map of all states.
native.files.paths() - Returns the app-specific data and cache roots.
native.files.readText(), writeText(), exists(), delete(), copy(), move(), list() - Reads and changes files below the app's isolated data or cache root.
native.storage.get(), set(), remove(), clear() - Stores JSON-compatible values by key for the current app.
native.camera.takePhoto() camera Opens the system camera and resolves to an object containing uri.
native.location.getCurrent(), requestUpdates(), stopUpdates() location Reads the latest location or controls location updates.
native.vibration.vibrate(ms), haptic() vibration Triggers a bounded vibration or short haptic pulse.
native.clipboard.getText(), setText(text) clipboard Reads or writes plain text.
native.share.open(options) share Opens Android sharing for { text, title, file, mimeType }. native.share(options) is shorthand.
native.notifications.notify(options) notifications Posts { title, body, id } through the app's notification channel.
native.intents.openUrl(), dial(), email(), maps(), settings() intents Opens controlled Android intents.

File operations use these signatures: readText(pathOrOptions) (alias read), writeText(path, text) or writeText(options) (alias write), and exists(pathOrOptions), delete(pathOrOptions), or list(pathOrOptions). Paths are relative and default to the persistent data scope. Pass an object to select data or cache explicitly:

await native.files.writeText({
  path: 'notes/welcome.txt',
  text: 'Hello from Droid Runtime',
  scope: 'data'
});

const text = await native.files.readText('notes/welcome.txt');
const names = await native.files.list('notes');

copy and move accept positional from, to paths or an object with from, to, and optional scope, fromScope, and toScope fields. Absolute paths, backslashes, and paths that escape the selected root are rejected.

Intent arguments are:

await native.intents.openUrl('https://example.com');
await native.intents.dial('+31123456789');
await native.intents.email({ to: 'hello@example.com', subject: 'Hello', body: 'Hi' });
await native.intents.maps({ latitude: 52.3676, longitude: 4.9041 });
await native.intents.settings();

Events

Lifecycle, location, and sensor listeners use on(event, handler) and off(event, handler). Remove a listener with the same function reference used to add it:

const onPause = ({ foreground }) => console.log('foreground:', foreground);
await native.lifecycle.on('pause', onPause);
await native.lifecycle.off('pause', onPause);

const onMotion = ({ x, y, z, timestamp }) => console.log(x, y, z, timestamp);
await native.sensors.accelerometer.on('change', onMotion);
await native.sensors.accelerometer.off('change', onMotion);

Available event sources are:

  • native.lifecycle: pause, resume
  • native.location: change
  • native.sensors.accelerometer: change
  • native.sensors.gyroscope: change
  • native.sensors.orientation: change

Declare location or sensors as appropriate. Before subscribing to location changes, call native.permissions.request('location') and confirm that it returns granted. Location payloads contain latitude, longitude, accuracy, altitude, speed, bearing, and timestamp; sensor payloads contain x, y, z, and timestamp.

Embedded Node.js

A Node-enabled host injects globalThis.droid into the configured Node entry. Register a handler in node/main.js:

globalThis.droid.expose('hello', async args => ({
  message: `Hello, ${args.name}`,
  dataDir: process.env.APP_DATA
}));

Call it from the WebView:

const result = await node.call('hello', { name: 'Droid' });
console.log(result.message);

Arguments and return values must be JSON-compatible. Node receives APP_ROOT, APP_DATA, and APP_CACHE environment variables. Calls made without a Node entry, without Node-Mobile in the host, or before Node has finished starting reject with a structured error.

Errors

Rejected bridge calls are Error objects with code, module, method, and optional details fields:

try {
  await native.camera.takePhoto();
} catch (error) {
  console.error(error.code, error.module, error.method, error.details);
}

Common codes include PERMISSION_DECLARED_REQUIRED, PERMISSION_DENIED, PERMISSION_REQUEST_IN_PROGRESS, INVALID_ARGUMENT, PATH_TRAVERSAL, FILE_NOT_FOUND, NODE_UNAVAILABLE, NODE_NOT_READY, and ACTIVITY_RESULT_CANCELLED.

CLI reference

Run the CLI directly with Node.js:

Command Description
node packages/cli/bin/droid.mjs validate <project> Validate app.json, entry points, paths, and permissions.
node packages/cli/bin/droid.mjs pack <project> [--out file] Create a .droid bundle.
node packages/cli/bin/droid.mjs run <project> Pack, deploy through ADB, and launch once.
node packages/cli/bin/droid.mjs dev <project> Launch, watch, redeploy, and stream runtime logcat output.

run and dev require the debug host APK to be installed and an authorized device or emulator to be connected. ADB is resolved from DROID_ADB, ANDROID_HOME, ANDROID_SDK_ROOT, or PATH. DROID_RUNTIME_PACKAGE can override the deployment package for a custom host whose launcher remains <package>/.MainActivity.

For a shorter local command, optionally run npm link inside packages/cli, then use droid validate, droid pack, droid run, and droid dev with the same arguments.

Examples and internals

  • examples/device-lab exercises the native APIs, permissions, storage, sensors, and Node bridge.
  • examples/node-webserver runs a real http.createServer() listener inside embedded Node.js and requires a Node-enabled host.
  • docs/architecture.md describes trust boundaries, request flow, application isolation, and the Node adapter.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages