Philipp Schmid · 博客

用 Gemini 3.5 Flash 电脑操控 Android 手机

Control an Android Phone with Gemini 3.5 Flash Computer Use

二〇二六年六月二十五日 · 英文原文

Google 发布 Gemini 3.5 Flash 的 Computer Use 功能,模型通过截图和 function calling 控制 Android 模拟器,支持 click、type、open_app 等操作。指南提供 Python SDK 和 ADBBridge 实现 agent 循环,坐标归一化至 0-999 网格,支持本地模拟器或远程设备。GitHub 仓库包含设置脚本和完整 agent.py 代码。

2026年6月25日 · 9分钟阅读查看代码

Gemini 3.5 Flash 内置了计算机使用功能。该模型查看截图,决定下一步操作,并返回类似 click(y=300, x=500) 的函数调用。你通过 ADB 在设备上执行该操作,截取新截图,然后将其发回。重复此过程,直到任务完成。

本指南将引导你使用 mobile 环境和 Python SDK 控制 Android 模拟器。

GitHub 仓库:https://github.com/google-gemini/gemini-android-computer-use-quickstart

什么是 Computer Use?

Computer Use 是 Gemini 3.5 Flash 中的一个原生工具。你向模型提供一张截图和一个目标(例如“打开设置并开启深色模式”)。模型会返回结构化的操作:点击、文本输入、滑动、启动应用。你的代码在目标设备上执行这些操作。

它的工作方式类似于 function calling。模型提出操作,你执行它们,然后发回结果。模型通过截图保持在循环中。

Gemini 支持三种 Computer Use 环境:browser 用于桌面 Web 自动化,mobile 用于移动设备/模拟器,以及 desktop 用于操作系统级别的控制。本指南使用 mobile

截图 ──> Gemini 3.5 Flash ──> function_call
      ▲                                    │
      │                                    ▼
      └──────── 通过 ADB 执行 ◄──────────┘

伪代码

Python

from google import genai
 
client = genai.Client()
bridge = ADBBridge()
 
# 截取初始截图并发送第一个请求
screenshot = bridge.screenshot()
 
interaction = client.interactions.create(
    model="gemini-3.5-flash",
    input=[
        {"type": "text", "text": "打开设置并启用深色模式"},
        {"type": "image", "data": b64(screenshot), "mime_type": "image/png"},
    ],
    tools=[{"type": "computer_use", "environment": "mobile"}],
)
 
# Agent 循环:执行操作,发回结果
while interaction has function_calls:
    for call in interaction.function_calls:
        bridge.execute(call.name, call.args)  # click, type, open_app...
 
    screenshot = bridge.screenshot()
    interaction = client.interactions.create(
        model="gemini-3.5-flash",
        previous_interaction_id=interaction.id,
        input=[function_results + screenshot],
        tools=[{"type": "computer_use", "environment": "mobile"}],
    )
 
print(interaction.output_text)

支持的操作(mobile 环境)

以下是模型在使用 environment: "mobile" 时可以请求的操作。你的代码必须处理每一个。

操作 描述 参数
open_app 按名称打开应用 app_name: str, intent: str (可选)
click 点击屏幕坐标 y: int (0-999), x: int (0-999)
type 输入文本 text: str, press_enter: bool (默认为 false)
long_press 长按坐标 y: int (0-999), x: int (0-999), seconds: int (默认为 2)
drag_and_drop 在两点之间拖动 start_y, start_x, end_y, end_x
press_key 按下按键(home、back、enter...) key: str
go_back 返回 (无)
wait 暂停执行 seconds: int (默认为 1)
list_apps 列出已安装的应用 (无)
take_screenshot 截取屏幕 (无)

坐标被归一化到 0-999 的网格。(0, 0) 是左上角,(999, 999) 是右下角。你的 bridge 会根据设备分辨率将这些坐标转换为实际的像素坐标。

设置

无需 Android Studio GUI。在你的 Mac 上运行设置脚本,从终端安装 Android SDK、模拟器并创建一个虚拟设备。

1. 运行设置脚本

安装脚本链接:https://github.com/google-gemini/gemini-android-computer-use-quickstart/blob/main/setup_emulator.sh

Bash

chmod +x setup_emulator.sh
./setup_emulator.sh

2. 安装 Python 依赖

Bash

pip install google-genai

Python agent 脚本会自动处理定位 SDK 路径和在后台启动模拟器,因此无需额外手动导出环境变量或进行终端设置。

Agent 循环

完整的工作脚本 agent.py。它会自动设置环境变量,在后台启动模拟器(如果尚未运行),等待其启动完成,然后开始 Computer Use 循环。

Python

import base64
import json
import os
import re
import subprocess
import sys
import time
 
