Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion uiautodev/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from uiautodev.router.device import make_router
from uiautodev.router.proxy import make_reverse_proxy
from uiautodev.router.proxy import router as proxy_router
from uiautodev.router.record import router as record_router
from uiautodev.router.xml import router as xml_router
from uiautodev.utils.envutils import Environment

Expand All @@ -40,7 +41,7 @@
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["GET", "POST"],
allow_methods=["GET", "POST", "DELETE", "PUT"],
allow_headers=["*"],
)

Expand Down Expand Up @@ -69,6 +70,7 @@
app.include_router(xml_router, prefix="/api/xml", tags=["xml"])
app.include_router(android_device_router, prefix="/api/android", tags=["android"])
app.include_router(proxy_router, tags=["proxy"])
app.include_router(record_router, prefix="/api/record", tags=["record"])


@app.get("/api/{platform}/features")
Expand Down
12 changes: 10 additions & 2 deletions uiautodev/command_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@

from __future__ import annotations

import logging
import time
import typing
from typing import Callable, Dict, List, Optional, Union

from pydantic import BaseModel

logger = logging.getLogger(__name__)

from uiautodev.command_types import AppLaunchRequest, AppTerminateRequest, By, Command, CurrentAppResponse, \
DumpResponse, FindElementRequest, FindElementResponse, InstallAppRequest, InstallAppResponse, SendKeysRequest, \
TapRequest, WindowSizeResponse
Expand Down Expand Up @@ -86,8 +89,13 @@ def app_current(driver: BaseDriver) -> CurrentAppResponse:

@register(Command.APP_LAUNCH)
def app_launch(driver: BaseDriver, params: AppLaunchRequest):
if params.stop:
driver.app_terminate(params.package)
"""
Launch an app.

Args:
driver: BaseDriver instance
params: AppLaunchRequest with package
"""
driver.app_launch(params.package)


Expand Down
1 change: 0 additions & 1 deletion uiautodev/command_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ class CurrentAppResponse(BaseModel):

class AppLaunchRequest(BaseModel):
package: str
stop: bool = False


class AppTerminateRequest(BaseModel):
Expand Down
110 changes: 109 additions & 1 deletion uiautodev/driver/android/adb_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,117 @@ def app_current(self) -> CurrentAppResponse:
)

def app_launch(self, package: str):
"""
Launch an app and bring it to foreground.

This method:
1. Checks if the app is installed
2. Uses 'monkey' command to launch the app

Note: To ensure a clean launch, call app_terminate() first before calling this method.

Args:
package: Package name of the app to launch
"""
if self.adb_device.package_info(package) is None:
raise AndroidDriverException(f"App not installed: {package}")
self.adb_device.app_start(package)

# Use monkey command to launch the app
print(f"[app_launch] Launching app {package} using monkey command")
logger.info(f"Launching app {package} using monkey command")
try:
result = self.adb_device.shell2([
"monkey", "-p", package, "-c", "android.intent.category.LAUNCHER", "1"
], timeout=10)

if result.returncode == 0:
print(f"[app_launch] Successfully launched {package} using monkey command")
logger.info(f"Successfully launched {package} using monkey command")
time.sleep(0.5) # Wait for app to appear
return
else:
error_msg = f"monkey command failed for {package}, returncode={result.returncode}, output={result.output}"
print(f"[app_launch] {error_msg}")
logger.error(error_msg)
raise AndroidDriverException(f"Failed to launch app {package}: {result.output}")
except Exception as e:
error_msg = f"Failed to launch {package} using monkey: {e}"
print(f"[app_launch] {error_msg}")
logger.error(error_msg)
raise AndroidDriverException(f"Failed to launch app {package}: {e}")

# Old code below (kept for reference, but should not be reached)
# Get the main activity using 'cmd package resolve-activity'
# This is more reliable than package_info.main_activity
try:
# Use 'cmd package resolve-activity' to get the launcher activity
result = self.adb_device.shell2([
"cmd", "package", "resolve-activity", "--brief", package
], rstrip=True, timeout=5)

if result.returncode == 0 and result.output:
# Parse the output to get activity name
# Output format is:
# priority=0 preferredOrder=0 match=0x108000 specificIndex=-1 isDefault=false
# com.package/.Activity
# The activity is usually on the last line
lines = [line.strip() for line in result.output.strip().split('\n') if line.strip()]
activity_line = None

