[Android] Make the video screenshots available in the Android photo picker

Nicolas Pomepuy git at videolan.org
Tue Mar 25 15:17:41 UTC 2025


vlc-android | branch: master | Nicolas Pomepuy <nicolas at videolabs.io> | Mon Mar 24 11:39:14 2025 +0100| [fa95814856710611d767223f44a990fc00bb3e93] | committer: Duncan McNamara

Make the video screenshots available in the Android photo picker

Fixes #3176

> https://code.videolan.org/videolan/vlc-android/commit/fa95814856710611d767223f44a990fc00bb3e93
---

 .../src/org/videolan/vlc/gui/helpers/BitmapUtil.kt | 56 +++++++++++++++++++++-
 .../videolan/vlc/gui/video/VideoPlayerActivity.kt  |  2 +-
 2 files changed, 55 insertions(+), 3 deletions(-)

diff --git a/application/vlc-android/src/org/videolan/vlc/gui/helpers/BitmapUtil.kt b/application/vlc-android/src/org/videolan/vlc/gui/helpers/BitmapUtil.kt
index 281daa7c59..5ac42ac36c 100644
--- a/application/vlc-android/src/org/videolan/vlc/gui/helpers/BitmapUtil.kt
+++ b/application/vlc-android/src/org/videolan/vlc/gui/helpers/BitmapUtil.kt
@@ -20,19 +20,33 @@
 
 package org.videolan.vlc.gui.helpers
 
+import android.content.ContentValues
 import android.content.Context
 import android.content.res.Resources
-import android.graphics.*
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import android.graphics.PorterDuffXfermode
+import android.graphics.Rect
+import android.graphics.RectF
 import android.graphics.drawable.BitmapDrawable
 import android.graphics.drawable.Drawable
 import android.graphics.drawable.StateListDrawable
 import android.graphics.drawable.VectorDrawable
 import android.net.Uri
 import android.os.Build
+import android.os.Environment
+import android.provider.MediaStore
 import android.util.Log
 import android.view.View
 import android.view.View.MeasureSpec
 import androidx.annotation.DrawableRes
+import androidx.annotation.RequiresApi
+import androidx.annotation.WorkerThread
 import androidx.appcompat.content.res.AppCompatResources
 import androidx.core.content.ContextCompat
 import androidx.core.graphics.drawable.DrawableCompat
@@ -50,6 +64,7 @@ import java.io.ByteArrayOutputStream
 import java.io.File
 import java.io.FileOutputStream
 import java.io.IOException
+import java.io.OutputStream
 import java.text.DecimalFormat
 
 
@@ -310,7 +325,9 @@ object BitmapUtil {
         else -> Rect(0, 0, width, height)
     }
 
-    fun saveOnDisk(bitmap: Bitmap, destPath: String):Boolean {
+    fun saveOnDisk(bitmap: Bitmap, destPath: String, publish: Boolean = false, context: Context? = null):Boolean {
+        if (publish && context == null) throw IllegalStateException("Cannot publish image without context")
+        if (publish && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) return saveImageInQ(context!!, bitmap, File(destPath).name)
         val destFile = File(destPath)
         return when {
             destFile.parentFile?.canWrite() == true -> {
@@ -319,6 +336,8 @@ object BitmapUtil {
                         bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream)
                         FileOutputStream(destFile).use { it.write(stream.toByteArray()) }
                     }
+                    if (publish)
+                        MediaStore.Images.Media.insertImage(context!!.contentResolver, destPath, File(destPath).name, File(destPath).name);
                     true
                 } catch (e: IOException) {
                     Log.e(TAG, "Could not save image to disk", e)
@@ -332,6 +351,39 @@ object BitmapUtil {
         }
     }
 
+    /**
+     * Save image for Android version >= Q
+     *
+     * @param context the context to use to get the [android.content.ContentResolver]
+     * @param bitmap the bitmap to save
+     * @param filename the filename
+     * @return true if the bitmap has been saved successfully
+     */
+    @WorkerThread
+    @RequiresApi(Build.VERSION_CODES.Q)
+    private fun saveImageInQ(context: Context, bitmap: Bitmap, filename: String): Boolean {
+        var fos: OutputStream? = null
+        var imageUri: Uri? = null
+        val contentValues = ContentValues().apply {
+            put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
+            put(MediaStore.MediaColumns.MIME_TYPE, "image/jpg")
+            put(MediaStore.MediaColumns.RELATIVE_PATH, "${Environment.DIRECTORY_PICTURES}/${Environment.DIRECTORY_SCREENSHOTS}")
+            put(MediaStore.Images.Media.IS_PENDING, 1)
+        }
+        val contentResolver = context.applicationContext.contentResolver
+        contentResolver.also { resolver ->
+            imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
+            fos = imageUri?.let { resolver.openOutputStream(it) }
+        }
+        fos?.use { bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it) }
+        contentValues.clear()
+        contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
+        imageUri?.let {
+            contentResolver.update(it, contentValues, null, null)
+        }
+        return imageUri != null
+    }
+
 }
 
 /**
diff --git a/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt b/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt
index 1dd2774cf3..660976a497 100644
--- a/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt
+++ b/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt
@@ -1152,7 +1152,7 @@ open class VideoPlayerActivity : AppCompatActivity(), PlaybackService.Callback,
                                 }
                                 val coords = IntArray(2)
                                 surfaceView.getLocationOnScreen(coords)
-                                if (BitmapUtil.saveOnDisk(bitmap, dst.absolutePath))
+                                if (BitmapUtil.saveOnDisk(bitmap, dst.absolutePath, publish = true, context = this at VideoPlayerActivity))
                                     screenshotDelegate.takeScreenshot(dst, bitmap, coords, surface.width, surface.height)
                                 else
                                     UiTools.snacker(this at VideoPlayerActivity, R.string.screenshot_error)



More information about the Android mailing list