from google import genai
 
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
 
 
def setup_android_env():
    paths_to_check = [
        os.environ.get("ANDROID_HOME"),
        "/opt/homebrew/share/android-commandlinetools",
        "/usr/local/share/android-commandlinetools",
    ]
    
    android_home = None
    for p in paths_to_check:
        if p and os.path.exists(p):
            android_home = p
            break
            
    if not android_home:
        print("错误:未找到 ANDROID_HOME。请先运行 setup_emulator.sh。", file=sys.stderr)
        sys.exit(1)
        
    os.environ["ANDROID_HOME"] = android_home
    sdk_paths = [
        os.path.join(android_home, "cmdline-tools", "latest", "bin"),
        os.path.join(android_home, "emulator"),
        os.path.join(android_home, "platform-tools"),
    ]
    
    current_path = os.environ.get("PATH", "")
    for p in sdk_paths:
        if p not in current_path:
            current_path = p + os.pathsep + current_path
            
    os.environ["PATH"] = current_path
    return android_home
 
 
def start_emulator(avd_name="AI_Agent_Phone"):
    setup_android_env()
    
    try:
        res = subprocess.run(["adb", "devices"], capture_output=True, text=True)
        if "emulator" in res.stdout:
            return
    except FileNotFoundError:
        pass
 
    print(f"正在启动模拟器 '{avd_name}'...")
    log_file = open(os.path.join(BASE_DIR, "emulator.log"), "w")
    
    subprocess.Popen(
        ["emulator", "-avd", avd_name, "-delay-adb"],
        stdout=log_file,
        stderr=log_file,
        start_new_session=True,
    )
    
    print("等待模拟器启动...")
    for _ in range(60):
        try:
            res = subprocess.run(["adb", "devices"], capture_output=True, text=True)
            if "emulator" in res.stdout:
                boot_res = subprocess.run(
                    ["adb", "shell", "getprop", "sys.boot_completed"],
                    capture_output=True,
                    text=True,
                )
                if boot_res.stdout.strip() == "1":
                    print("模拟器已就绪。")
                    return
        except Exception:
            pass
        time.sleep(2)
        
    print("错误:模拟器启动失败。", file=sys.stderr)
    sys.exit(1)
 
 
class ADBBridge:
    def __init__(self, device_id=None):
        self.prefix = ["adb"] + (["-s", device_id] if device_id else [])
        self.width, self.height = self._screen_size()
 
    def _run(self, args, check=True):
        result = subprocess.run(self.prefix + args, capture_output=True, text=True)
        if check and result.returncode != 0:
            raise RuntimeError(f"ADB 错误:{result.stderr.strip()}")
        return result.stdout
 
    def _screen_size(self):
        output = self._run(["shell", "wm", "size"])
        match = re.search(r"Physical size: (\d+)x(\d+)", output)
        return (int(match.group(1)), int(match.group(2))) if match else (1080, 1920)
 
    def _px(self, x, y):
        return int(x / 1000 * self.width), int(y / 1000 * self.height)
 
    def click(self, y, x, **_):
        px, py = self._px(x, y)
        self._run(["shell", "input", "tap", str(px), str(py)])
 
    def type(self, text, press_enter=False, **_):
        self._run(["shell", "input", "text", text.replace(" ", "%s")])
        if press_enter:
            self._run(["shell", "input", "keyevent", "66"])
 
    def open_app(self, app_name=None, package_name=None, **_):
        pkg = app_name or package_name
        if not pkg:
            raise ValueError("open_app 需要 app_name 或 package_name")
            
        stdout = self._run(["shell", "monkey", "--pct-syskeys", "0", "-p", pkg,
                            "-c", "android.intent.category.LAUNCHER", "1"], check=False)
        if "No activities found" in stdout or "monkey aborted" in stdout:
            raise RuntimeError(f"应用 {pkg} 未安装或没有启动器 activity。")
 
    def scroll(self, y, x, direction, magnitude=800, **_):
        px, py = self._px(x, y)
        dist = int(magnitude / 1000 * self.height)
        dx, dy = {"up": (0, -dist), "down": (0, dist),
                  "left": (-dist, 0), "right": (dist, 0)}.get(direction, (0, 0))
        self._run(["shell", "input", "swipe", str(px), str(py),
                   str(px + dx), str(py + dy), "300"])
 
    def long_press(self, y, x, seconds=2, **_):
        px, py = self._px(x, y)
        self._run(["shell", "input", "swipe", str(px), str(py),
                   str(px), str(py), str(seconds * 1000)])
 
    def drag_and_drop(self, start_y, start_x, end_y, end_x, **_):
        sx, sy = self._px(start_x, start_y)
        ex, ey = self._px(end_x, end_y)
        self._run(["shell", "input", "swipe", str(sx), str(sy),
                   str(ex), str(ey), "300"])
 
    def press_key(self, key, **_):
        keymap = {"home": "3", "back": "4", "enter": "66",
                  "app_switch": "187", "menu": "82"}
        self._run(["shell", "input", "keyevent", keymap.get(key.lower(), key)])
 
    def go_back(self, **_):
        self._run(["shell", "input", "keyevent", "4"])
 
    def wait(self, seconds=1, **_):
        time.sleep(seconds)
 
    def list_apps(self, **_):
        output = self._run(["shell", "pm", "list", "packages", "-3"])
        apps = [l.split(":")[1] for l in output.splitlines() if l.startswith("package:")]
        if not apps:
            return {"apps": "此设备上未安装第三方应用。"}
        return {"apps": apps}
 
    def take_screenshot(self, **_):
        return None
 
    def screenshot(self) -> bytes:
        result = subprocess.run(
            self.prefix + ["exec-out", "screencap", "-p"], capture_output=True
        )
        return result.stdout
 
 
