Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9ce4f38
feat: add device.webview.* method stubs
gmegidish May 11, 2026
8dabdb1
feat: align webview methods with trimmed OpenRPC spec
gmegidish May 12, 2026
3945a67
feat: add webview CLI commands
gmegidish May 12, 2026
36729b8
feat: implement WebViewListCommand for Android
gmegidish May 12, 2026
cef8b30
fix: remove hardcoded DEX_PATH from jvmti_agent.c
gmegidish May 12, 2026
559d39d
feat: implement webview goto, evaluate (url + title) for Android
gmegidish May 12, 2026
a6e7d1e
fix: webview url/title/eval returning empty result
gmegidish May 12, 2026
37687ee
feat: implement webview back, forward, content
gmegidish May 12, 2026
d8109a6
fix: ensure bare expressions are wrapped with return in WebViewEvaluate
gmegidish May 12, 2026
46f452c
feat: implement webview reload
gmegidish May 12, 2026
e6a0c33
fix: handle null evalJs result and ClassCastException in evaluateExpr…
gmegidish May 12, 2026
98d936d
build: wire agents/android into root Makefile
gmegidish May 12, 2026
7808755
feat: implement webview waitForLoadState
gmegidish May 12, 2026
31386a7
refactor: replace map[string]any with typed result structs
gmegidish May 12, 2026
4abe35b
chore: add agent sources, Makefile, and adb client
gmegidish May 12, 2026
f42b53c
chore: remove built agent binaries from git, add to gitignore
gmegidish May 12, 2026
581330f
fix: cross-platform agent Makefile and CI agent build job
gmegidish May 12, 2026
b1cfde6
feat: iOS simulator webview agent (list, url, goto)
gmegidish May 12, 2026
c6d1eea
no dylib in git
gmegidish May 12, 2026
49d63ee
feat: complete iOS simulator webview support + WebViewable interface
gmegidish May 12, 2026
c74d7b7
feat: iOS real device webview list (inject via lldb, forward via go-ios)
gmegidish May 12, 2026
c0e2d01
fix: iOS 26 SDK compat for real device webview injection via LLDB
gmegidish May 13, 2026
0391621
performance
gmegidish May 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
.PHONY: all build test test-cover lint fmt clean
.PHONY: all build agents test test-cover lint fmt clean

all: build

build:
agents:
$(MAKE) -C agents/android all

build: agents
go mod tidy
CGO_ENABLED=0 go build -ldflags="-s -w"

build-cover:
build-cover: agents
go mod tidy
CGO_ENABLED=0 go build -ldflags="-s -w" -cover

Expand All @@ -31,4 +34,5 @@ fmt:
$(shell go env GOPATH)/bin/goimports -w .

clean:
$(MAKE) -C agents/android clean
rm -f mobilecli coverage.out coverage.html
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ A universal command-line tool for managing iOS and Android devices, simulators,
- **Device Control**: Reboot devices, tap screen coordinates, press hardware buttons
- **App Management**: Launch, terminate, install, uninstall, list, and get foreground apps
- **Crash Reports**: List and fetch crash reports from iOS and Android devices
- **Webview Inspection**: List, navigate, query DOM, and evaluate JavaScript in embedded webviews

### 🎯 Platform Support

Expand Down Expand Up @@ -255,6 +256,66 @@ Example output for `agent status`:
}
```

### Webview Inspection 🌐

Inspect and interact with embedded webviews (`WKWebView` on iOS, `android.webkit.WebView` on Android) running inside native apps.

```bash
# List embedded webviews in the foreground app
mobilecli webview list --device <device-id>

# Navigate a webview to a URL
mobilecli webview goto <id> https://example.com --device <device-id>

# Reload, go back or forward
mobilecli webview reload <id> --device <device-id>
mobilecli webview back <id> --device <device-id>
mobilecli webview forward <id> --device <device-id>

# Get current URL and page title
mobilecli webview url <id> --device <device-id>
mobilecli webview title <id> --device <device-id>

# Dump the full HTML content of the page
mobilecli webview content <id> --device <device-id>

# Query DOM elements by CSS selector
mobilecli webview query <id> "button" --device <device-id>
mobilecli webview query <id> "[data-testid='submit']" --device <device-id>

# Evaluate arbitrary JavaScript
mobilecli webview eval <id> "document.querySelectorAll('a').length" --device <device-id>