# Try to find activity in output (usually the last line that contains package name and '/')
for line in reversed(lines): # Check from last line first
if '/' in line and package in line and not line.startswith('priority'):
# Remove "name=" prefix if present
activity_line = line.replace('name=', '').strip()
break

if activity_line and '/' in activity_line:
# Launch using the resolved activity
logger.info(f"Attempting to launch {package} with activity: {activity_line}")
launch_result = self.adb_device.shell2([
"am", "start", "-n", activity_line
], timeout=5)
if launch_result.returncode == 0:
logger.info(f"Successfully launched {package} using activity: {activity_line}")
# Wait a moment for app to appear
time.sleep(0.3)
return
else:
logger.warning(f"am start failed for {activity_line}, returncode={launch_result.returncode}, output={launch_result.output}")
else:
logger.warning(f"Could not parse activity from resolve-activity output. Lines: {lines}, Output: {result.output}")

# Fallback: try using package_info if resolve-activity fails
logger.warning(f"Could not resolve activity for {package}, trying package_info")
package_info = self.adb_device.package_info(package)
if isinstance(package_info, dict):
main_activity = package_info.get('main_activity')
else:
main_activity = getattr(package_info, 'main_activity', None)

if main_activity:
activity_name = main_activity if main_activity.startswith(".") else f"{package}/{main_activity}"
launch_result = self.adb_device.shell2([
"am", "start", "-n", activity_name
], timeout=5)
if launch_result.returncode == 0:
logger.info(f"Successfully launched {package} using main activity: {activity_name}")
time.sleep(0.3)
return
else:
logger.warning(f"am start failed for {activity_name}: {launch_result.output}")
except Exception as e:
logger.warning(f"Failed to launch using resolved activity: {e}, falling back to app_start")

# Final fallback: use app_start
logger.info(f"Using app_start as fallback for {package}")
try:
self.adb_device.app_start(package)
logger.info(f"app_start completed for {package}")
time.sleep(0.3)
except Exception as e:
logger.error(f"app_start failed for {package}: {e}")
raise AndroidDriverException(f"Failed to launch app {package}: {e}")
Comment on lines +151 to +222
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

Unreachable code detected. Lines 167-238 will never execute because all code paths before this either return on line 155 or raise an exception on line 160/165. This entire block of "Old code" should be removed as it's commented as being kept "for reference" but creates dead code in production.

Copilot uses AI. Check for mistakes.

def app_terminate(self, package: str):
self.adb_device.app_stop(package)
Expand Down
57 changes: 57 additions & 0 deletions uiautodev/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,60 @@ class AppInfo(BaseModel):
packageName: str
versionName: Optional[str] = None # Allow None values
versionCode: Optional[int] = None


# Recording related models
class Selector(BaseModel):
"""Element selector for recording"""
id: Optional[str] = None # resource-id
text: Optional[str] = None
className: Optional[str] = None
xpath: Optional[str] = None
contentDesc: Optional[str] = None # content-desc / accessibilityLabel


class RecordEvent(BaseModel):
"""Recording event model"""
action: str # tap, long_press, input, swipe, scroll, back, home, etc.
selector: Optional[Selector] = None
value: Optional[str] = None # for input action
timestamp: Optional[float] = None
x: Optional[float] = None # for coordinate-based actions
y: Optional[float] = None
x1: Optional[float] = None # for swipe actions
y1: Optional[float] = None
x2: Optional[float] = None
y2: Optional[float] = None
duration: Optional[float] = None # for long_press, swipe duration


class RecordScript(BaseModel):
"""Recorded script model"""
id: Optional[str] = None
name: str
platform: str # android, ios, harmony
deviceSerial: Optional[str] = None
appPackage: Optional[str] = None
appActivity: Optional[str] = None
events: List[RecordEvent] = []
createdAt: Optional[float] = None
updatedAt: Optional[float] = None
scriptType: str = "appium_python" # appium_python, appium_js, uiautomator2, xcuitest


class SaveScriptRequest(BaseModel):
"""Request model for saving script"""
name: str
platform: str
deviceSerial: Optional[str] = None
appPackage: Optional[str] = None
appActivity: Optional[str] = None
events: List[RecordEvent]
scriptType: str = "appium_python"


class SaveScriptResponse(BaseModel):
"""Response model for saving script"""
id: str
success: bool
message: str
Loading
Loading