SYSTEM_PROMPT = """你正在操作一部 Android 手机。
* 使用提供的工具完成任务。
* 在假设某个元素缺失之前,请向下滚动检查整个屏幕。
* 你可以从任何地方通过包名打开应用。
* 仅使用 `type` 工具输入文本。不要使用虚拟键盘。
* 如果任务已经完成,请直接说明。
"""
 
 
def run_agent(task: str, device_id: str = None, max_turns: int = 100):
    start_emulator()
    client = genai.Client()
    bridge = ADBBridge(device_id)
 
    print(f"\n任务:{task}")
    print("-" * 40)
 
    screenshot_bytes = bridge.screenshot()
    user_input = [
        {"type": "text", "text": task},
        {
            "type": "image",
            "data": base64.b64encode(screenshot_bytes).decode(),
            "mime_type": "image/png",
        },
    ]
    
    previous_interaction_id = None
    turn = 0
    
    while turn < max_turns:
        turn += 1
        
        interaction = client.interactions.create(
            model="gemini-3.5-flash",
            system_instruction=SYSTEM_PROMPT,
            input=user_input,
            tools=[{"type": "computer_use", "environment": "mobile"}],
            previous_interaction_id=previous_interaction_id,
        )
        
        function_responses = []
        for step in interaction.steps:
            if step.type == "function_call":
                print(f"[function_call] {step.name}({step.arguments})")
                handler = getattr(bridge, step.name, None)
                result_text = {"status": "ok"}
                
                if handler:
                    try:
                        res = handler(**step.arguments)
                        if isinstance(res, dict):
                            result_text.update(res)
                    except Exception as e:
                        result_text = {"status": "error", "error": str(e)}
                else:
                    result_text = {"status": "error", "error": f"未知操作:{step.name}"}
                    
                print(f"[function_result] {result_text}")
                
                if "safety_decision" in step.arguments:
                    # 演示时自动批准安全决策
                    result_text["safety_acknowledgement"] = True
 
                screenshot_bytes = bridge.screenshot()
                
                fr = {
                    "type": "function_result",
                    "name": step.name,
                    "call_id": step.id,
                    "result": [
                        {"type": "text", "text": json.dumps(result_text)},
                        {
                            "type": "image",
                            "data": base64.b64encode(screenshot_bytes).decode(),
                            "mime_type": "image/png",
                        },
                    ],
                }
                function_responses.append(fr)
            else:
                print(f"\n结果:{interaction.output_text}")
                break
                
        user_input = function_responses
        previous_interaction_id = interaction.id
        
        if not function_responses:
            break
            
    return interaction
 
 
if __name__ == "__main__":
    task_desc = "查找 Philipp Schmid 的最新博客文章并总结它。"
    if len(sys.argv) > 1:
        task_desc = " ".join(sys.argv[1:])
    run_agent(task_desc)

如何运行

Bash

# 设置你的 API 密钥
export GEMINI_API_KEY="your-key"
 
# 运行 agent(如果需要,会自动启动模拟器)
python agent.py "打开设置并启用深色模式"

连接到远程设备

你也可以将物理 Android 设备或远程云模拟器作为目标,而不是本地虚拟设备。

  1. 启用 USB/无线调试:在目标设备上,启用开发者选项并打开 USB 调试或无线调试。
  2. 通过 ADB 连接:使用 adb connect 命令通过 TCP/IP 连接到远程设备:Bash

adb connect <device-ip-address>:5555 有关设置无线调试的详细信息,请参阅官方 Android 开发者文档中关于通过 Wi-Fi 使用 ADB 的部分。 3. 将设备 ID 传递给 Agent:将远程设备的连接字符串作为 device_id 参数传递给 agent 循环,以将其设为目标:Python

# 将远程或云托管的模拟器设为目标
run_agent("查看天气", device_id="35.200.100.10:5555")

后续步骤与开发者提示

希望这能帮助你开始在移动设备上使用 Gemini 3.5 Flash Computer Use。后续步骤可以包括:


感谢阅读!如果你有任何问题或反馈,请通过 TwitterLinkedIn 告诉我。

译自 Philipp Schmid · 博客 · 录于 二〇二六年六月二十五日