# ADB Python Tutorial: Control an Android Emulator

> An adb Python tutorial for Macro Automation Studio: tap, swipe, type, press keys and take screenshots on BlueStacks, with adb managed by the app for you.

Source: https://automationmacro.com/docs/guides/control-an-emulator-from-python (Guides, updated 2026-09-04)

Most adb Python tutorials end in a `subprocess` wrapper around `adb shell input tap`. This adb Python tutorial for Macro Automation Studio (MAS) takes a different route: the app owns adb, and your script talks to the app through the `mas` package. This guide goes from a fresh project to a script that opens an app, searches for a term, scrolls and saves a screenshot. It is for Python developers who want to drive BlueStacks, LDPlayer, MuMu Player or MEmu without writing adb calls.

## Before you start

- MAS is installed and you are signed in with the trial or a plan. See [Install MAS](/docs/install).
- An emulator is running with ADB enabled and is added as a device in **Device Groups**. See [Devices](/docs/devices).
- You know basic Python. Nothing else is installed by you: MAS ships its own Python 3.13 environment with `mas` inside.

## How the pieces fit

MAS starts your script as `python -u -m src.app` with the project folder as the working directory. It passes the connection details in environment variables (`MAS_RPC_PORT`, `MAS_RPC_HOST`, `MAS_DEVICE_ID`, `MAS_SESSION_TOKEN`). Every `mas.*` call becomes a JSON-RPC 2.0 request to the app. The app runs the adb command, the template search or the OCR, and sends the result back. The device is bound before your first line runs, so there is no `connect()` call and no serial to manage. See [Concepts](/docs/concepts).

## Create the project

1. Open **Macros** and click **Create New Project**.
2. Choose **Code-Based**, set **Target Device** to mobile, name the project and click **Create Project**.
3. Open the project. The Code Editor shows `src/app.py`.
4. Pick the emulator in the device selector and click **Run** (<kbd>F5</kbd>) whenever you want to try a snippet. **Stop** is <kbd>Shift</kbd>+<kbd>F5</kbd>.

## Screen size and coordinates

Every coordinate you pass to the SDK is a pixel offset from the top-left corner of the device screen, in the device's own resolution. The size of the emulator window on your monitor does not matter.

```python
import mas

size = mas.get_screen_size()
print(f"Screen: {size.width}x{size.height}")

center = (size.width // 2, size.height // 2)
mas.click(*center)
```

`get_screen_size()` returns a `ScreenSize` with `width` and `height`. `get_device_info()` returns the same plus the device `name`, `type` and `connected` flag. When a script must survive a resolution change, compute positions as fractions of the size, as the example at the end does. Template images and OCR regions do not survive it; see the [image recognition guide](/docs/guides/image-recognition-macros).

## Take a Python adb screenshot

```python
import base64
import mas

shot = mas.take_screenshot()
print(shot.width, shot.height, shot.timestamp)

with open("screen.png", "wb") as f:
    f.write(base64.b64decode(shot.base64))
```

`take_screenshot()` returns a `Screenshot`: `base64` holds a PNG, `width` and `height` are its pixel size, and `timestamp` is an ISO 8601 string. The file above lands in the project folder because that is the working directory. Pass the same object to several `find_object` calls with `screenshot=shot` and they all search one frame instead of capturing again.

## Tap

```python
mas.click(270, 800)                 # tap, then wait 1000 ms
mas.click(270, 800, delay_ms=300)   # shorter pause for tight loops
```

`click(x, y, delay_ms=1000)` taps once and waits `delay_ms` afterwards so the app can react. There is no hold duration on a tap; for a long press use `key_press` with `duration_ms`.

## Swipe

```python
mas.swipe((270, 750), (270, 300), duration_ms=400)    # scroll a list down: drag from lower to upper
mas.swipe((100, 500), (400, 500), duration_ms=1000)   # drag and drop: slow and deliberate
```

`swipe(from_coords, to_coords, duration_ms=1000)` takes two `(x, y)` tuples. A short duration flicks; a long one drags. Pinch gestures are separate calls: `zoom_in()` and `zoom_out()` default to the screen centre with `percent=50`.

## Type

```python
mas.click(270, 120, delay_ms=500)          # focus the field first
mas.input_text("hello world")
mas.input_text("new value", clear=True)    # replace what is there
```

`input_text(text, delay_ms=0, clear=False)` types into the focused field, so tap the field first. `clear=True` moves the cursor to the end and deletes the existing characters before typing.

## Press keys

```python
from mas import KeyCode

mas.key_press(KeyCode.BACK)
mas.key_press(KeyCode.HOME)
mas.key_press(KeyCode.ENTER)
mas.key_press(KeyCode.DELETE, repeat=10)          # ten presses in one command
mas.key_press(KeyCode.POWER, duration_ms=3000)    # long press
```

`key_press(key_code, modifiers=None, duration_ms=100, repeat=1)` sends an Android key event. A `duration_ms` of 500 or more becomes a long press; the exact number of milliseconds beyond that is not honoured. `repeat` runs from 1 to 100 and is much faster than a Python loop. `KeyCode` also has the D-pad, volume, media and number keys.

## Open and check apps

