"""
System-related functions for Phone MCP.
This module provides functions to access system-level information on the phone.
"""
import asyncio
import json
import re
from ..core import run_command
async def get_current_window():
"""Get information about the current active window on the device.
Retrieves details about the currently focused window, active application,
and foreground activities on the device using multiple methods for reliability.
Returns:
str: JSON string with current window details or error message
"""
# Check for connected device
from ..core import check_device_connection
connection_status = await check_device_connection()
if "ready" not in connection_status:
return connection_status
try:
window_info = {}
success_count = 0
# Method 1: Get current focus information (most reliable)
cmd = "adb shell dumpsys window windows"
success, output = await run_command(cmd)
if success and output.strip():
success_count += 1
# Process the output with Python instead of grep
focus_patterns = ["mCurrentFocus", "mFocusedApp"]
for line in output.strip().split("\n"):
line = line.strip()
if any(pattern in line for pattern in focus_patterns):
if "mCurrentFocus" in line:
window_info["current_focus"] = line.replace(
"mCurrentFocus=", ""
).strip()
# Extract package and activity
match = re.search(r"(\S+)/(\S+)", line)
if match:
window_info["package_name"] = match.group(1)
window_info["activity_name"] = match.group(2)
elif "mFocusedApp" in line:
window_info["focused_app"] = line.replace(
"mFocusedApp=", ""
).strip()
# Method 2: Get current resumed activity
cmd = "adb shell dumpsys activity activities"
success, output = await run_command(cmd)
if success and output.strip():
# Use Python to find resumed activities
activity_patterns = ["ResumedActivity", "mResumedActivity"]
for line in output.strip().split("\n"):
if any(pattern in line for pattern in activity_patterns):
window_info["resumed_activity"] = line.strip()
success_count += 1
break
# Method 3: Get top activity using am command
cmd = "adb shell am stack list"
success, output = await run_command(cmd)
if not success or "Error" in output:
# Newer Android versions use different command
cmd = "adb shell cmd activity activities"
success, output = await run_command(cmd)
if success and output.strip():
# Python processing for activity/topResumedActivity
filtered_lines = []
for line in output.strip().split("\n"):
if "ACTIVITY" in line or "topResumedActivity" in line:
filtered_lines.append(line.strip())
if filtered_lines:
output = "\n".join(filtered_lines)
if success and output.strip():
success_count += 1
window_info["activity_stack"] = output.strip().split("\n")[0:2]
# Try to extract the package name if we don't have it yet
if "package_name" not in window_info:
package_match = re.search(r"([a-zA-Z0-9_.]+)/[a-zA-Z0-9_.]+", output)
if package_match:
window_info["package_name"] = package_match.group(1)
# Method 4: Get recent apps info
cmd = "adb shell dumpsys activity recents"
success, output = await run_command(cmd)
if success and output.strip():
# Use Python to find Recent #0 line
for line in output.strip().split("\n"):
if "Recent #0" in line:
window_info["recent_activity"] = line.strip()
success_count += 1
break
# If we still don't have package info, try direct method
if "package_name" not in window_info:
cmd = "adb shell dumpsys window"
success, output = await run_command(cmd)
if success and output.strip():
# Python processing for mCurrentFocus
for line in output.strip().split("\n"):
if "mCurrentFocus" in line:
match = re.search(r"([a-zA-Z0-9_.]+)/[a-zA-Z0-9_.]+", line)
if match:
window_info["package_name"] = match.group(1)
break
# Add device screen state
cmd = "adb shell dumpsys power"
success, output = await run_command(cmd)
if success and output.strip():
# Python processing for power state
screen_state_patterns = ["mWakefulness=", "Display Power"]
for line in output.strip().split("\n"):
if any(pattern in line for pattern in screen_state_patterns):
window_info["screen_state"] = (
"on" if "ON" in line or "AWAKE" in line else "off"
)
break
if success_count > 0:
return json.dumps(window_info, indent=2)
else:
# Last resort - just get basic device info
cmd = "adb shell getprop ro.product.model"
success, model = await run_command(cmd)
if success:
return json.dumps(
{
"basic_info": "Unable to get window details",
"device_model": model.strip(),
},
indent=2,
)
else:
return "Failed to retrieve current window information"
except Exception as e:
return f"Error retrieving window information: {str(e)}"
async def launch_app_activity(package_component=None, action=None, extra_args=None):
"""Launch an app by starting a specific activity with custom action and component.
This function starts an application by launching a specific activity component.
It effectively launches the app and can be used in conjunction with get_app_shortcuts
to discover available activities, but does not require it as a prerequisite.
Args:
package_component (str): App component in format "package/activity" (e.g. "com.example.app/.MainActivity")
This is the primary way to specify which app and activity to launch
action (str, optional): Intent action to use (e.g. "android.intent.action.VIEW")
extra_args (str, optional): Additional intent arguments to pass (e.g. "-d 'content://contacts/people/'")
Returns:
str: Success message with the launched component name, or error details if launch failed
Example:
To launch the main activity of an app:
launch_app_activity("com.example.app/.MainActivity")
To launch a specific activity with an action and data:
launch_app_activity("com.android.browser/.BrowserActivity",
action="android.intent.action.VIEW",
extra_args="-d 'https://example.com'")
"""
# Check for connected device
from ..core import check_device_connection
connection_status = await check_device_connection()
if "ready" not in connection_status:
return connection_status
try:
cmd_parts = ["adb shell am start"]
# Add action if provided
if action:
cmd_parts.append(f'-a "{action}"')
# Add component if provided
if package_component:
cmd_parts.append(f'-n "{package_component}"')
# Add any extra arguments
if extra_args:
cmd_parts.append(extra_args)
# Combine into final command
cmd = " ".join(cmd_parts)
# Execute the command
success, output = await run_command(cmd)
if success and ("Starting" in output or "Activity" in output):
component = package_component or "specified activity"
return f"Successfully launched {component}"
else:
return f"Failed to launch activity: {output}"
except Exception as e:
return f"Error launching activity: {str(e)}"
async def get_app_shortcuts(package_name=None):
"""Get application shortcuts for installed apps.
Retrieves shortcuts (quick actions) available for Android apps.
If package_name is provided, returns shortcuts only for that app,
otherwise lists all apps with shortcuts.
Args:
package_name (str, optional): Specific app package to get shortcuts for
Returns:
str: JSON string with app shortcuts information or error message
"""
# Check for connected device
from ..core import check_device_connection
connection_status = await check_device_connection()
if "ready" not in connection_status:
return connection_status
try:
# Use direct dumpsys shortcut command - most reliable method
cmd = "adb shell dumpsys shortcut"
success, output = await run_command(cmd)
if not success or not output.strip():
return "Failed to retrieve shortcut information"
# Filter by package name if provided (using Python instead of grep)
if package_name:
package_found = False
filtered_lines = []
line_count = 0
max_lines = 50 # Equivalent to grep -A 50
for line in output.strip().split("\n"):
if line.strip().startswith(f"Package: {package_name}"):
package_found = True
filtered_lines.append(line)
line_count = 0
elif package_found and line_count < max_lines:
filtered_lines.append(line)
line_count += 1
if filtered_lines:
output = "\n".join(filtered_lines)
else:
return f"No shortcuts found for package: {package_name}"
# Process the results
result = {}
# Extract packages with shortcuts
packages = []
current_package = None
shortcuts = []
lines = output.strip().split("\n")
for i, line in enumerate(lines):
line = line.strip()
if line.startswith("Package:"):
# Save previous package data if exists
if current_package and shortcuts:
result[current_package] = {"shortcuts": shortcuts}
# Start new package
parts = line.split(":", 1)
if len(parts) > 1:
current_package = (
parts[1].strip().split()[0]
) # Get package name without UID
packages.append(current_package)
shortcuts = []
# Extract shortcut info when inside a package section
elif current_package and "ShortcutInfo" in line:
shortcut = {}
shortcut_id_match = re.search(r"\{id=([^,]+)", line)
if shortcut_id_match:
shortcut["id"] = shortcut_id_match.group(1)
# Continue collecting data for this shortcut - use current index instead of searching
collect_for = 5 # Collect next 5 lines for this shortcut
for j in range(i + 1, min(i + collect_for, len(lines))):
subline = lines[j].strip()
# Extract shortcut label
if "shortLabel=" in subline:
label_match = re.search(r"shortLabel=([^,]+)", subline)
if label_match:
shortcut["label"] = label_match.group(1).strip()
# Extract intent data
elif "intents=" in subline:
intent_match = re.search(r"act=([^ ]+)", subline)
if intent_match:
shortcut["action"] = intent_match.group(1)
component_match = re.search(r"cmp=([^ \}/]+)", subline)
if component_match:
shortcut["component"] = component_match.group(1)
if shortcut.get("id"):
shortcuts.append(shortcut)
# Add the last package
if current_package and shortcuts:
result[current_package] = {"shortcuts": shortcuts}
# If we're looking for a specific package but didn't find any shortcuts
if package_name and package_name not in result:
return f"No shortcuts found for package: {package_name}"
# If not looking for a specific package, include list of all packages
if not package_name:
result["all_packages"] = packages
return json.dumps(result, indent=2)
except Exception as e:
return f"Error retrieving app shortcuts: {str(e)}"