0

I have the following code, which works perfectly fine on Android 12 and below, but for a weird reason, Android 13 takes "black" screenshots, about 95% of the time.

fun setVirtualDisplay() {
    mImageReader = ImageReader.newInstance(
        deviceScreenUtils.getWidth(),
        deviceScreenUtils.getHeight(),
        PixelFormat.RGBA_8888,
        2
    )

    mImageReader?.let {
        val flags =
            DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY or DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC

        try {
            mVirtualDisplay = mMediaProjection?.createVirtualDisplay(
                "screen-mirror", deviceScreenUtils.getWidth(), deviceScreenUtils.getHeight(),
                deviceScreenUtils.getDensity(), flags, it.surface, null, null
            )
        } catch (e: Throwable) {
            Log.i(TAG, "Media Projection not longer available...")
            mMediaProjectionIntent = null
            mImageReader = null
        }
    }
}

fun takeScreenshot() {
    Log.i(TAG, "Taking screenshot...")
    val handler = Handler(Looper.getMainLooper())
    mImageReader?.setOnImageAvailableListener({ imageReader ->
        val image = imageReader.acquireLatestImage()
        Log.i(TAG, "Acquiring image...")
        if (image != null) {
            val planes = image.planes
            val buffer = planes[0].buffer
            val pixelStride = planes[0].pixelStride
            val rowStride = planes[0].rowStride
            val rowPadding: Int = rowStride - pixelStride * deviceScreenUtils.getWidthPixels()
            val bitmap = Bitmap.createBitmap(
                deviceScreenUtils.getWidth() + (rowPadding.toFloat() / pixelStride.toFloat()).toInt(),
                deviceScreenUtils.getHeight(),
                Bitmap.Config.ARGB_8888
            )
            bitmap.copyPixelsFromBuffer(buffer)
            image.close()

            fileUtil.saveImage(bitmap)?.let {
                lastScreenshot.onNext(it)
            }
        }

        imageReader.setOnImageAvailableListener(null, handler)
        releaseBuffer()
    }, handler)
}

Did something change for this specific functionality? Or do I have something wrong in the code and it has been magically working on previous versions?

RandomGuy
  • 41
  • 5
  • This sounds like the behavior of trying to take a screenshot of an app that has FLAG_SECURE set, so I'm guessing that this would have something to do with permissions or configuration not being correct. – undermark5 Oct 18 '22 at 03:40
  • Hi undermark5, I thought of that option too, but even on the launcher, it takes 1-2 out of 10 screenshots fine, all the other 9 remain black. So I believe the flag might not be the problem. – RandomGuy Oct 18 '22 at 14:09
  • Hmm, does logcat give any additional information? I'm not exactly familiar with what you are trying to do, but it looks like you are effectively "casting" the display into your app and capturing the image, which is how the more modern screen recording apps work, perhaps you can find an open source one and see what they are doing. – undermark5 Oct 18 '22 at 17:18
  • Logcat doesn't say anything, because technically... is taking the screenshot fine, everything is working as expected but... it is all black =/ Found [somebody else](https://stackoverflow.com/questions/74088324/android-13-media-projection-screenshot-is-black) with a similar problem actually – RandomGuy Oct 18 '22 at 21:27
  • Then I'd suggest filing a bug report with Google as it seems that this may be an issue with the system. – undermark5 Oct 19 '22 at 15:52
  • I have been dealing with this issue since the release of android 13, I came to the conclusion that it has to be a bug after rewriting code multiple different ways. – Jayce Oct 25 '22 at 20:47

3 Answers3

2

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
0

Step 1:

private fun captureScreenShot(view: View): Bitmap {
    val returnedBitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
    val canvas = Canvas(returnedBitmap)
    val bgDrawable = view.background
    if (bgDrawable != null) bgDrawable.draw(canvas)
    else canvas.drawColor(Color.WHITE)
    view.draw(canvas)
    return returnedBitmap
}

below step 2 is inside oncreate method step 2:

val btn: Button = findViewById(R.id.btn)
    val image: ImageView =findViewById(R.id.image)

    btn.setOnClickListener{
        val constraintLayout: ConstraintLayout = findViewById(R.id.container)
        val screenshot: Bitmap =captureScreenShot(constraintLayout)
        image.setImageBitmap(screenshot)
    }

Screenshot

helvete
  • 2,455
  • 13
  • 33
  • 37
0
 val bitmap = IntentShareUtils.captureBitmapFromView(binding.root)
 val shareUrl = IntentShareUtils.saveToSharedImage(this, bitmap)
 IntentShareUtils.shareEtc(this, shareUrl)

public static Bitmap captureBitmapFromView(View v) {
    Bitmap b = Bitmap.createBitmap(v.getMeasuredWidth(), v.getMeasuredHeight(), Bitmap.Config.ARGB_8888);
    Canvas c = new Canvas(b);
    v.layout(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
    v.draw(c);
    return b;
}

public static String saveToSharedImage(Context context, Bitmap bitmap) {
    if (bitmap == null) {
        return "";
    }
    File externalFilesDir = context.getExternalFilesDir("share_image");
    if (!externalFilesDir.exists()) {
        externalFilesDir.mkdir();
    }
    File file = new File(externalFilesDir, "share_image.jpg");
    if (file.exists()) {
        file.delete();
    }
    try {
        file.createNewFile();
    } catch (IOException e) {
        e.printStackTrace();
    }
    try {
        OutputStream fileOutputStream = new FileOutputStream(file);
        bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream);
        fileOutputStream.close();
    } catch (IOException e2) {
        e2.printStackTrace();
    }
    String absolutePath = file.getAbsolutePath();

    return absolutePath;
}

public static void shareEtc(Context context, String file_path) {
    Intent intent = new Intent("android.intent.action.SEND");
    intent.setType("image/*");

    Parcelable c;
    if (Build.VERSION.SDK_INT >= 23) {
        c = getUri(context, file_path);
    } else {
        c = getUri(file_path);
    }

    intent.putExtra("android.intent.extra.STREAM", c);
    context.startActivity(Intent.createChooser(intent, "Share to"));
}
Kaan Koca
  • 75
  • 1
  • 11