# Open App Package over ADB in Python, Android Device Info

> Open an app by package name over ADB, check app state, read Android device info, log to the MAS run console and send webhook events from a Python macro.

Source: https://automationmacro.com/docs/sdk/apps-and-device (Python SDK, updated 2026-09-04)

This page covers the functions a Macro Automation Studio (MAS) macro uses around its main loop. You open an app by package name from Python, over adb on an emulator, check its state, read Android device info, write to the run console, read the clipboard, and tell the outside world that something happened. All of them are top-level functions on `mas`. Full signatures are on the [API reference](/docs/api-reference). If you describe the task to [MAS Agent](/agent), it writes this code for you.

## App management: open an app package over ADB from Python

Android apps are addressed by package name, such as `com.android.chrome` or `com.android.settings`. To find a package name, open the app on the device and call `get_current_app()`.

### open_app and close_app

[`open_app(package_name, timeout_ms=2000)`](/docs/api-reference#open_app) launches an app and waits up to `timeout_ms` for it to come up. Heavy games need more than the default two seconds; give them 5000 or more, then wait for a known template with `find_object_retry` before you tap anything. [`close_app(package_name)`](/docs/api-reference#close_app) force-stops the app. That clears its state and is the reliable way to reset an app that has hung.

### get_app_state

[`get_app_state(package_name)`](/docs/api-reference#get_app_state) returns an integer. Compare it with the constants exported by `mas` rather than with literal numbers.

| Constant | Value | Meaning |
|---|---|---|
| `mas.RUNNING_IN_FOREGROUND` | 4 | the app is on screen |
| `mas.RUNNING_IN_BACKGROUND` | 3 | running, but another app is in front |
| `mas.RUNNING_IN_BACKGROUND_SUSPENDED` | 2 | in the background and suspended |
| `mas.NOT_RUNNING` | 1 | installed but not running |
| `mas.NOT_INSTALLED` | 0 | not installed on this device |

### is_app_focused and get_current_app

[`is_app_focused(package_name)`](/docs/api-reference#is_app_focused) asks the window manager whether the package has input focus and returns a bool. [`get_current_app()`](/docs/api-reference#get_current_app) returns the package name of the foreground app. Use them after `open_app` to wait until the app is in front, and inside long loops to notice when a pop-up or another app has taken over.

```python
import time
import mas

mas.open_app("com.android.chrome", timeout_ms=5000)
for _ in range(20):
    if mas.is_app_focused("com.android.chrome"):
        break
    time.sleep(0.5)
mas.log(f"foreground app: {mas.get_current_app()}")
```

## Android device info from Python

- [`get_screen_size()`](/docs/api-reference#get_screen_size) returns a `ScreenSize` with `width` and `height` in pixels. Use it to derive positions instead of hard-coding them; see [Interaction](/docs/sdk/interaction).
- [`get_device_info()`](/docs/api-reference#get_device_info) returns a `DeviceInfo` with `id`, `name`, `type` (`"android"` or `"desktop"`), `screen_width`, `screen_height` and `connected`.
- [`get_host_machine_id()`](/docs/api-reference#get_host_machine_id) returns the persistent UUID of the computer running MAS. It is generated once, survives restarts, and is shared by every script on that machine. Storage keys depend on it, so the call raises `RPCError` rather than returning an empty string when the host has no ID.

```python
import mas

info = mas.get_device_info()
size = mas.get_screen_size()
mas.log(f"{info.name} ({info.type}) {size.width}x{size.height}")
```

## get_clipboard

[`get_clipboard()`](/docs/api-reference#get_clipboard) returns the device clipboard as a string. Copying is app-specific: usually a long press on the text, then a tap on the app's copy control. Read the result straight afterwards.

```python
import mas

mas.click(300, 500, delay_ms=1500)     # select the text
mas.click(400, 300)                    # tap the app's Copy control
code = mas.get_clipboard().strip()
mas.log(f"copied: {code}")
```

## log

[`log(message, level=LogLevel.INFO)`](/docs/api-reference#log) writes a line to the run console in the Code Editor, tagged with a level, and mirrors it to the app's own log. `level` is a `LogLevel` member or one of the strings `"debug"`, `"info"`, `"warning"` and `"error"`. Unlike `print`, the line is leveled, so warnings and errors stand out in the console. No device is required, so `log` also works in the parts of a script that run before the first device call.

```python
import mas
from mas import LogLevel

mas.log("run started")
mas.log("retries running low", level="warning")
mas.log("step failed", level=LogLevel.ERROR)
```

## webhook

[`webhook(event, data=None)`](/docs/api-reference#webhook) sends a custom event to the webhook endpoints you configured on the **Webhooks** page. Use it for things only your script knows: a level reached, a captcha on screen, an inventory full. You write no HTTP code, hold no signing secret and implement no retries. The event leaves from MAS's servers, not from the device, so it is signed, retried and recorded in the delivery log next to `macro.started` and `macro.failed`.

- `event` is your name for the event: letters, numbers, dots, dashes and underscores, 64 characters or fewer. It is namespaced under `custom.`, so `"level.reached"` arrives as `custom.level.reached`. A script can never impersonate a lifecycle event.
- `data` is an optional JSON-serializable dictionary of up to 50 keys. The run and the device are attached for you as `execution_id` and `device_name`; you cannot set them yourself.
- The return value is a dictionary with `event_id` (the receiver's dedupe key), `event_type` (the namespaced name) and `queued` (how many endpoints it went to). A `queued` of 0 is not an error; it means no endpoint is subscribed to this event. If the event cannot be queued at all, the call raises instead of failing silently.

Subscribe an endpoint to `custom.*`, to `*`, or to the exact name such as `custom.level.reached`. Payload format, signature verification and the retry schedule are on [Webhooks](/docs/webhooks).

```python
import mas

result = mas.webhook("level.reached", {"level": 40, "account": "alt-3"})
mas.log(f"{result['event_type']} sent to {result['queued']} endpoint(s)")
```

## Example: restart the app if it crashed

Check the state and the focus, force-stop and relaunch when either is wrong, then wait for a template that proves the app is usable.

```python
import mas

PACKAGE = "com.example.game"
images = mas.images({"lobby": 501})

def ensure_game_running() -> bool:
    state = mas.get_app_state(PACKAGE)
    if state == mas.NOT_INSTALLED:
        mas.log(f"{PACKAGE} is not installed", level="error")
        return False
    if state != mas.RUNNING_IN_FOREGROUND or not mas.is_app_focused(PACKAGE):
        mas.log("game is not in front, restarting", level="warning")
        mas.close_app(PACKAGE)
        mas.open_app(PACKAGE, timeout_ms=8000)
    lobby = mas.find_object_retry(images.lobby, total_tries=10, time_sleep=3.0)
    if lobby is None:
        mas.log("lobby never appeared after restart", level="error")
        return False
    return True

if not ensure_game_running():
    raise SystemExit(1)
```

Call it at the top of the run and again whenever a template search keeps failing; a crashed app is the most common reason a macro stops seeing what it expects.

## Example: emit a custom event when a task finishes

Read whatever the receiver needs from the screen, then send it. The device port tags the event when several devices run the same macro; it comes from [Storage](/docs/sdk/storage), which uses the same port to key its entries.

```python
import mas
from mas import Region

ENERGY = Region(x1=880, y1=40, x2=1060, y2=90)

def run_daily_tasks() -> int:
    done = 0
    # ... tap through the task list, counting each success ...
    return done

completed = run_daily_tasks()
energy_left = mas.read_text(region=ENERGY).text.strip()

mas.webhook("daily.finished", {
    "tasks_completed": completed,
    "energy_left": energy_left,
    "device_port": mas.get_current_device_port(),
})
mas.log(f"daily tasks done: {completed}")
```

A receiver subscribed to `custom.daily.finished` gets the signed payload with your keys plus `execution_id` and `device_name`, and can match it to the run's `macro.completed` event by the execution id.

## Errors

`open_app`, `close_app` and the state checks raise `DeviceNotConnectedError` when no device is bound to the run and `CommandFailedError` when the device rejects the command. `log` and `webhook` need no device. The full hierarchy is on [Errors and exceptions](/docs/sdk/errors).