# Wait for the page to finish loading
mobilecli webview wait <id> --state load --device <device-id>
mobilecli webview wait <id> --state domcontentloaded --timeout 5000 --device <device-id>
```

Example output for `webview list`:
```json
{
"status": "ok",
"data": [
{
"id": "1",
"url": "https://example.com",
"title": "Example Domain"
}
]
}
```

Example output for `webview query <id> "button"`:
```json
{
"status": "ok",
"data": [
{ "tag": "button", "text": "Sign In", "id": "login-btn", "class": "btn-primary", "value": null, "href": null },
{ "tag": "button", "text": "Cancel", "id": null, "class": "btn-secondary", "value": null, "href": null }
]
}
```

### Crash Reports 💥

```bash
Expand Down
9 changes: 9 additions & 0 deletions agents/agents.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package agents

import _ "embed"

//go:embed android/devicekit.so
var AndroidDevicekitSO []byte

//go:embed android/devicekit.dex
var AndroidDevicekitDEX []byte
Comment on lines +8 to +9
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether embed targets exist under agents/
fd -i '^devicekit\.(so|dex)$' agents
fd -i '^android$' agents -t d --exec ls -la

Repository: mobile-next/mobilecli

Length of output: 1958


🏁 Script executed:

#!/bin/bash
# Check if android/devicekit.dex exists in agents/ directory
find . -name "devicekit.dex" 2>/dev/null
find . -name "devicekit.so" 2>/dev/null
find . -type d -name "android" 2>/dev/null

Repository: mobile-next/mobilecli

Length of output: 82


🏁 Script executed:

#!/bin/bash
# List contents of agents/android directory
ls -la agents/android/
# Also check for devicekit files anywhere in the agents directory
find agents/ -type f -name "devicekit*"

Repository: mobile-next/mobilecli

Length of output: 239


🏁 Script executed:

#!/bin/bash
# Read agents/agents.go to see the embed directives
cat -n agents/agents.go | head -20

Repository: mobile-next/mobilecli

Length of output: 289


Missing embed targets cause compile failure

Both android/devicekit.so (line 5) and android/devicekit.dex (line 8) are missing from the repository. The //go:embed directives will fail at build time until these artifacts are added.

🧰 Tools
🪛 GitHub Check: test

