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. 28
      app/src/main/java/com/quillstudios/pokegoalshelper/ScreenCaptureService.kt
  2. 173
      app/src/main/java/com/quillstudios/pokegoalshelper/capture/LongScreenshotCapture.kt

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

@ -1318,10 +1318,30 @@ class ScreenCaptureService : Service() {
{ {
PGHLog.d(TAG, "📸 Capturing long screenshot frame") PGHLog.d(TAG, "📸 Capturing long screenshot frame")
val captured = longScreenshotCapture?.captureFrame() ?: false // Get the current image from the existing screen capture system
if (!captured) latestImage?.let { image ->
{ // Convert the image to Mat first, then to Bitmap
PGHLog.w(TAG, "⚠️ Failed to capture long screenshot frame") 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")
} }
} }

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

@ -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 (!isCapturing.get()) if (!isCapturing.get())
{ {
@ -190,20 +170,21 @@ class LongScreenshotCapture(
return try return try
{ {
PGHLog.d(TAG, "📸 Triggering manual frame capture") PGHLog.d(TAG, "📸 Processing 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 {
processImageAsync(image) processBitmapAsync(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 (!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)
{ {
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

Loading…
Cancel
Save