Skip to content

Commit 9a379ce

Browse files
committed
feat: Enhance Samsung emulator support and system image preparation
- Updated FingerprintSpoofer to handle root access more gracefully and improved reboot handling for Samsung props. - Added comprehensive Samsung properties in firmware extractor for better device emulation. - Improved SQLite connection settings to enhance performance and concurrency. - Adjusted WebSocket configuration in frontend to align with new API server port. - Updated Vite configuration to reflect new WebSocket server settings. - Introduced Samsung skin manager to auto-generate AVD skin directories and download official skins. - Implemented Samsung system image preparation to extract and configure Samsung One UI system images for the emulator.
1 parent 2168e0b commit 9a379ce

13 files changed

Lines changed: 1408 additions & 76 deletions

File tree

backend/api/routes/devices.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -497,12 +497,12 @@ async def start_device(
497497

498498
device.status = DeviceStatus.booting
499499
device.adb_serial = None
500-
await db.flush()
500+
await _log_operation(db, device_id, current_user.id, "start_device", OperationStatus.pending, "Starting emulator")
501+
await db.commit()
501502
await db.refresh(device)
502503
await ws_manager.broadcast(device_id, {"type": "status", "status": "booting", "device_id": device_id})
503504

504505
background_tasks.add_task(_start_device_background, device_id)
505-
await _log_operation(db, device_id, current_user.id, "start_device", OperationStatus.pending, "Starting emulator")
506506
return device
507507

508508

@@ -535,7 +535,14 @@ async def _start_device_background(device_id: int):
535535
)
536536
logger.info(f"Will inject {len(samsung_boot_props)} Samsung props at boot for device {device_id}")
537537

538-
success, info = await emulator_manager.start(device, samsung_props=samsung_boot_props)
538+
# Wipe data only on first start (status=created). Subsequent starts preserve
539+
# overlayfs Samsung build.prop patches stored in /data/overlayfs/.
540+
is_first_start = device.status == DeviceStatus.created
541+
success, info = await emulator_manager.start(
542+
device,
543+
samsung_props=samsung_boot_props,
544+
wipe_data=is_first_start,
545+
)
539546
adb_serial = None
540547
console_port = None
541548
if success:
@@ -657,10 +664,10 @@ async def restart_device(
657664
device.status = DeviceStatus.booting
658665
device.pid = None
659666
device.adb_serial = None
660-
await db.flush()
667+
await _log_operation(db, device_id, current_user.id, "restart_device", OperationStatus.pending)
668+
await db.commit()
661669
await db.refresh(device)
662670
background_tasks.add_task(_start_device_background, device_id)
663-
await _log_operation(db, device_id, current_user.id, "restart_device", OperationStatus.pending)
664671
return device
665672

666673

backend/api/routes/firmware_route.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,3 +531,117 @@ async def sync_firmware_entries(
531531

532532
await db.commit()
533533
return {"success": True, "imported": imported}
534+
535+
536+
# ── Samsung System Image preparation ─────────────────────────────────────────
537+
538+
class SystemImagePrepRequest(BaseModel):
539+
firmware_entry_id: int
540+
device_id: Optional[int] = None # if set, start device with the prepared system image
541+
542+
543+
@router.post("/prepare-system-image")
544+
async def prepare_samsung_system_image(
545+
req: SystemImagePrepRequest,
546+
db: AsyncSession = Depends(get_db),
547+
_user: User = Depends(get_current_user),
548+
) -> Dict[str, Any]:
549+
"""
550+
Extract Samsung One UI system.img from AP firmware and prepare it for AVD use.
551+
552+
Pipeline:
553+
AP_*.tar.md5 → super.img → system.img (+ vendor.img)
554+
OR: extract Samsung APKs directly for sideloading.
555+
556+
Returns paths to prepared images and list of extractable Samsung APKs.
557+
If device_id is provided, stores the system image path on the device record
558+
so the next start uses it via -system flag.
559+
"""
560+
from core.firmware.system_image_prep import SamsungSystemImagePrep
561+
562+
entry_row = await db.execute(select(FirmwareEntry).where(FirmwareEntry.id == req.firmware_entry_id))
563+
entry = entry_row.scalar_one_or_none()
564+
if not entry:
565+
raise HTTPException(404, "Firmware entry not found")
566+
567+
fw_dir = Path(settings.FIRMWARE_PACKAGES_DIR)
568+
if entry.filename:
569+
tar_path = fw_dir / entry.filename
570+
firmware_dir = tar_path.parent if tar_path.exists() else fw_dir
571+
else:
572+
firmware_dir = fw_dir
573+
574+
work_dir = firmware_dir / "_prep" / (entry.device_model or "unknown")
575+
prep = SamsungSystemImagePrep(firmware_dir=firmware_dir, work_dir=work_dir)
576+
577+
result = await prep.prepare()
578+
579+
# If APK-only (no simg2img/lpunpack), try lighter extraction
580+
if result["mode"] == "none" or not result["apks"]:
581+
apks = await prep.extract_apks_only()
582+
if apks:
583+
result["apks"] = apks
584+
if result["mode"] == "none":
585+
result["mode"] = "apk_sideload"
586+
587+
return {
588+
"firmware_entry_id": req.firmware_entry_id,
589+
"device_model": entry.device_model,
590+
"mode": result["mode"],
591+
"system_img": result["system_img"],
592+
"vendor_img": result["vendor_img"],
593+
"boot_img": result["boot_img"],
594+
"apks": [
595+
{"name": a["name"], "package": a["package"], "path": a["path"]}
596+
for a in result["apks"]
597+
],
598+
"apks_count": len(result["apks"]),
599+
"error": result["error"],
600+
"tools_available": {
601+
"simg2img": bool(__import__("shutil").which("simg2img")),
602+
"lpunpack": bool(__import__("shutil").which("lpunpack")),
603+
"lz4": bool(__import__("shutil").which("lz4")),
604+
"debugfs": bool(__import__("shutil").which("debugfs")),
605+
},
606+
}
607+
608+
609+
@router.post("/install-samsung-apks/{device_id}")
610+
async def install_samsung_apks_on_device(
611+
device_id: int,
612+
apk_paths: List[str],
613+
db: AsyncSession = Depends(get_db),
614+
_user: User = Depends(get_current_user),
615+
) -> Dict[str, Any]:
616+
"""
617+
Sideload extracted Samsung APKs onto a running device.
618+
apk_paths: list of absolute paths returned by prepare-system-image.
619+
"""
620+
from db.models import Device
621+
from core.tools.adb import ADBTool
622+
623+
device_row = await db.execute(select(Device).where(Device.id == device_id))
624+
device = device_row.scalar_one_or_none()
625+
if not device:
626+
raise HTTPException(404, "Device not found")
627+
if not device.adb_serial:
628+
raise HTTPException(400, "Device is not running (no adb_serial)")
629+
630+
adb = ADBTool()
631+
installed, failed, skipped = [], [], []
632+
633+
for apk_path in apk_paths:
634+
p = Path(apk_path)
635+
if not p.exists():
636+
skipped.append(f"{apk_path}: file not found")
637+
continue
638+
try:
639+
out = await adb._run(["-s", device.adb_serial, "install", "-r", "-t", str(p)], check=False)
640+
if "Success" in (out or ""):
641+
installed.append(p.name)
642+
else:
643+
failed.append(f"{p.name}: {(out or '')[:100]}")
644+
except Exception as e:
645+
failed.append(f"{p.name}: {e}")
646+
647+
return {"installed": installed, "failed": failed, "skipped": skipped}

0 commit comments

Comments
 (0)