Skip to content

Commit 54231d2

Browse files
authored
Add APK upload/install endpoints and androguard (#643)
1 parent 050fc8b commit 54231d2

2 files changed

Lines changed: 93 additions & 27 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ dependencies = [
103103
"pymysql>=1.1.2",
104104
"mysql-connector-python>=9.5.0",
105105
"oracledb>=3.4.1",
106+
"androguard>=4.1.2",
106107
]
107108

108109
[dependency-groups]

server/mobile.py

Lines changed: 92 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1+
import asyncio
2+
import base64
13
import hashlib
24
import os
5+
import shutil
36
import subprocess
4-
import base64
57
from typing import Literal
6-
import asyncio
78

89
import requests
9-
from fastapi import APIRouter
10+
from androguard.core.apk import APK
11+
from fastapi import APIRouter, UploadFile, File
1012
from pydantic import BaseModel
1113

12-
from Framework.Utilities import ConfigModule, CommonUtil
14+
from Framework.Utilities import CommonUtil, ConfigModule
15+
from settings import ZEUZ_NODE_DOWNLOADS_DIR
1316

1417
ADB_PATH = "adb" # Ensure ADB is in PATH
1518
UI_XML_PATH = "ui.xml"
@@ -29,12 +32,14 @@ class InspectorResponse(BaseModel):
2932

3033
class DeviceInfo(BaseModel):
3134
"""Model for device information."""
35+
3236
serial: str
3337
status: str
3438
name: str | None = None
3539
# model: str | None = None
3640
# product: str | None = None
3741

42+
3843
@router.get("/devices", response_model=list[DeviceInfo])
3944
def get_devices():
4045
"""Get list of connected Android devices."""
@@ -72,29 +77,28 @@ def inspect(device_serial: str | None = None):
7277
capture_screenshot(device_serial=device_serial)
7378

7479
# Read XML file
75-
with open(UI_XML_PATH, 'r') as xml_file:
80+
with open(UI_XML_PATH, "r") as xml_file:
7681
xml_content = xml_file.read()
77-
82+
7883
# Read and encode screenshot
79-
with open(SCREENSHOT_PATH, 'rb') as img_file:
84+
with open(SCREENSHOT_PATH, "rb") as img_file:
8085
screenshot_bytes = img_file.read()
81-
screenshot_base64 = base64.b64encode(screenshot_bytes).decode('utf-8')
82-
86+
screenshot_base64 = base64.b64encode(screenshot_bytes).decode("utf-8")
87+
8388
return InspectorResponse(
84-
status="ok",
85-
ui_xml=xml_content,
86-
screenshot=screenshot_base64
89+
status="ok", ui_xml=xml_content, screenshot=screenshot_base64
8790
)
8891
except Exception as e:
89-
return InspectorResponse(
90-
status="error",
91-
error=str(e)
92-
)
92+
return InspectorResponse(status="error", error=str(e))
93+
9394

9495
@router.get("/dump/driver")
9596
def dump_driver():
9697
"""Dump the current driver."""
97-
from Framework.Built_In_Automation.Mobile.CrossPlatform.Appium.BuiltInFunctions import appium_driver
98+
from Framework.Built_In_Automation.Mobile.CrossPlatform.Appium.BuiltInFunctions import (
99+
appium_driver,
100+
)
101+
98102
if appium_driver is None:
99103
return
100104
return appium_driver.page_source
@@ -103,7 +107,14 @@ def dump_driver():
103107
def run_adb_command(command):
104108
"""Run an ADB command and return the output."""
105109
try:
106-
result = subprocess.run(command, shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
110+
result = subprocess.run(
111+
command,
112+
shell=True,
113+
check=True,
114+
stdout=subprocess.PIPE,
115+
stderr=subprocess.PIPE,
116+
text=True,
117+
)
107118
return result.stdout.strip()
108119
except subprocess.CalledProcessError as e:
109120
return f"Error: {e.stderr.strip()}"
@@ -116,11 +127,14 @@ def capture_ui_dump(device_serial: str | None = None):
116127
f"{ADB_PATH} {device_flag} shell uiautomator dump /sdcard/ui.xml".strip()
117128
)
118129
if out.startswith("Error:"):
119-
from Framework.Built_In_Automation.Mobile.CrossPlatform.Appium.BuiltInFunctions import appium_driver
130+
from Framework.Built_In_Automation.Mobile.CrossPlatform.Appium.BuiltInFunctions import (
131+
appium_driver,
132+
)
133+
120134
if appium_driver is None:
121135
return
122136
page_src = appium_driver.page_source
123-
with open(UI_XML_PATH, 'w') as xml_file:
137+
with open(UI_XML_PATH, "w") as xml_file:
124138
xml_file.write(page_src)
125139
else:
126140
out = run_adb_command(
@@ -137,7 +151,10 @@ def capture_screenshot(device_serial: str | None = None):
137151
f"{ADB_PATH} {device_flag} shell screencap -p /sdcard/screen.png".strip()
138152
)
139153
if out.startswith("Error:"):
140-
from Framework.Built_In_Automation.Mobile.CrossPlatform.Appium.BuiltInFunctions import appium_driver
154+
from Framework.Built_In_Automation.Mobile.CrossPlatform.Appium.BuiltInFunctions import (
155+
appium_driver,
156+
)
157+
141158
if appium_driver is None:
142159
return
143160
full_screenshot_path = os.path.join(os.getcwd(), SCREENSHOT_PATH)
@@ -156,10 +173,16 @@ async def upload_android_ui_dump():
156173
try:
157174
capture_ui_dump()
158175
try:
159-
with open(UI_XML_PATH, 'r') as xml_file:
176+
with open(UI_XML_PATH, "r") as xml_file:
160177
xml_content = xml_file.read()
161-
xml_content = xml_content.replace("<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>", "", 1)
162-
new_xml_hash = hashlib.sha256(xml_content.encode('utf-8')).hexdigest()
178+
xml_content = xml_content.replace(
179+
"<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>",
180+
"",
181+
1,
182+
)
183+
new_xml_hash = hashlib.sha256(
184+
xml_content.encode("utf-8")
185+
).hexdigest()
163186
# Don't upload if the content hasn't changed
164187
if prev_xml_hash == new_xml_hash:
165188
await asyncio.sleep(5)
@@ -169,17 +192,59 @@ async def upload_android_ui_dump():
169192
except FileNotFoundError:
170193
await asyncio.sleep(5)
171194
continue
172-
url = ConfigModule.get_config_value("Authentication", "server_address").strip() + "/node_ai_contents/"
195+
url = (
196+
ConfigModule.get_config_value(
197+
"Authentication", "server_address"
198+
).strip()
199+
+ "/node_ai_contents/"
200+
)
173201
apiKey = ConfigModule.get_config_value("Authentication", "api-key").strip()
174202
res = requests.post(
175203
url,
176204
headers={"X-Api-Key": apiKey},
177205
json={
178206
"dom_mob": {"dom": xml_content},
179-
"node_id": CommonUtil.MachineInfo().getLocalUser().lower()
180-
})
207+
"node_id": CommonUtil.MachineInfo().getLocalUser().lower(),
208+
},
209+
)
181210
if res.ok:
182211
CommonUtil.ExecLog("", "UI dump uploaded successfully", iLogLevel=1)
183212
except Exception as e:
184213
CommonUtil.ExecLog("", f"Error uploading UI dump: {str(e)}", iLogLevel=3)
185214
await asyncio.sleep(5)
215+
216+
217+
@router.post("/apk-upload")
218+
def handle_apk_upload(file: UploadFile = File(...)):
219+
dir_path = f"{ZEUZ_NODE_DOWNLOADS_DIR}/apk"
220+
if not os.path.exists(dir_path):
221+
os.makedirs(dir_path)
222+
filename = file.filename or "uploaded.apk"
223+
file_path = os.path.join(dir_path, filename)
224+
with open(file_path, "wb") as buffer:
225+
shutil.copyfileobj(file.file, buffer)
226+
return {"message": "APK uploaded successfully", "filename": filename}
227+
228+
229+
def get_package_name(file_path: str) -> str | None:
230+
"""Extract package name from APK using androguard."""
231+
try:
232+
apk = APK(file_path)
233+
return apk.get_package()
234+
except Exception:
235+
return None
236+
237+
238+
@router.post("/apk-install")
239+
def handle_apk_install(filename: str, serial: str):
240+
dir_path = f"{ZEUZ_NODE_DOWNLOADS_DIR}/apk"
241+
file_path = os.path.join(dir_path, filename)
242+
if not os.path.exists(file_path):
243+
return {"message": "APK not found", "filename": filename}
244+
package_name = get_package_name(file_path)
245+
try:
246+
subprocess.run([ADB_PATH, "-s", serial, "install", file_path], check=True)
247+
return {"message": "APK installed successfully", "filename": filename, "package_name": package_name}
248+
except Exception as e:
249+
return {"message": f"Error installing APK: {str(e)}", "filename": filename, "package_name": package_name}
250+

0 commit comments

Comments
 (0)