Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
37 changes: 35 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,38 @@ 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. By default, stops the app first to ensure clean launch.

This ensures the app is brought to foreground reliably.

Args:
driver: BaseDriver instance
params: AppLaunchRequest with package and optional stop flag
"""
# Use stop parameter from request (default is True in AppLaunchRequest)
# This ensures app is brought to foreground reliably
stop_first = params.stop

# Check if driver's app_launch supports stop_first parameter
import inspect
try:
sig = inspect.signature(driver.app_launch)
if 'stop_first' in sig.parameters:
# Driver supports stop_first parameter
driver.app_launch(params.package, stop_first=stop_first)
return
except (TypeError, AttributeError):
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.

'except' clause does nothing but pass and there is no explanatory comment.

Suggested change
except (TypeError, AttributeError):
except (TypeError, AttributeError):
# If signature inspection fails, fallback to manual stop/launch below.

Copilot uses AI. Check for mistakes.
pass

# Fallback: manually stop then launch
if stop_first:
try:
driver.app_terminate(params.package)
time.sleep(0.3) # Brief wait for app to stop
except Exception as e:
logger.warning(f"Failed to stop app before launch: {e}")

driver.app_launch(params.package)


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

class AppLaunchRequest(BaseModel):
package: str
stop: bool = False
stop: bool = True # Default to True: stop app before launch for clean start
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.

Breaking change in default behavior: The default value of stop has been changed from False to True. This means that existing code calling AppLaunchRequest without explicitly setting stop will now stop the app before launching, which is a behavioral change that could break existing workflows. Consider documenting this change clearly or using a different parameter name to avoid breaking existing code.

Suggested change
stop: bool = True # Default to True: stop app before launch for clean start
stop: bool = False # Default to False: do not stop app before launch (backward compatible)

Copilot uses AI. Check for mistakes.


class AppTerminateRequest(BaseModel):
Expand Down
128 changes: 126 additions & 2 deletions uiautodev/driver/android/adb_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,134 @@ def app_current(self) -> CurrentAppResponse:
package=info.package, activity=info.activity, pid=info.pid
)

def app_launch(self, package: str):
def app_launch(self, package: str, stop_first: bool = True):
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

stop_first这里不要加,分开成两个操作。 app_stop() 和 app_launch(). 因为除了android驱动还有ios,鸿蒙驱动。修改一个就好把其他的两个也一起改了。不划算

"""
Launch an app and bring it to foreground.

This method:
1. Checks if the app is installed
2. Optionally stops the app first to ensure clean launch (default: True)
3. Uses 'am start' command with resolved main activity to launch the app

Note: By default, this method stops the app first to ensure a clean launch.
This is more reliable than just starting an app that may already be running in background.

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

# Step 1: Stop the app first to ensure clean launch
if stop_first:
print(f"[app_launch] Stopping app {package} before launch")
logger.info(f"Stopping app {package} before launch")
try:
self.app_terminate(package)
time.sleep(0.5) # Wait for app to fully stop
print(f"[app_launch] App {package} stopped successfully")
logger.info(f"App {package} stopped successfully")
except Exception as e:
print(f"[app_launch] Failed to stop {package}: {e}")
logger.warning(f"Failed to stop {package} before launch: {e}")

# Step 2: 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}")
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.

Redundant logging detected: Both print() and logger calls are used throughout the app_launch method to log identical messages. This creates code duplication and maintenance overhead. Consider removing the print statements and relying solely on the logger.

Copilot uses AI. Check for mistakes.

# 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