[failure] 8-8:
pattern android/devicekit.dex: no matching files found

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agents/agents.go` around lines 8 - 9, The build is failing because the
//go:embed directives reference missing files; update the embed usage for the
variables AndroidDevicekitSO and AndroidDevicekitDEX by either (A) adding the
actual artifacts android/devicekit.so and android/devicekit.dex into the repo at
those paths, or (B) removing or guarding the //go:embed lines and associated
variables (AndroidDevicekitSO, AndroidDevicekitDEX) behind an appropriate build
tag or feature flag so the embed is not evaluated when the files are absent;
choose one approach and adjust the agents.go file accordingly so the embed
target names and variables match actual files or are not compiled.

28 changes: 28 additions & 0 deletions agents/android/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
NDK := $(HOME)/Library/Android/sdk/ndk/29.0.13113456
TOOLCHAIN := $(NDK)/toolchains/llvm/prebuilt/darwin-x86_64/bin
CC_ANDROID := $(TOOLCHAIN)/aarch64-linux-android26-clang
JDK_INC := $(shell brew --prefix openjdk@21)/include
SYSROOT := $(NDK)/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/include
JAVAC := $(shell brew --prefix openjdk@21)/bin/javac
D8 := $(HOME)/Library/Android/sdk/build-tools/36.0.0/d8
ANDROID_JAR := $(HOME)/Library/Android/sdk/platforms/android-35/android.jar

JAVA_SRCS := $(wildcard java/*.java)

all: devicekit.so devicekit.dex

devicekit.so: jvmti_agent.c
$(CC_ANDROID) -shared -fPIC -O2 \
-I$(SYSROOT) -I$(JDK_INC) -I$(JDK_INC)/darwin \
-o $@ $< -llog

devicekit.dex: $(JAVA_SRCS)
mkdir -p .dex_build
$(JAVAC) --release 8 -cp $(ANDROID_JAR) -d .dex_build $(JAVA_SRCS)
$(D8) --min-api 26 --output .dex_build $$(find .dex_build -name "*.class")
mv .dex_build/classes.dex devicekit.dex
rm -rf .dex_build

clean:
rm -f devicekit.so devicekit.dex
rm -rf .dex_build
Binary file added agents/android/devicekit.dex
Binary file not shown.
Binary file added agents/android/devicekit.so
Binary file not shown.
107 changes: 107 additions & 0 deletions agents/android/java/AndroidBridge.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.mobilenext.mobilecli;

import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebView;

import java.lang.reflect.Field;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
import java.util.stream.Stream;

class AndroidBridge {

static Handler sMainHandler;

private static String sPackageName;
private static Field sMViewsField;
private static Object sWmgInstance;

static void init() {
sMainHandler = new Handler(Looper.getMainLooper());
}

@SuppressWarnings("unchecked")
static List<View> getRootViews() throws Exception {
if (sMViewsField == null) {
Class<?> wmgClass = Class.forName("android.view.WindowManagerGlobal");
sMViewsField = wmgClass.getDeclaredField("mViews");
sMViewsField.setAccessible(true);
sWmgInstance = wmgClass.getMethod("getInstance").invoke(null);
}
return (List<View>) sMViewsField.get(sWmgInstance);
}

static String getPackageName() {
if (sPackageName != null) {
return sPackageName;
}
try {
byte[] buf = new byte[256];
java.io.FileInputStream fis = new java.io.FileInputStream("/proc/self/cmdline");
int len = fis.read(buf);
fis.close();
int end = 0;
while (end < len && buf[end] != 0 && buf[end] != ':') {
end++;
}
sPackageName = new String(buf, 0, end).trim();
return sPackageName;
} catch (Exception e) {
return "unknown";
}
}

static Stream<WebView> streamWebViews(View view) {
if (view instanceof WebView) {
return Stream.of((WebView) view);
} else if (view instanceof ViewGroup) {
ViewGroup vg = (ViewGroup) view;
return IntStream.range(0, vg.getChildCount())
.mapToObj(vg::getChildAt)
.flatMap(AndroidBridge::streamWebViews);
}
return Stream.empty();
}

@SuppressWarnings("unchecked")
static <T> T runOnMainThread(Callable<T> task) throws Exception {
Object[] result = {null};
Exception[] err = {null};
CountDownLatch latch = new CountDownLatch(1);
sMainHandler.post(() -> {
try {
result[0] = task.call();
} catch (Exception e) {
err[0] = e;
} finally {
latch.countDown();
}
});
if (!latch.await(5, TimeUnit.SECONDS)) {
throw new Exception("timed out");
}
if (err[0] != null) {
throw err[0];
}
return (T) result[0];
}

static String evalJs(WebView wv, String script) throws Exception {
String[] result = {null};
CountDownLatch latch = new CountDownLatch(1);
sMainHandler.post(() -> wv.evaluateJavascript(script, value -> {
result[0] = value;
latch.countDown();
}));
if (!latch.await(10, TimeUnit.SECONDS)) {
throw new Exception("evaluateJavascript timed out");
}
return result[0];
}
}
67 changes: 67 additions & 0 deletions agents/android/java/HttpRpcServer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.mobilenext.mobilecli;

import android.net.LocalServerSocket;
import android.net.LocalSocket;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;

class HttpRpcServer {

static void start() {
String name = "mobilecli." + AndroidBridge.getPackageName();
new Thread(() -> {
try {
LocalServerSocket server = new LocalServerSocket(name);
android.util.Log.d("MobileCliAgent", "listening on localabstract:" + name);
while (true) {
handleClient(server.accept());
}
} catch (Exception e) {
android.util.Log.e("MobileCliAgent", "server error: " + e.getMessage());
}
}, "vta-server").start();
}

private static void handleClient(LocalSocket client) {
new Thread(() -> {
try (
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
PrintWriter out = new PrintWriter(client.getOutputStream(), false)
) {
String requestLine = in.readLine();
if (requestLine == null) {
return;
}

int contentLength = 0;
String header;
while ((header = in.readLine()) != null && !header.isEmpty()) {
if (header.toLowerCase().startsWith("content-length:")) {
contentLength = Integer.parseInt(header.substring(15).trim());
}
}

char[] body = new char[contentLength];
if (contentLength > 0) {
in.read(body, 0, contentLength);
}
String bodyStr = new String(body).trim();

String response = JsonRpcDispatcher.dispatch(bodyStr.isEmpty() ? "{}" : bodyStr);

byte[] bytes = response.getBytes("UTF-8");
out.print("HTTP/1.1 200 OK\r\n");
out.print("Content-Type: application/json\r\n");
out.print("Content-Length: " + bytes.length + "\r\n");
out.print("Connection: close\r\n");
out.print("\r\n");
out.print(response);
out.flush();
} catch (Exception e) {
android.util.Log.e("MobileCliAgent", "client error: " + e.getMessage());
}
}, "vta-client").start();
}
}
Loading
Loading