1+ import asyncio
2+ import base64
13import hashlib
24import os
5+ import shutil
36import subprocess
4- import base64
57from typing import Literal
6- import asyncio
78
89import requests
9- from fastapi import APIRouter
10+ from androguard .core .apk import APK
11+ from fastapi import APIRouter , UploadFile , File
1012from 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
1417ADB_PATH = "adb" # Ensure ADB is in PATH
1518UI_XML_PATH = "ui.xml"
@@ -29,12 +32,14 @@ class InspectorResponse(BaseModel):
2932
3033class 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 ])
3944def 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" )
9596def 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():
103107def 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