Skip to content

Commit d7b7f88

Browse files
committed
feat(android): implement screenshot_region command
Takes a full screenshot, crops to the specified region in screenshot coordinate space, optionally scales down via output_max_width/height. Same params as desktop: min_x, min_y, max_x, max_y, quality, max_width, max_height, output_max_width, output_max_height.
1 parent 20b664c commit d7b7f88

2 files changed

Lines changed: 81 additions & 1 deletion

File tree

android/app/src/main/java/com/doodkin/screenmcp/WebSocketClient.kt

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -693,8 +693,87 @@ class WebSocketClient(
693693
}
694694
}
695695

696+
"screenshot_region" -> {
697+
if (service.isPhoneLocked()) {
698+
sendResponse(ws, id, "error", error = "phone is locked")
699+
return
700+
}
701+
val minX = params?.optDouble("min_x", Double.NaN) ?: Double.NaN
702+
val minY = params?.optDouble("min_y", Double.NaN) ?: Double.NaN
703+
val maxX = params?.optDouble("max_x", Double.NaN) ?: Double.NaN
704+
val maxY = params?.optDouble("max_y", Double.NaN) ?: Double.NaN
705+
if (minX.isNaN() || minY.isNaN() || maxX.isNaN() || maxY.isNaN()) {
706+
sendResponse(ws, id, "error", error = "missing min_x/min_y/max_x/max_y params")
707+
return
708+
}
709+
val quality = params?.optInt("quality", 100) ?: 100
710+
711+
service.takeScreenshot(object : AccessibilityService.TakeScreenshotCallback {
712+
override fun onSuccess(result: AccessibilityService.ScreenshotResult) {
713+
try {
714+
val hwBuffer = result.hardwareBuffer
715+
val colorSpace = result.colorSpace
716+
val bitmap = Bitmap.wrapHardwareBuffer(hwBuffer, colorSpace)
717+
if (bitmap == null) {
718+
sendResponse(ws, id, "error", error = "failed to create bitmap")
719+
return
720+
}
721+
val softBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false)
722+
bitmap.recycle()
723+
hwBuffer.close()
724+
725+
val fullW = softBitmap.width.toDouble()
726+
val fullH = softBitmap.height.toDouble()
727+
728+
// Scale from screenshot space to actual pixels
729+
val mw = params?.optDouble("max_width", DEFAULT_SCALE_WIDTH) ?: DEFAULT_SCALE_WIDTH
730+
val mh = params?.optDouble("max_height", DEFAULT_SCALE_HEIGHT) ?: DEFAULT_SCALE_HEIGHT
731+
val sx = if (mw > 0) fullW / mw else 1.0
732+
val sy = if (mh > 0) fullH / mh else 1.0
733+
734+
val pxMinX = (minX * sx).toInt().coerceIn(0, softBitmap.width - 1)
735+
val pxMinY = (minY * sy).toInt().coerceIn(0, softBitmap.height - 1)
736+
val pxMaxX = (maxX * sx).toInt().coerceIn(0, softBitmap.width)
737+
val pxMaxY = (maxY * sy).toInt().coerceIn(0, softBitmap.height)
738+
739+
val cropW = pxMaxX - pxMinX
740+
val cropH = pxMaxY - pxMinY
741+
if (cropW <= 0 || cropH <= 0) {
742+
softBitmap.recycle()
743+
sendResponse(ws, id, "error", error = "region has zero or negative size")
744+
return
745+
}
746+
747+
var cropped = Bitmap.createBitmap(softBitmap, pxMinX, pxMinY, cropW, cropH)
748+
softBitmap.recycle()
749+
750+
// Only scale down if output_max specified
751+
val outMaxW = params?.optInt("output_max_width", 0) ?: 0
752+
val outMaxH = params?.optInt("output_max_height", 0) ?: 0
753+
if (outMaxW > 0 || outMaxH > 0) {
754+
cropped = service.scaleBitmap(cropped,
755+
if (outMaxW > 0) outMaxW else null,
756+
if (outMaxH > 0) outMaxH else null)
757+
}
758+
759+
val bytes = service.compressToWebP(cropped, quality)
760+
cropped.recycle()
761+
762+
val base64 = Base64.encodeToString(bytes, Base64.NO_WRAP)
763+
sendResponse(ws, id, "ok", JSONObject().put("image", base64))
764+
} catch (e: Exception) {
765+
sendResponse(ws, id, "error", error = "screenshot_region failed: ${e.message}")
766+
}
767+
}
768+
769+
override fun onFailure(errorCode: Int) {
770+
sendResponse(ws, id, "error", error = "screenshot failed: $errorCode")
771+
}
772+
})
773+
}
774+
696775
"hold_key", "release_key", "press_key", "hotkey", "mouse_move",
697-
"screenshot_window", "screenshot_region", "is_elevated", "elevate" -> {
776+
"screenshot_window", "is_elevated", "elevate" -> {
698777
sendResponse(ws, id, "ok", result = JSONObject().put("unsupported", true))
699778
}
700779

docs/implementations.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ The worker relays all commands — it does not interpret them. The matrix below
107107
| `focus_window` | yes | yes | yes | yes | yes | yes | yes | yes |
108108
| `active_window` | yes | yes | yes | yes | yes | yes | yes | yes |
109109
| `screenshot_window` | unsupported | yes | yes | yes | yes | yes | yes | yes |
110+
| `screenshot_region` | yes | yes | yes | yes | yes | yes | yes | yes |
110111

111112
### System (Desktop Only)
112113

0 commit comments

Comments
 (0)