```python
mas.open_app("com.android.settings", timeout_ms=5000)
print(mas.get_current_app())                        # package name in front
print(mas.is_app_focused("com.android.settings"))   # True or False

if mas.get_app_state("com.android.chrome") == mas.NOT_RUNNING:
    mas.open_app("com.android.chrome")

mas.close_app("com.android.settings")
```

`open_app(package_name, timeout_ms=2000)` launches by package name and waits `timeout_ms`. To learn a package name, open the app by hand and print `mas.get_current_app()`. `get_app_state` returns `NOT_INSTALLED`, `NOT_RUNNING`, `RUNNING_IN_BACKGROUND_SUSPENDED`, `RUNNING_IN_BACKGROUND` or `RUNNING_IN_FOREGROUND`. `close_app` force-stops the package.

## A complete script

The script opens Android Settings, searches for a term, scrolls the results, saves a screenshot and checks with OCR that the term is on screen. The search box is a template you crop in Asset Lab; replace the ID with yours. The swipe uses fractions of the screen size so it works at any resolution.

```python
import base64
import sys
import time

import mas
from mas import KeyCode

PACKAGE = "com.android.settings"
TERM = "Display"

images = mas.images({"search_box": 301})


def main():
    size = mas.get_screen_size()
    mas.log(f"Screen {size.width}x{size.height}")

    mas.open_app(PACKAGE, timeout_ms=5000)
    if not mas.is_app_focused(PACKAGE):
        mas.log(f"{PACKAGE} did not come to the front", level="error")
        sys.exit(2)

    box = mas.find_object_retry(images.search_box, total_tries=3, time_sleep=2.0)
    if box is None:
        mas.log("Search box not found; crop it in Asset Lab", level="error")
        sys.exit(2)
    mas.click(box.x, box.y, delay_ms=800)
    mas.input_text(TERM, clear=True)
    mas.key_press(KeyCode.ENTER)
    time.sleep(2)

    x = size.width // 2
    mas.swipe((x, int(size.height * 0.75)), (x, int(size.height * 0.35)), duration_ms=600)
    time.sleep(1)

    shot = mas.take_screenshot()
    with open("search_results.png", "wb") as f:
        f.write(base64.b64decode(shot.base64))
    mas.log(f"Saved search_results.png ({shot.width}x{shot.height})")

    text = mas.read_text(screenshot=shot, psm=11)
    mas.log(f"Term on screen: {TERM.lower() in text.text.lower()}")

    mas.key_press(KeyCode.HOME)


if __name__ == "__main__":
    main()
```

`read_text` with `psm=11` reads sparse text across the whole frame, which suits a results list. The [OCR guide](/docs/guides/ocr-text-reading) covers regions and modes for reading one counter. If you would rather describe the task than write it, [MAS Agent](/agent) writes this kind of script for you.

## Where adb fits in this Python tutorial

You never call adb, but it is doing the work underneath.

- **Binary.** The installer ships adb. On Windows it sits under `C:\ProgramData\MacroAutomationStudio\3rdparty`, with a built-in copy as a fallback. On a Mac it sits inside the app bundle, with Homebrew's adb as the fallback. Nothing goes on your PATH. The [Install](/docs/install) page has the details.
- **Connection.** Emulators expose adb on a local TCP port. When you click **Start** or **Run**, MAS runs `adb connect 127.0.0.1:<port>` for the port on the device card and keeps that session for the run.
- **Input.** `click` becomes `adb shell input tap`, `swipe` becomes `adb shell input touchscreen swipe`, `input_text` becomes `adb shell input text`, and `key_press` becomes `adb shell input keyevent`. Pinch gestures are written straight to the emulator's touch input device.
- **Screen.** `take_screenshot` and every `find_object` capture with `adb exec-out screencap -p`, the binary-safe form that does not mangle the PNG on Windows. `get_screen_size` reads `wm size`; `get_current_app` reads the window manager.
- **Apps.** `open_app` fires the launcher intent through `monkey` and falls back to `am start`; `close_app` runs `am force-stop`.
- **Servers.** MAS runs its own adb server. If a second adb build on your PATH starts its own, the two replace each other and your device drops to Connecting. The [ADB troubleshooting](/docs/adb-troubleshooting) page lists the fixes, port by port.

Because the app does all of this, a script written on BlueStacks runs unchanged on LDPlayer, on a cloud device, or on your own phone over a local port.

## What can go wrong

### Could not discover RPC port

The script was started from a terminal instead of from MAS, so the environment variables are missing. Run it with **Run** in the Code Editor or from a device card.

### Taps land in the wrong place

You are using window pixels instead of device pixels, or the emulator's resolution changed. Print `mas.get_screen_size()` and compare it with the coordinates you pass. Coordinates from Asset Lab are already device pixels.

### input_text typed nothing

No field had focus. Tap the field with `click` and give it `delay_ms=500` before typing. Some apps open a keyboard that covers the field; `key_press(KeyCode.BACK)` closes it after typing.

### Device not found or offline

ADB is off in the emulator, the port on the device card is wrong, or another adb server took over. Follow [ADB troubleshooting](/docs/adb-troubleshooting).

If the app you drive is a game, treat it with the same care as any other automated account. No automation tool is 100% risk-free, so automate responsibly and at your own discretion.
