The Java client
It speaks to the same tool layer as the CLI and the MCP server, by spawning
mobium pipe, so the mobium binary has to be on PATH or named by
MOBIUM_BIN_PATH. The quick start goes from nothing to a
running script.
Install
git clone https://github.com/mobiumdev/mobium.git
cd mobium/clients/java && ./mvnw -q install -DskipTests
Connect
try (Mobium device = Mobium.builder().platform("android").app("com.android.settings").start()) {
for (Element el : device.map()) System.out.println(el.ref() + " " + el.label());
}
device below is a dev.mobium.Mobium from Mobium.builder()...start() or Mobium.connect().
Every tool, by method
Connecting, ending, and errors
builder
public static Builder builder()
Configures a connection or a session.
@return a builder, which connects or starts when asked
Configure, then start or connect
try (Mobium phone = Mobium.builder().platform("android").app("com.example.shop").start()) {
phone.tap("text=Sign in");
}
start
public Mobium start()
Connects and opens the session on the device: the device-side server is started now and, given an
app, it is launched and in front when this returns.
Nothing requires it -- every call opens a session on first use --
but it puts the slow first start (installing UiAutomator2, building
WebDriverAgent on an iPhone) where it was asked for. End it with
Mobium#quit(), or in try-with-resources, which quits:
try (Mobium device = Mobium.builder().platform("android").app("com.android.settings").start()) {
device.map();
}
}```
@return a device with its session open
**Open a session and launch the app fresh**
```java
try (Mobium phone = Mobium.builder().platform("ios").app("com.example.shop").start()) {
Session s = phone.session();
System.out.println(s.device() + " " + s.driver());
} // try-with-resources quits: the session ends here
connect
public static Mobium connect()
public Mobium connect()
Connects to mobium, for whichever device is running. It does not touch
the device: the session there opens on the first call that needs it, or
with Builder#start().
@return a connection; close it when done
Connect without touching the device
try (Mobium phone = Mobium.connect()) {
System.out.println(phone.current());
} // close() leaves the session open for whoever started it
Connect with options
try (Mobium phone = Mobium.builder().device("emulator-5554").connect()) {
phone.map();
}
platform
public Builder platform(String name)
The platform for start(): "android" or "ios".
"ios" picks the wda driver, so none need be named.
@param name "android" or "ios"
@return this builder
Pick iOS when both are attached
try (Mobium phone = Mobium.builder().platform("ios").start()) {
phone.launch("com.apple.Preferences");
}
app
public Builder app(String id)
An app for start() to launch once the session is up.
@param id a package name (Android) or bundle id (iOS) @return this builder
Launch an app as the session starts
try (Mobium phone = Mobium.builder().app("com.android.settings").start()) {
phone.waitFor("text=Network & internet");
}
binary
public Builder binary(String path)
Pins the mobium executable, ahead of MOBIUM_BIN_PATH and PATH.
@param path the mobium executable @return this builder
Use a mobium binary that is not on PATH
try (Mobium phone = Mobium.builder().binary("/opt/mobium/bin/mobium").connect()) {
System.out.println(phone.doctor());
}
device
public Builder device(String serial)
Targets one device by serial or UDID. Omit when only one is running.
@param serial a serial (Android) or UDID (iOS) @return this builder
Target one of several devices
try (Mobium phone = Mobium.builder().device("emulator-5556").start()) {
phone.press("home");
}
driver
public Builder driver(String name)
Chooses the driver: uiautomator2 (default on Android),
uiautomator (installs nothing, slower, cannot type) or
wda (iOS simulators and iPhones).
@param name "uiautomator2", "uiautomator" or "wda"
@return this builder
Use the zero-install Android driver
try (Mobium phone = Mobium.builder().driver("uiautomator").connect()) {
System.out.println(phone.text());
}
callTimeout
public Builder callTimeout(Duration timeout)
The longest any one call may take before the connection is given up, the handshake included. Unlimited by default, because the first session on an iPhone builds WebDriverAgent and that takes minutes.
A call that runs out ends the connection: there is one
pipe, replies are told apart only by id, and the late answer to an
abandoned call would be read as the answer to the next one. Every
call after throws, saying so; connect again. The device's session
lives in the daemon, so reconnecting is cheap. Set it well above the
longest WaitFor#timeout you use.
@param timeout how long, positive @return this builder
Bound every call
try (Mobium phone = Mobium.builder().callTimeout(Duration.ofMinutes(2)).connect()) {
phone.map(); // a call past two minutes ends the connection
}
session
public Builder session(String name)
public Session session()
A daemon of this connection's own, by name, as MOBIUM_SESSION
sets one. One daemon serves one call at a time across every device,
so parallel runs on different devices should each name one:
sharing, an emulator's calls waited behind a simulator's, 22.3s
against 0.3s. Keep it short; it is part of a socket path.
@param name the daemon's name @return this builder
Name a session of your own
try (Mobium phone = Mobium.builder().session("checkout").connect()) {
phone.map();
}
Read what start() opened
try (Mobium phone = Mobium.builder().start()) {
Session s = phone.session(); // null from connect()
System.out.println(s.platform() + " " + s.device());
}
quit
public void quit()
Ends the session on the device and closes the connection. The teardown is the daemon's own: accessibility settings put
back, a recording or route stopped, WebViews detached, the device-side
server stopped, and the app start() launched, if any, stopped
too. Quitting a session that is not open succeeds, and a second quit --
say, try-with-resources closing after an explicit one -- does nothing.
A program that exits without quitting or closing has the sessions it
started ended for it: mobium sees the client go.
End the session explicitly
Mobium phone = Mobium.builder().app("com.example.shop").start();
try {
phone.tap("text=Sign in");
} finally {
phone.quit(); // puts back what the session changed
}
close
public void close()
public void close()
Asks mobium to exit and waits for it, killing it after ten seconds. Safe to call more than once, and from another thread while a call is waiting: that call then fails, saying the connection was closed.
Close a connection, leaving the session open
Mobium phone = Mobium.connect();
phone.tap("@e2");
phone.close(); // safe to call twice
sessions
public List<Map<String, Object>> sessions()
The sessions open on the daemon.
@return each with device, platform and driver
List the sessions open on the daemon
for (Map<String, Object> s : device.sessions()) {
System.out.println(s.get("device") + " " + s.get("platform"));
}
call
public Map<String, Object> call(String tool, Map<String, Object> arguments)
Runs any tool by name, for anything this class does not wrap yet, and returns its structured answer.
@param tool the tool's name, such as "app_map"
@param arguments the tool's arguments, as its schema names them
@return the tool's structured answer
Call any tool by name
Map<String, Object> result = device.call("app_tap", Map.of("target", "@e2", "fingers", 2));
step
public static Map<String, Object> step(String tool, Map<String, Object> arguments)
One step for batch(List): a tool and the arguments it takes on
its own.
@param tool the tool, e.g. "app_tap"
@param arguments its arguments, or null for none
@return the step
Build steps for a batch
var steps = List.of(
Mobium.step("app_launch", Map.of("app", "com.android.settings")),
Mobium.step("app_map", null));
device.batch(steps);
MobiumException
public class MobiumException extends RuntimeException
public MobiumException(String message)
public MobiumException(String message, String tool)
A tool reported that it could not do what was asked, or the connection to Mobium failed.
Every tool failure carries a stable code() code, the same in
every client and on the wire (the error codes in the mobium
repository's docs/guides/cli.md), and each code has a subclass — NoSuchElementException,
UnsupportedException and so on — so a test catches the kind it can
handle and lets the rest through. remedy() says what to do about
it, retryable() whether the same call can succeed if made again.
A code this client does not know, a daemon too old to send one, and a
failure of the connection itself are plain MobiumExceptions.
Unchecked on purpose. Every call in this client can fail — a device can be unplugged mid-flow — and forcing a try/catch around each one would make a readable test unreadable without making it safer.
Read what failed and what to do
try {
device.tap("text=Continue");
} catch (MobiumException e) {
System.err.println(e.tool() + " " + e.code() + ": " + e.remedy());
if (!e.retryable()) throw e;
}
details
public Map<String, Object> details()
Machine-readable facts: the locator, the W3C code a device server sent.
@return the facts, never null
Find the step a batch stopped at
try {
device.batch(List.of(Mobium.step("app_tap", Map.of("target", "text=Sign in"))));
} catch (MobiumException e) {
System.err.println("stopped at step " + e.details().get("step"));
}
NoSuchElementException
public final class NoSuchElementException extends MobiumException
A locator or ref matched nothing on screen. Worth scrolling for.
Shares its simple name with java.util.NoSuchElementException;
import this one by name rather than through a wildcard.
Error code no_such_element. See MobiumException.
Scroll when a tap finds nothing
try {
device.tap("text=Continue");
} catch (dev.mobium.NoSuchElementException e) { // not java.util's
device.scrollTo("text=Continue", "down");
}
TimedOutException
public final class TimedOutException extends MobiumException
A wait ran out. Named TimedOut so it does not collide with
java.util.concurrent.TimeoutException.
Error code timeout. See MobiumException.
Handle a wait that never held
try {
device.waitFor("text=Welcome", WaitFor.visible().timeout(Duration.ofSeconds(3)));
} catch (TimedOutException e) {
System.err.println(e.getMessage()); // says what was on screen instead
}
AmbiguousLocatorException
public final class AmbiguousLocatorException extends MobiumException
A locator matched more than one element. Narrow it; Mobium never guesses.
Error code ambiguous_locator. See MobiumException.
Narrow a locator that matched twice
try {
device.tap("text=OK");
} catch (AmbiguousLocatorException e) {
device.tap(device.find("text=OK").get(0).ref());
}
UnsupportedException
public final class UnsupportedException extends MobiumException
This driver or platform cannot do it, and says why. Retrying cannot help.
Error code unsupported. See MobiumException.
Skip what a platform lacks
try {
device.press("back");
} catch (UnsupportedException e) { // iOS has no back button
System.err.println(e.remedy());
}
DeviceNotReadyException
public final class DeviceNotReadyException extends MobiumException
The device is there and cannot be driven yet: locked, not trusted, Developer Mode off.
Error code device_not_ready. See MobiumException.
Unlock before launching
try {
device.launch("com.example.shop");
} catch (DeviceNotReadyException e) {
device.screenLocked(false);
device.launch("com.example.shop");
}
NoDeviceException
public final class NoDeviceException extends MobiumException
Nothing to drive: no device matches, or none is connected.
Error code no_device. See MobiumException.
Report that nothing is attached
try {
device.map();
} catch (NoDeviceException e) {
System.err.println("start an emulator first: " + e.remedy());
}
NoSuchAlertException
public final class NoSuchAlertException extends MobiumException
A dialog was expected and none is on screen.
Error code no_such_alert. See MobiumException.
Answer a dialog only if one is up
try {
device.answerAlert(false);
} catch (NoSuchAlertException e) {
// nothing was up
}
NoSuchContextException
public final class NoSuchContextException extends MobiumException
A WebView context that is not there.
Error code no_such_context. See MobiumException.
Switch to a WebView that may not be there
try {
device.context("WEBVIEW_com.example.shop");
} catch (NoSuchContextException e) {
System.err.println(device.contexts());
}
ElementNotReachableException
public final class ElementNotReachableException extends MobiumException
The element was found and cannot be touched where it is, usually off screen.
Error code element_not_reachable. See MobiumException.
Handle a target that cannot be reached
try {
device.tap("text=Delete");
} catch (ElementNotReachableException e) {
System.err.println(e.remedy());
}
NotConfirmedException
public final class NotConfirmedException extends MobiumException
The command reported success and reading the state back disagreed.
Error code not_confirmed. See MobiumException.
Handle a change the device did not confirm
try {
device.timezone("Asia/Tokyo");
} catch (NotConfirmedException e) {
System.err.println(e.getMessage());
}
InvalidArgumentException
public final class InvalidArgumentException extends MobiumException
The request itself is wrong.
Error code invalid_argument. See MobiumException.
A null is refused before anything is sent
try {
device.tap(null);
} catch (InvalidArgumentException e) {
System.err.println(e.getMessage()); // names the argument
}
ToolchainMissingException
public final class ToolchainMissingException extends MobiumException
Something on this machine is missing: adb, Xcode, a signing certificate.
Error code toolchain_missing. See MobiumException.
Report a missing toolchain
try {
device.launch("com.example.shop");
} catch (ToolchainMissingException e) {
System.err.println(e.remedy());
}
DeviceServerException
public final class DeviceServerException extends MobiumException
The device side failed (a device server, or adb, simctl, devicectl, lockdown); a server's W3C code is details().get("w3c").
Error code device_server. See MobiumException.
Retry once when the device server says so
try {
device.tap("@e2");
} catch (DeviceServerException e) {
if (e.retryable()) device.tap("@e2");
}
InternalException
public final class InternalException extends MobiumException
A bug in Mobium.
Error code internal. See MobiumException.
Report an internal failure
try {
device.map();
} catch (InternalException e) {
System.err.println("mobium bug: " + e.getMessage());
}
visible
public static WaitFor visible()
Wait for the element to be on screen. The default.
@return the condition
Wait for an element to appear
Element e = device.waitFor("text=Sign in", WaitFor.visible());
hidden
public static WaitFor hidden()
Wait for it to go away — a spinner, say.
@return the condition
Wait for a spinner to go
device.waitFor("role=progressbar", WaitFor.hidden());
exactText
public static WaitFor exactText(String expected)
Wait until its text is exactly this, where text waits for a part.
@param expected the whole text @return the condition
Wait for an exact text
device.waitFor("testid=total", WaitFor.exactText("$12.00"));
value
public static WaitFor value(String expected)
Wait until a field holds exactly this value; "" waits for it to
be empty. A password field is refused, since its value is never read.
@param expected the whole value the field must hold @return the condition
Wait for a field's value
device.waitFor("testid=email", WaitFor.value("hello@example.com"));
enabled
public static WaitFor enabled()
Wait for a control to be enabled — a Submit the app enables once a form is valid.
@return the condition
Wait for a button to become tappable
device.waitFor("text=Continue", WaitFor.enabled());
disabled
public static WaitFor disabled()
Wait for a control to be disabled.
@return the condition
Wait for a button to be disabled
device.waitFor("text=Submit", WaitFor.disabled());
checked
public static WaitFor checked()
Wait for a checkbox, radio or switch to be checked.
@return the condition
Wait for a checkbox to be checked
device.waitFor("testid=termsCheck", WaitFor.checked());
unchecked
public static WaitFor unchecked()
Wait for a checkbox, radio or switch to be unchecked.
@return the condition
Wait for a switch to be off
device.waitFor("@e4", WaitFor.unchecked());
focused
public static WaitFor focused()
Wait for a field to have keyboard focus.
@return the condition
Wait for a field to take focus
device.waitFor("testid=search", WaitFor.focused());
count
public static WaitFor count(int n)
Wait until the locator matches exactly this many elements on screen.
@param n how many, 0 or more @return the condition
Wait for exactly three rows
device.waitFor("role=cell", WaitFor.count(3));
not
public WaitFor not()
The opposite of this condition: text("Sending").not() waits for the
text to change.
@return a copy of this condition, negated
Wait for a text to stop matching
device.waitFor("testid=status", WaitFor.text("Loading").not());
timeout
public WaitFor timeout(Duration d)
How long before giving up. Ten seconds by default, two minutes at most.
@param d how long, at most two minutes @return a copy of this condition with the timeout set
Wait longer than ten seconds
device.waitFor("text=Done", WaitFor.visible().timeout(Duration.ofSeconds(30)));
centerX
public int centerX()
The x coordinate a tap targets.
@return the horizontal center
Tap the middle of an element's bounds
Bounds b = device.find("testid=map").get(0).bounds();
device.tap(b.centerX(), b.centerY());
width
public int width()
How wide it is.
@return the width, in device pixels
Measure an element
Bounds b = device.waitFor("testid=banner").bounds();
System.out.println(b.width() + "x" + b.height() + " px");
Element
public record Element(String ref, String label, String role, String locator, Bounds bounds, String context, Boolean checked, boolean selected, String value, boolean disabled)
One actionable thing on screen.
@param ref the @e1 handle, valid only for the screen it came from
@param label what a person would call it
@param role button, input, list and so on; empty if Mobium could not say
@param locator how the ref resolves on a later screen, as kind=value
@param bounds where it is, in device pixels
@param context the WebView it came from, empty for native elements
@param checked a checkbox, radio or switch's state; null for anything with
no such state, which is a different answer from unchecked
@param selected true for what the platform reports chosen: the current tab,
the chosen segment of a segmented control
@param value what a slider reads, as the app states it ("80%", "1.2");
empty for anything else
@param disabled true for what the platform reports not enabled: an action
on it waits for it to be enabled, and is refused if it stays
disabled
Read what map hands out
Element e = device.find("text=Sign in").get(0);
System.out.println(e.ref() + " " + e.role() + " " + e.bounds() + " checked=" + e.checked());
Location
public final class Location
Where a device believes it is.
mock and mocking answer different questions and disagree
after a clear: the first says this fix was injected, the second says a test
provider is installed now. Android keeps its last known position after the
provider supplying it is removed, so a read straight after clearing reports
an injected fix from a provider that no longer exists.
known is false when the platform cannot report a position at all,
as on iOS, which is different from the device having none.
Read where the device believes it is
Location here = device.location();
if (here.mock) System.out.println(here.latitude + ", " + here.longitude);
Types
| Type | What it is |
|---|---|
AmbiguousLocatorException | A locator matched more than one element. |
App | One installed app. |
Bounds | An on-screen rectangle in device pixels, on both platforms. |
DeviceInfo | One attached device or simulator. |
DeviceNotReadyException | The device is there and cannot be driven yet: locked, not trusted, Developer Mode off. |
DeviceServerException | The device side failed (a device server, or adb, simctl, devicectl, lockdown); a server's W3C code is details().get("w3c"). |
Element | One actionable thing on screen. |
ElementNotReachableException | The element was found and cannot be touched where it is, usually off screen. |
InternalException | A bug in Mobium. |
InvalidArgumentException | The request itself is wrong. |
Location | Where a device believes it is. |
Mobium | Drives mobile apps on Android emulators, Android phones, iOS simulators and iPhones. |
Builder | Options for Mobium#connect() and Builder#start(). |
MobiumException | A tool reported that it could not do what was asked, or the connection to Mobium failed. |
NoDeviceException | Nothing to drive: no device matches, or none is connected. |
NoSuchAlertException | A dialog was expected and none is on screen. |
NoSuchContextException | A WebView context that is not there. |
NoSuchElementException | A locator or ref matched nothing on screen. |
NotConfirmedException | The command reported success and reading the state back disagreed. |
Session | What Mobium.Builder#start() opened: the device and how it is driven. |
TimedOutException | A wait ran out. |
ToolchainMissingException | Something on this machine is missing: adb, Xcode, a signing certificate. |
UnsupportedException | This driver or platform cannot do it, and says why. |
WaitFor | What to wait for, and how long. |