"""App-related phone control functions."""
import json
import re
import logging
from ..core import run_command, check_device_connection
from typing import Optional, Dict
logger = logging.getLogger("phone_mcp")
async def list_installed_apps(
only_system=False, only_third_party=False, page=1, page_size=10, basic=True
):
"""List installed applications on the device with pagination support.
Args:
only_system (bool): If True, only show system apps
only_third_party (bool): If True, only show third-party apps
page (int): Page number (starts from 1)
page_size (int): Number of items per page
basic (bool): If True, only return basic info (faster loading, default behavior)
Returns:
str: JSON string containing:
{
"status": "success" or "error",
"message": Error message if status is error,
"total_count": Total number of apps,
"total_pages": Total number of pages,
"current_page": Current page number,
"page_size": Number of items per page,
"apps": [
{
"package_name": str,
"app_name": str,
"system_app": bool,
"version_name": str (if not basic),
"version_code": str (if not basic),
"install_time": str (if not basic)
},
...
]
}
"""
# Ensure page and page_size are integers
try:
page = int(page)
page_size = int(page_size)
except (ValueError, TypeError):
return json.dumps({
"status": "error",
"message": "Invalid page or page_size parameter. Must be integers."
}, indent=2)
# Check for connected device
connection_status = await check_device_connection()
if "ready" not in connection_status:
return connection_status
# Build the command based on options
if only_system:
cmd = "adb shell cmd package list packages -s" # System packages only
elif only_third_party:
cmd = "adb shell cmd package list packages -3" # Third-party packages only
else:
cmd = "adb shell cmd package list packages" # All packages
success, output = await run_command(cmd)
if not success:
return json.dumps(
{
"status": "error",
"message": "Failed to get installed apps",
"output": output,
},
indent=2,
)
# Process the output - convert package list to array
all_packages = []
for line in output.strip().split("\n"):
if line.startswith("package:"):
package_name = line[8:].strip() # Remove "package:" prefix
all_packages.append(package_name)
# Calculate pagination
total_count = len(all_packages)
total_pages = (total_count + page_size - 1) // page_size
# Validate page number
if page < 1:
page = 1
elif page > total_pages:
page = total_pages if total_pages > 0 else 1
# Calculate slice indices for pagination
start_idx = (page - 1) * page_size
end_idx = min(start_idx + page_size, total_count)
# Get packages for current page
current_packages = all_packages[start_idx:end_idx]
# Process packages based on basic/detailed mode
apps_info = []
for package_name in current_packages:
app_info = {"package_name": package_name}
# Get app label (name)
cmd = f'adb shell cmd package get-app-label {package_name}'
success, label_output = await run_command(cmd)
if success and label_output:
app_info["app_name"] = label_output.strip()
else:
app_info["app_name"] = package_name
# Check if system app
cmd = f'adb shell pm path {package_name}'
success, path_output = await run_command(cmd)
app_info["system_app"] = success and "system" in path_output.lower()
if not basic:
# Get detailed info
cmd = f'adb shell dumpsys package {package_name}'
success, info_output = await run_command(cmd)
if success:
# Parse version info
version_name = re.search(r"versionName=([^=\s]+)", info_output)
if version_name:
app_info["version_name"] = version_name.group(1)
version_code = re.search(r"versionCode=(\d+)", info_output)
if version_code:
app_info["version_code"] = version_code.group(1)
# Parse install time
first_install = re.search(r"firstInstallTime=([^=\s]+)", info_output)
if first_install:
app_info["install_time"] = first_install.group(1)
apps_info.append(app_info)
# Create the final result
result = {
"status": "success",
"total_count": total_count,
"total_pages": total_pages,
"current_page": page,
"page_size": page_size,
"apps": apps_info
}
if only_system:
result["type"] = "system"
elif only_third_party:
result["type"] = "third_party"
return json.dumps(result, indent=2)
async def list_app_activities(package_name: str):
"""List all activities in a specific app package.
Args:
package_name (str): Package name of the app
Returns:
str: JSON string with list of activities or error message
"""
# Check for connected device
connection_status = await check_device_connection()
if "ready" not in connection_status:
return connection_status
# Build the command to query activities
cmd = f"adb shell cmd package query-activities -a android.intent.action.MAIN -c android.intent.category.LAUNCHER --components {package_name}"
success, output = await run_command(cmd)
if not success:
return json.dumps(
{
"status": "error",
"message": f"Failed to get activities for {package_name}",
"output": output,
},
indent=2,
)
activities = []
# Process the output - extract activities
for line in output.strip().split("\n"):
if package_name in line:
# Format: packageName/activityName
parts = line.strip().split()
if len(parts) > 0:
activity = parts[0].strip()
activities.append(activity)
return json.dumps(
{
"status": "success",
"package": package_name,
"count": len(activities),
"activities": activities,
},
indent=2,
)
async def terminate_app(package_name: str):
"""Force stop an application on the device.
Args:
package_name (str): Package name of the app to terminate
Returns:
str: Success or error message
"""
# Check for connected device
connection_status = await check_device_connection()
if "ready" not in connection_status:
return connection_status
# Verify the package exists
cmd = f"adb shell pm list packages | grep {package_name}"
success, output = await run_command(cmd)
if not success or package_name not in output:
# Try an exact match instead of grep
verify_cmd = "adb shell pm list packages"
success, verify_output = await run_command(verify_cmd)
package_exists = False
if success:
for line in verify_output.strip().split("\n"):
if line.strip() == f"package:{package_name}":
package_exists = True
break
if not package_exists:
return f"Package {package_name} not found on device"
# Force stop the application
cmd = f"adb shell am force-stop {package_name}"
success, output = await run_command(cmd)
if success:
return f"Successfully terminated {package_name}"
else:
return f"Failed to terminate app: {output}"
async def set_alarm(hour: int, minute: int, label: str = "Alarm") -> str:
"""Set an alarm on the phone.
Creates a new alarm with the specified time and label using the default
clock application.
Args:
hour (int): Hour in 24-hour format (0-23)
minute (int): Minute (0-59)
label (str): Optional label for the alarm (default: "Alarm")
Returns:
str: Success message if the alarm was set, or an error message
if the alarm could not be created.
"""
# Check for connected device
connection_status = await check_device_connection()
if "ready" not in connection_status:
return connection_status
# Validate time inputs
if not (0 <= hour <= 23 and 0 <= minute <= 59):
return "Invalid time. Hour must be 0-23 and minute must be 0-59."
# Format time for display
time_str = f"{hour:02d}:{minute:02d}"
escaped_label = label.replace("'", "\\'")
# Create the alarm using the alarm clock intent
cmd = (
f"adb shell am start -a android.intent.action.SET_ALARM "
f"-e android.intent.extra.alarm.HOUR {hour} "
f"-e android.intent.extra.alarm.MINUTES {minute} "
f"-e android.intent.extra.alarm.MESSAGE '{escaped_label}' "
f"-e android.intent.extra.alarm.SKIP_UI true"
)
success, output = await run_command(cmd)
if success:
return f"Alarm set for {time_str} with label '{label}'"
else:
return f"Failed to set alarm: {output}"
async def launch_app_activity(package_name: str, activity_name: str = None) -> str:
"""Launch an app using package name and optionally an activity name
This function uses adb to start an application on the device either by package name
or by specifying both package and activity. It provides reliable app launching across
different Android devices and versions.
Args:
package_name (str): The package name of the app to launch (e.g., "com.android.contacts")
activity_name (str): The specific activity to launch. If not provided,
launches the app's main activity. Defaults to None.
Returns:
str: JSON string with operation result:
For successful operations:
{
"status": "success",
"message": "Successfully launched <package_name>"
}
For failed operations:
{
"status": "error",
"message": "Failed to launch app: <error details>"
}
Examples:
# Launch an app using just the package name
result = await launch_app_activity("com.android.contacts")
# Launch a specific activity within an app
result = await launch_app_activity("com.android.dialer", "com.android.dialer.DialtactsActivity")
# Launch Android settings
result = await launch_app_activity("com.android.settings")
"""
try:
if activity_name:
# Launch specific activity
cmd = f"adb shell am start -n {package_name}/{activity_name}"
else:
# Launch app's main activity
cmd = f"adb shell monkey -p {package_name} -c android.intent.category.LAUNCHER 1"
success, output = await run_command(cmd)
if success:
return json.dumps({
"status": "success",
"message": f"Successfully launched {package_name}"
})
else:
return json.dumps({
"status": "error",
"message": f"Failed to launch app: {output}"
})
except Exception as e:
logger.error(f"Error launching app {package_name}: {str(e)}")
return json.dumps({
"status": "error",
"message": f"Failed to launch app: {str(e)}"
})
async def launch_intent(intent_action: str, intent_type: Optional[str] = None, extras: Optional[Dict[str, str]] = None) -> str:
"""Launch an activity using Android intent system
This function allows launching activities using Android's intent system, which can be used
to perform various actions like opening the contacts app in "add new contact" mode,
opening URLs, or sharing content between apps.
Args:
intent_action (str): The action to perform (e.g., "android.intent.action.INSERT",
"android.intent.action.VIEW", "android.intent.action.SEND")
intent_type (str, optional): The MIME type for the intent (e.g., "vnd.android.cursor.dir/contact",
"text/plain", "image/jpeg"). Defaults to None.
extras (Dict[str, str], optional): Extra data to pass with the intent as key-value pairs.
Useful for pre-populating fields or passing data. Defaults to None.
Returns:
str: JSON string with operation result:
For successful operations:
{
"status": "success",
"message": "Successfully launched intent: <intent_action>"
}
For failed operations:
{
"status": "error",
"message": "Failed to launch intent: <error details>"
}
Examples:
# Open contacts app in "add new contact" mode
extras = {"name": "John Doe", "phone": "1234567890"}
result = await launch_intent(
"android.intent.action.INSERT",
"vnd.android.cursor.dir/contact",
extras
)
# Open URL in browser
result = await launch_intent(
"android.intent.action.VIEW",
None,
{"uri": "https://www.example.com"}
)
# Share text
result = await launch_intent(
"android.intent.action.SEND",
"text/plain",
{"android.intent.extra.TEXT": "Hello world!"}
)
"""
try:
# Construct base command
cmd = f"adb shell am start -a {intent_action}"
# Add type if provided
if intent_type:
cmd += f" -t {intent_type}"
# Add extras if provided
if extras:
for key, value in extras.items():
# Escape quotes in value
value = value.replace('"', '\\"')
cmd += f' --es {key} "{value}"'
success, output = await run_command(cmd)
if success:
return json.dumps({
"status": "success",
"message": f"Successfully launched intent: {intent_action}"
})
else:
return json.dumps({
"status": "error",
"message": f"Failed to launch intent: {output}"
})
except Exception as e:
logger.error(f"Error launching intent {intent_action}: {str(e)}")
return json.dumps({
"status": "error",
"message": f"Failed to launch intent: {str(e)}"
})