Skip to content

Commit 3ee1e3a

Browse files
Fix: Prevent continuous screenshots by on-demand VirtualDisplay
The OnImageAvailableListener was being triggered continuously because the VirtualDisplay and ImageReader remained active. This commit modifies the takeScreenshot() method in ScreenCaptureService.kt to: - Create VirtualDisplay and ImageReader instances only when a screenshot is requested. - Release these resources immediately after a screenshot is captured. - Keep the MediaProjection session active throughout this process. This ensures that resources are utilized efficiently and prevents the continuous generation of screenshots. Additionally, the log message for the initial screenshot delay in the startCapture() method has been verified to be as specified.
1 parent 4e7e8a9 commit 3ee1e3a

1 file changed

Lines changed: 118 additions & 128 deletions

File tree

app/src/main/kotlin/com/google/ai/sample/ScreenCaptureService.kt

Lines changed: 118 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ class ScreenCaptureService : Service() {
5252
private var virtualDisplay: VirtualDisplay? = null
5353
private var imageReader: ImageReader? = null
5454
private var isReady = false // Flag to indicate if MediaProjection is set up and active
55-
private var shouldTakeScreenshot = false // Flag to control when to actually capture
5655

5756
// Callback for MediaProjection
5857
private val mediaProjectionCallback = object : MediaProjection.Callback() {
@@ -188,133 +187,124 @@ class ScreenCaptureService : Service() {
188187
}
189188
}
190189

191-
private fun takeScreenshot() {
192-
if (!isReady || mediaProjection == null) {
193-
Log.e(TAG, "Cannot take screenshot - service not ready or mediaProjection is null. isReady=$isReady, mediaProjectionIsNull=${mediaProjection == null}")
194-
return
195-
}
196-
Log.d(TAG, "takeScreenshot: Preparing to capture.")
197-
198-
// Set flag to capture on next frame
199-
shouldTakeScreenshot = true
200-
201-
try {
202-
// Check if we need to initialize VirtualDisplay and ImageReader
203-
if (virtualDisplay == null || imageReader == null) {
204-
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
205-
val displayMetrics = DisplayMetrics()
206-
207-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
208-
val defaultDisplay = windowManager.defaultDisplay
209-
if (defaultDisplay != null) {
210-
defaultDisplay.getRealMetrics(displayMetrics)
211-
} else {
212-
val bounds = windowManager.currentWindowMetrics.bounds
213-
displayMetrics.widthPixels = bounds.width()
214-
displayMetrics.heightPixels = bounds.height()
215-
displayMetrics.densityDpi = resources.displayMetrics.densityDpi
216-
}
217-
} else {
218-
@Suppress("DEPRECATION")
219-
windowManager.defaultDisplay.getMetrics(displayMetrics)
220-
}
221-
222-
val width = displayMetrics.widthPixels
223-
val height = displayMetrics.heightPixels
224-
val density = displayMetrics.densityDpi
225-
226-
if (width <= 0 || height <= 0) {
227-
Log.e(TAG, "Invalid display dimensions: ${width}x${height}. Cannot create ImageReader.")
228-
shouldTakeScreenshot = false
229-
return
230-
}
231-
Log.d(TAG, "Display dimensions: ${width}x${height}, density: $density")
232-
233-
imageReader?.close() // Close previous reader if any
234-
virtualDisplay?.release() // Release previous display if any
235-
236-
imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 1)
237-
val localImageReader = imageReader ?: run {
238-
Log.e(TAG, "ImageReader is null after creation attempt.")
239-
shouldTakeScreenshot = false
240-
return
241-
}
242-
243-
localImageReader.setOnImageAvailableListener({ reader ->
244-
// Only process image if screenshot was requested
245-
if (!shouldTakeScreenshot) {
246-
reader.acquireLatestImage()?.close()
247-
return@setOnImageAvailableListener
248-
}
249-
250-
var image: android.media.Image? = null
251-
try {
252-
image = reader.acquireLatestImage()
253-
if (image != null) {
254-
val planes = image.planes
255-
val buffer = planes[0].buffer
256-
val pixelStride = planes[0].pixelStride
257-
val rowStride = planes[0].rowStride
258-
val rowPadding = rowStride - pixelStride * width
259-
260-
val bitmap = Bitmap.createBitmap(
261-
width + rowPadding / pixelStride,
262-
height,
263-
Bitmap.Config.ARGB_8888
264-
)
265-
bitmap.copyPixelsFromBuffer(buffer)
266-
Log.d(TAG, "Bitmap created, proceeding to save.")
267-
saveScreenshot(bitmap)
268-
269-
// Reset flag after capturing
270-
shouldTakeScreenshot = false
271-
} else {
272-
Log.w(TAG, "acquireLatestImage returned null.")
273-
}
274-
} catch (e: Exception) {
275-
Log.e(TAG, "Error processing image", e)
276-
shouldTakeScreenshot = false
277-
} finally {
278-
image?.close()
279-
Log.d(TAG, "Screenshot captured, keeping resources for reuse.")
280-
}
281-
}, Handler(Looper.getMainLooper()))
282-
283-
virtualDisplay = mediaProjection?.createVirtualDisplay(
284-
"ScreenCapture",
285-
width, height, density,
286-
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
287-
localImageReader.surface,
288-
object : VirtualDisplay.Callback() {
289-
override fun onPaused() { Log.d(TAG, "VirtualDisplay paused") }
290-
override fun onResumed() { Log.d(TAG, "VirtualDisplay resumed") }
291-
override fun onStopped() { Log.d(TAG, "VirtualDisplay stopped") }
292-
},
293-
Handler(Looper.getMainLooper())
294-
)
295-
296-
if (virtualDisplay == null) {
297-
Log.e(TAG, "Failed to create VirtualDisplay.")
298-
localImageReader.close()
299-
this.imageReader = null
300-
shouldTakeScreenshot = false
301-
return
302-
}
303-
Log.d(TAG, "VirtualDisplay and ImageReader initialized for reuse.")
304-
} else {
305-
// Resources already exist, flag is set, listener will capture on next frame
306-
Log.d(TAG, "Using existing VirtualDisplay and ImageReader, screenshot will be captured on next frame.")
307-
}
308-
309-
} catch (e: Exception) {
310-
Log.e(TAG, "Error in takeScreenshot setup", e)
311-
shouldTakeScreenshot = false
312-
virtualDisplay?.release()
313-
virtualDisplay = null
314-
imageReader?.close()
315-
imageReader = null
316-
}
317-
}
190+
private fun takeScreenshot() {
191+
if (!isReady || mediaProjection == null) {
192+
Log.e(TAG, "Cannot take screenshot - service not ready or mediaProjection is null. isReady=$isReady, mediaProjectionIsNull=${mediaProjection == null}")
193+
return
194+
}
195+
Log.d(TAG, "takeScreenshot: Preparing to capture.")
196+
197+
try {
198+
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
199+
val displayMetrics = DisplayMetrics()
200+
201+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
202+
val defaultDisplay = windowManager.defaultDisplay
203+
if (defaultDisplay != null) {
204+
defaultDisplay.getRealMetrics(displayMetrics)
205+
} else {
206+
val bounds = windowManager.currentWindowMetrics.bounds
207+
displayMetrics.widthPixels = bounds.width()
208+
displayMetrics.heightPixels = bounds.height()
209+
displayMetrics.densityDpi = resources.displayMetrics.densityDpi
210+
}
211+
} else {
212+
@Suppress("DEPRECATION")
213+
windowManager.defaultDisplay.getMetrics(displayMetrics)
214+
}
215+
216+
val width = displayMetrics.widthPixels
217+
val height = displayMetrics.heightPixels
218+
val density = displayMetrics.densityDpi
219+
220+
if (width <= 0 || height <= 0) {
221+
Log.e(TAG, "Invalid display dimensions: ${width}x${height}. Cannot create ImageReader.")
222+
return
223+
}
224+
Log.d(TAG, "Display dimensions: ${width}x${height}, density: $density")
225+
226+
// Always close previous resources before creating new ones
227+
virtualDisplay?.release()
228+
virtualDisplay = null
229+
imageReader?.close()
230+
imageReader = null
231+
232+
// Create new ImageReader for this screenshot
233+
imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 1)
234+
val localImageReader = imageReader ?: run {
235+
Log.e(TAG, "ImageReader is null after creation attempt.")
236+
return
237+
}
238+
239+
// Create VirtualDisplay for this screenshot
240+
virtualDisplay = mediaProjection?.createVirtualDisplay(
241+
"ScreenCapture",
242+
width, height, density,
243+
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
244+
localImageReader.surface,
245+
object : VirtualDisplay.Callback() {
246+
override fun onPaused() { Log.d(TAG, "VirtualDisplay paused") }
247+
override fun onResumed() { Log.d(TAG, "VirtualDisplay resumed") }
248+
override fun onStopped() { Log.d(TAG, "VirtualDisplay stopped") }
249+
},
250+
Handler(Looper.getMainLooper())
251+
)
252+
253+
if (virtualDisplay == null) {
254+
Log.e(TAG, "Failed to create VirtualDisplay.")
255+
localImageReader.close()
256+
this.imageReader = null
257+
return
258+
}
259+
260+
// Set up one-shot image capture
261+
localImageReader.setOnImageAvailableListener({ reader ->
262+
var image: android.media.Image? = null
263+
try {
264+
image = reader.acquireLatestImage()
265+
if (image != null) {
266+
val planes = image.planes
267+
val buffer = planes[0].buffer
268+
val pixelStride = planes[0].pixelStride
269+
val rowStride = planes[0].rowStride
270+
val rowPadding = rowStride - pixelStride * width
271+
272+
val bitmap = Bitmap.createBitmap(
273+
width + rowPadding / pixelStride,
274+
height,
275+
Bitmap.Config.ARGB_8888
276+
)
277+
bitmap.copyPixelsFromBuffer(buffer)
278+
Log.d(TAG, "Bitmap created, proceeding to save.")
279+
saveScreenshot(bitmap)
280+
} else {
281+
Log.w(TAG, "acquireLatestImage returned null.")
282+
}
283+
} catch (e: Exception) {
284+
Log.e(TAG, "Error processing image", e)
285+
} finally {
286+
image?.close()
287+
288+
// Clean up VirtualDisplay and ImageReader after screenshot
289+
// BUT keep MediaProjection active
290+
Log.d(TAG, "Cleaning up VirtualDisplay and ImageReader (MediaProjection remains active)")
291+
this.virtualDisplay?.release()
292+
this.virtualDisplay = null
293+
reader.close()
294+
if (this.imageReader == reader) {
295+
this.imageReader = null
296+
}
297+
}
298+
}, Handler(Looper.getMainLooper()))
299+
300+
} catch (e: Exception) {
301+
Log.e(TAG, "Error in takeScreenshot setup", e)
302+
virtualDisplay?.release()
303+
virtualDisplay = null
304+
imageReader?.close()
305+
imageReader = null
306+
}
307+
}
318308

319309
private fun saveScreenshot(bitmap: Bitmap) {
320310
try {

0 commit comments

Comments
 (0)