@@ -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
0 commit comments