Browse Source

fix: resolve MediaProjection VirtualDisplay conflict in LongScreenshotCapture

- Removed separate VirtualDisplay creation to avoid MediaProjection conflict
- Switched to lightweight initialization approach without creating new VirtualDisplay
- Modified captureFrame() to accept Bitmap data from existing screen capture system
- Updated ScreenCaptureService to provide bitmap data via convertImageToMat -> matToBitmap
- Cleaned up unused ImageReader and VirtualDisplay references
- Fixed "Don't re-use the resultData" SecurityException

Key changes:
- LongScreenshotCapture now works as a bitmap processor, not a separate capture system
- Reuses existing MediaProjection infrastructure without conflicts
- Maintains separation of concerns while sharing capture resources efficiently

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
feature/pgh-30-long-screenshot-capture
Dan 5 months ago
parent
commit
e4ecfa6997
  1. 26
      app/src/main/java/com/quillstudios/pokegoalshelper/ScreenCaptureService.kt
  2. 141
      app/src/main/java/com/quillstudios/pokegoalshelper/capture/LongScreenshotCapture.kt

26
app/src/main/java/com/quillstudios/pokegoalshelper/ScreenCaptureService.kt

@ -1318,12 +1318,32 @@ class ScreenCaptureService : Service() {
{
PGHLog.d(TAG, "📸 Capturing long screenshot frame")
val captured = longScreenshotCapture?.captureFrame() ?: false
if (!captured)
{
// Get the current image from the existing screen capture system
latestImage?.let { image ->
// Convert the image to Mat first, then to Bitmap
val mat = convertImageToMat(image)
if (mat != null) {
// Convert Mat to Bitmap
val bitmap = Bitmap.createBitmap(mat.cols(), mat.rows(), Bitmap.Config.ARGB_8888)
Utils.matToBitmap(mat, bitmap)
// Pass the bitmap to the long screenshot system
val captured = longScreenshotCapture?.captureFrame(bitmap) ?: false
if (!captured) {
PGHLog.w(TAG, "⚠️ Failed to capture long screenshot frame")
}
// Clean up resources
bitmap.recycle()
mat.release()
} else {
PGHLog.w(TAG, "⚠️ Failed to convert image to Mat for long screenshot")
}
} ?: run {
PGHLog.w(TAG, "⚠️ No image available for long screenshot capture")
}
}
catch (e: Exception)
{

141
app/src/main/java/com/quillstudios/pokegoalshelper/capture/LongScreenshotCapture.kt

@ -46,8 +46,6 @@ class LongScreenshotCapture(
// Core components
private var mediaProjection: MediaProjection? = null
private var virtualDisplay: VirtualDisplay? = null
private var imageReader: ImageReader? = null
// Screen dimensions
private var screenWidth = 0
@ -75,7 +73,7 @@ class LongScreenshotCapture(
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
{
@ -94,31 +92,12 @@ class LongScreenshotCapture(
this.screenHeight = screenHeight
this.screenDensity = screenDensity
// Create dedicated ImageReader for long screenshots
imageReader = ImageReader.newInstance(screenWidth, screenHeight, PixelFormat.RGBA_8888, BUFFER_COUNT)
imageReader?.setOnImageAvailableListener(onImageAvailableListener, handler)
// 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
}
// Note: We don't create our own VirtualDisplay/ImageReader since Android doesn't allow
// multiple VirtualDisplays from the same MediaProjection. Instead, we'll capture
// screenshots by requesting them from the existing screen capture system.
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
}
@ -172,8 +151,9 @@ class LongScreenshotCapture(
/**
* 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 (!isCapturing.get())
{
@ -190,20 +170,21 @@ class LongScreenshotCapture(
return try
{
PGHLog.d(TAG, "📸 Triggering manual frame capture")
PGHLog.d(TAG, "📸 Processing manual frame capture")
// Force a capture by accessing the ImageReader
// The VirtualDisplay should automatically provide images to the ImageReader
imageReader?.acquireLatestImage()?.let { image ->
PGHLog.d(TAG, "📸 Image acquired from ImageReader, processing...")
if (imageData != null)
{
PGHLog.d(TAG, "📸 Image data received, processing...")
// Process the image in a coroutine
// Process the bitmap in a coroutine
captureScope.launch {
processImageAsync(image)
processBitmapAsync(imageData)
}
return true
} ?: run {
PGHLog.w(TAG, "⚠️ No image available from ImageReader")
}
else
{
PGHLog.w(TAG, "⚠️ No image data provided")
return false
}
@ -304,27 +285,7 @@ class LongScreenshotCapture(
this.errorCallback = callback
}
private val onImageAvailableListener = ImageReader.OnImageAvailableListener { reader ->
if (!isCapturing.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)
private suspend fun processBitmapAsync(bitmap: Bitmap) = withContext(Dispatchers.IO)
{
try
{
@ -332,10 +293,7 @@ class LongScreenshotCapture(
val filename = "screenshot_${timestamp}.png"
val file = File(storageDir, filename)
// Convert image to bitmap and save
val bitmap = convertImageToBitmap(image)
if (bitmap != null)
{
// Save bitmap to file
saveBitmapToFile(bitmap, file)
val screenshot = CapturedScreenshot(
@ -343,8 +301,8 @@ class LongScreenshotCapture(
filename = filename,
filePath = file.absolutePath,
timestamp = timestamp,
width = screenWidth,
height = screenHeight
width = bitmap.width,
height = bitmap.height
)
capturedScreenshots.offer(screenshot)
@ -357,63 +315,14 @@ class LongScreenshotCapture(
progressCallback?.invoke(count)
}
bitmap.recycle()
}
else
{
PGHLog.e(TAG, "❌ Failed to convert image to bitmap")
errorCallback?.invoke("Failed to process screenshot")
}
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error processing image", e)
PGHLog.e(TAG, "❌ Error processing bitmap", e)
errorCallback?.invoke("Failed to save screenshot: ${e.message}")
}
finally
{
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)
// Crop if there's padding
if (rowPadding == 0)
{
bitmap
}
else
{
val croppedBitmap = Bitmap.createBitmap(bitmap, 0, 0, screenWidth, screenHeight)
bitmap.recycle()
croppedBitmap
}
}
catch (e: Exception)
{
PGHLog.e(TAG, "❌ Error converting image to bitmap", e)
null
}
}
private fun saveBitmapToFile(bitmap: Bitmap, file: File)
{
@ -454,10 +363,6 @@ class LongScreenshotCapture(
// Cancel any ongoing coroutines
captureScope.cancel()
// Clean up capture resources
virtualDisplay?.release()
imageReader?.close()
// Clear collections
capturedScreenshots.clear()
screenshotCount.set(0)
@ -466,8 +371,6 @@ class LongScreenshotCapture(
clearStoredScreenshots()
// Clear references
virtualDisplay = null
imageReader = null
mediaProjection = null
progressCallback = null
errorCallback = null

Loading…
Cancel
Save