@ -44,10 +44,8 @@ class LongScreenshotCapture(
private const val MAX_SCREENSHOTS = 50
private const val MAX_SCREENSHOTS = 50
}
}
// Core components
// Core components
private var mediaProjection : MediaProjection ? = null
private var mediaProjection : MediaProjection ? = null
private var virtualDisplay : VirtualDisplay ? = null
private var imageReader : ImageReader ? = null
// Screen dimensions
// Screen dimensions
private var screenWidth = 0
private var screenWidth = 0
@ -75,7 +73,7 @@ class LongScreenshotCapture(
private var errorCallback : ( ( error : String ) -> Unit ) ? = null
private var errorCallback : ( ( error : String ) -> Unit ) ? = null
/ * *
/ * *
* Initialize the long screenshot system with existing MediaProjection
* Initialize the long screenshot system ( lightweight - no VirtualDisplay needed )
* /
* /
fun initialize ( mediaProjection : MediaProjection , screenWidth : Int , screenHeight : Int , screenDensity : Int ) : Boolean
fun initialize ( mediaProjection : MediaProjection , screenWidth : Int , screenHeight : Int , screenDensity : Int ) : Boolean
{
{
@ -94,31 +92,12 @@ class LongScreenshotCapture(
this . screenHeight = screenHeight
this . screenHeight = screenHeight
this . screenDensity = screenDensity
this . screenDensity = screenDensity
// Create dedicated ImageReader for long screenshots
// Note: We don't create our own VirtualDisplay/ImageReader since Android doesn't allow
imageReader = ImageReader . newInstance ( screenWidth , screenHeight , PixelFormat . RGBA_8888 , BUFFER_COUNT )
// multiple VirtualDisplays from the same MediaProjection. Instead, we'll capture
imageReader ?. setOnImageAvailableListener ( onImageAvailableListener , handler )
// screenshots by requesting them from the existing screen capture system.
// Create dedicated VirtualDisplay for long screenshots
virtualDisplay = mediaProjection . createVirtualDisplay (
" LongScreenshotCapture " ,
screenWidth ,
screenHeight ,
screenDensity ,
DisplayManager . VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR ,
imageReader ?. surface ,
null ,
handler
)
if ( virtualDisplay == null )
{
PGHLog . e ( TAG , " ❌ Failed to create VirtualDisplay for long screenshots " )
cleanup ( )
return false
}
isInitialized . set ( true )
isInitialized . set ( true )
PGHLog . i ( TAG , " ✅ Long screenshot capture system initialized successfully " )
PGHLog . i ( TAG , " ✅ Long screenshot capture system initialized successfully (lightweight mode) " )
return true
return true
}
}
@ -172,8 +151,9 @@ class LongScreenshotCapture(
/ * *
/ * *
* Capture a single frame manually ( on - demand )
* Capture a single frame manually ( on - demand )
* This will be called by the service which will provide the actual image data
* /
* /
fun captureFrame ( ) : Boolean
fun captureFrame ( imageData : Bitmap ? ) : Boolean
{
{
if ( !is Capturing . get ( ) )
if ( !is Capturing . get ( ) )
{
{
@ -190,20 +170,21 @@ class LongScreenshotCapture(
return try
return try
{
{
PGHLog . d ( TAG , " 📸 Trigger ing manual frame capture " )
PGHLog . d ( TAG , " 📸 Process ing manual frame capture " )
// Force a capture by accessing the ImageReader
if ( imageData != null )
// The VirtualDisplay should automatically provide images to the ImageReader
{
imageReader ?. acquireLatestImage ( ) ?. let { image ->
PGHLog . d ( TAG , " 📸 Image data received, processing... " )
PGHLog . d ( TAG , " 📸 Image acquired from ImageReader, processing... " )
// Process the image in a coroutine
// Process the bitmap in a coroutine
captureScope . launch {
captureScope . launch {
processImage Async ( image )
processBitmap Async ( imageData )
}
}
return true
return true
} ?: run {
}
PGHLog . w ( TAG , " ⚠️ No image available from ImageReader " )
else
{
PGHLog . w ( TAG , " ⚠️ No image data provided " )
return false
return false
}
}
@ -304,27 +285,7 @@ class LongScreenshotCapture(
this . errorCallback = callback
this . errorCallback = callback
}
}
private val onImageAvailableListener = ImageReader . OnImageAvailableListener { reader ->
private suspend fun processBitmapAsync ( bitmap : Bitmap ) = withContext ( Dispatchers . IO )
if ( !is Capturing . get ( ) ) return @OnImageAvailableListener
try
{
val image = reader . acquireLatestImage ( )
if ( image != null )
{
captureScope . launch {
processImageAsync ( image )
}
}
}
catch ( e : Exception )
{
PGHLog . e ( TAG , " ❌ Error in onImageAvailableListener " , e )
errorCallback ?. invoke ( " Image capture failed: ${e.message} " )
}
}
private suspend fun processImageAsync ( image : Image ) = withContext ( Dispatchers . IO )
{
{
try
try
{
{
@ -332,89 +293,37 @@ class LongScreenshotCapture(
val filename = " screenshot_ ${timestamp} .png "
val filename = " screenshot_ ${timestamp} .png "
val file = File ( storageDir , filename )
val file = File ( storageDir , filename )
// Convert image to bitmap and save
// Save bitmap to file
val bitmap = convertImageToBitmap ( image )
saveBitmapToFile ( bitmap , file )
if ( bitmap != null )
{
saveBitmapToFile ( bitmap , file )
val screenshot = CapturedScreenshot (
id = timestamp ,
filename = filename ,
filePath = file . absolutePath ,
timestamp = timestamp ,
width = screenWidth ,
height = screenHeight
)
capturedScreenshots . offer ( screenshot )
val count = screenshotCount . incrementAndGet ( )
PGHLog . i ( TAG , " 📸 Screenshot # $count captured: $filename " )
// Notify progress on main thread
handler . post {
progressCallback ?. invoke ( count )
}
bitmap . recycle ( )
}
else
{
PGHLog . e ( TAG , " ❌ Failed to convert image to bitmap " )
errorCallback ?. invoke ( " Failed to process screenshot " )
}
}
val screenshot = CapturedScreenshot (
catch ( e : Exception )
id = timestamp ,
{
filename = filename ,
PGHLog . e ( TAG , " ❌ Error processing image " , e )
filePath = file . absolutePath ,
errorCallback ?. invoke ( " Failed to save screenshot: ${e.message} " )
timestamp = timestamp ,
}
width = bitmap . width ,
finally
height = bitmap . height
{
image . close ( )
}
}
private fun convertImageToBitmap ( image : Image ) : Bitmap ?
{
return try
{
val planes = image . planes
val buffer = planes [ 0 ] . buffer
val pixelStride = planes [ 0 ] . pixelStride
val rowStride = planes [ 0 ] . rowStride
val rowPadding = rowStride - pixelStride * screenWidth
val bitmap = Bitmap . createBitmap (
screenWidth + rowPadding / pixelStride ,
screenHeight ,
Bitmap . Config . ARGB_8888
)
)
bitmap . copyPixelsFromBuffer ( buffer )
capturedScreenshots . offer ( screenshot )
val count = screenshotCount . incrementAndGet ( )
// Crop if there's padding
PGHLog . i ( TAG , " 📸 Screenshot # $count captured: $filename " )
if ( rowPadding == 0 )
{
// Notify progress on main thread
bitmap
handler . post {
}
progressCallback ?. invoke ( count )
else
{
val croppedBitmap = Bitmap . createBitmap ( bitmap , 0 , 0 , screenWidth , screenHeight )
bitmap . recycle ( )
croppedBitmap
}
}
}
}
catch ( e : Exception )
catch ( e : Exception )
{
{
PGHLog . e ( TAG , " ❌ Error converting image to bitmap " , e )
PGHLog . e ( TAG , " ❌ Error processing bitmap " , e )
null
errorCallback ?. invoke ( " Failed to save screenshot: ${e.message} " )
}
}
}
}
private fun saveBitmapToFile ( bitmap : Bitmap , file : File )
private fun saveBitmapToFile ( bitmap : Bitmap , file : File )
{
{
FileOutputStream ( file ) . use { stream ->
FileOutputStream ( file ) . use { stream ->
@ -454,10 +363,6 @@ class LongScreenshotCapture(
// Cancel any ongoing coroutines
// Cancel any ongoing coroutines
captureScope . cancel ( )
captureScope . cancel ( )
// Clean up capture resources
virtualDisplay ?. release ( )
imageReader ?. close ( )
// Clear collections
// Clear collections
capturedScreenshots . clear ( )
capturedScreenshots . clear ( )
screenshotCount . set ( 0 )
screenshotCount . set ( 0 )
@ -466,8 +371,6 @@ class LongScreenshotCapture(
clearStoredScreenshots ( )
clearStoredScreenshots ( )
// Clear references
// Clear references
virtualDisplay = null
imageReader = null
mediaProjection = null
mediaProjection = null
progressCallback = null
progressCallback = null
errorCallback = null
errorCallback = null