1

I am having an error with the media projection and taking screenshots but only with android 13, sometimes they come out black, but not always. I have tried to put a delay (up to 5 seconds) to see if maybe the android system was the cause of it, but it still happens, any help is appreciated. I did search the site, but nothing comes up with android 13 issue.

WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
windowManager.getDefaultDisplay().getRealSize(size);
final int width = size.x, height = size.y;

final ImageReader imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 1);

imageReader.setOnImageAvailableListener(reader -> {
    //-> Stop our media projection just in case it is running
    mediaProjection.stop();

    Image image = reader.acquireLatestImage();
    if (image != null){
      Image.Plane[] planes = image.getPlanes();
      ByteBuffer buffer = planes[0].getBuffer();
      int pixelStride = planes[0].getPixelStride(), rowStride = planes[0].getRowStride(), rowPadding = rowStride - pixelStride * width;
      bitmap = Bitmap.createBitmap(width + rowPadding / pixelStride, height, Bitmap.Config.ARGB_8888);
      bitmap.copyPixelsFromBuffer(buffer); 

      String fileName = "Screenshot_" + System.currentTimeMillis() + ".jpg";
      String destinationPath = this.getExternalFilesDir(null) + "/screenshots/" + fileName;

      File imageFile = new File(destinationPath);
      FileOutputStream outputStream = new FileOutputStream(imageFile);
      int quality = 100;
      bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream);
      outputStream.flush();
      outputStream.close();

      String mediaPath = Environment.DIRECTORY_PICTURES + File.separator + "Screenshots/myapp" + File.separator;

      ContentValues values = new ContentValues();
      values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis());
      values.put(MediaStore.Images.Media.IS_PENDING, 0);
      values.put(MediaStore.Images.Media.RELATIVE_PATH, mediaPath);
      values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
      values.put(MediaStore.Images.Media.SIZE, imageFile.length());
      values.put(MediaStore.Images.Media.DISPLAY_NAME, fileName);
      Uri path = this.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);

      OutputStream imageOutStream = this.getContentResolver().openOutputStream(path);

      bitmap.compress(Bitmap.CompressFormat.JPEG, 100, imageOutStream);
      if (imageOutStream != null) {
         imageOutStream.flush();
         imageOutStream.close();
      }

      if (image != null) { image.close(); }
      mediaProjection.stop();
      if (reader != null){ reader.close(); }
    }
}, null);
Jayce
  • 781
  • 3
  • 16
  • 35
  • First, read the API change of android 13, check the related change if has. Then, the code here is good to debug, you should use `log` to show some break point, Ex: the exception when flush or close stream, the `path` result of `insert` method, or even the image, bitmap value... I think we can found the problem somewhere here. – NamNH Oct 26 '22 at 06:40
  • 1
    This problem also causes by the Emulator device(*If you test your app in an emulator*). I was face this issue when I tried my app to take screenshots in the android 13 `emulator`. I suggest you try your app in *Real Mobile Device* of android 13. Maybe this issue is also the same as mine. – M DEV Oct 27 '22 at 06:20
  • @MDEV this is being tested on a real device, I never use an emulator because of those issues. It appears it is just a bug with android 13 as other developers are also having this issue with their apps. – Jayce Nov 01 '22 at 13:57

1 Answers1

6

In my testing and observation, there're two issues when using ImageReader with media projection on Android 13

First, the callback result of setOnImageAvailableListener sometimes returns buffer with empty pixels. So we can just wait for the next image until we get a non-empty bitmap

imageReader.setOnImageAvailableListener({ imageReader ->
    val image = imageReader.acquireLatestImage()
    // ... get buffer here
    val bitmap = Bitmap.createBitmap(screenWidth + rowPadding / pixelStride, screenHeight, Bitmap.Config.ARGB_8888)
    bitmap.copyPixelsFromBuffer(buffer)

    // IMPORTANT!
    val isEmptyBitmap = bitmap.isEmptyBitmap()
    if (isEmptyBitmap) {
        // don't stop the listener and let the imageReader continue to run so that we can get next round of image buffer
    } else {
        // save bitmap to file
    }

    }, Handler(Looper.getMainLooper()))

the isEmptyBitmap() is just an extension of Bitmap:

fun Bitmap.isEmptyBitmap(): Boolean {
    val emptyBitmap = Bitmap.createBitmap(width, height, config)
    return this.sameAs(emptyBitmap)
}

The second issue is that, the OnImageAvailableListener sometimes doesn't even callback! In this case, I set a timeout to wait for the result, and when timed-out, recreate the VirtualDisplay object from the same MediaProjection instance and it just works.

I'm using kotlin coroutine so the code snippet may look like:

retry(10) {
    val imageVirtualDisplay = createVirtualDisplay(...)
    try {
        withTimeout(100) {
            // awaitImageAvailable will suspend here to get the non-empty bitmap
            val bitmap = imageReader.awaitImageAvailable(screenWidth, screenHeight)
            // save bitmap to file
        }
    } finally {
        Timber.d("screenshot imageVirtualDisplay release")
        imageVirtualDisplay?.release()
    }
}

the implementation of awaitImageAvailable:

suspend fun ImageReader.awaitImageAvailable(screenWidth: Int, screenHeight: Int): Bitmap =
suspendCancellableCoroutine { cont ->
    setOnImageAvailableListener({ imageReader ->

        val image = imageReader.acquireLatestImage() ?: throw Error("get screen image result failed")

        val planes = image.planes
        val buffer = planes[0].buffer

        val pixelStride = planes[0].pixelStride
        val rowStride = planes[0].rowStride
        val rowPadding: Int = rowStride - pixelStride * screenWidth
        var bitmap =
            Bitmap.createBitmap(screenWidth + rowPadding / pixelStride, screenHeight, Bitmap.Config.ARGB_8888)
        bitmap.copyPixelsFromBuffer(buffer)
        bitmap = Bitmap.createBitmap(bitmap, 0, 0, screenWidth, screenHeight)
        image.close()

        if (!bitmap.isEmptyBitmap()) {
            Timber.d("image not empty")
            setOnImageAvailableListener(null, null)
            cont.resume(bitmap)
        } else {
            Timber.w("image empty")
        }
    }, Handler(Looper.getMainLooper()))

    cont.invokeOnCancellation { setOnImageAvailableListener(null, null) }
}
diousk
  • 634
  • 1
  • 8
  • 13