Mobile Development 12 min read

Saving Images to Android Gallery Using the MediaStore API

This article explains how to save images to the Android gallery using the MediaStore API, covering permission changes across Android versions, the insertion and output stream workflow, code examples in Kotlin, and considerations for sharing images without unnecessary FileProvider usage.

Sohu Tech Products
Sohu Tech Products
Sohu Tech Products
Saving Images to Android Gallery Using the MediaStore API

Google has tightened privacy and system security on Android, making direct file access increasingly difficult. Starting with Android 6.0, runtime storage permissions are required; Android 10 introduced scoped storage (which can be disabled with android:requestLegacyExternalStorage="true" ), and Android 11 enforces scoped storage, requiring all apps targeting API 30 to use MediaStore for shared media access.

The recommended approach is to use the MediaStore framework, which behaves like a database for media files. The general workflow is:

Insert a new image record into MediaStore to obtain a content Uri .

Open an output stream for that Uri and write the image data.

For Android 10+ set the IS_PENDING flag to 0 after writing so other apps can see the file.

Below are the essential code snippets.

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />

private fun saveImageInternal() { val uri = assets.open("wallhaven_rdyyjm.jpg").use { it.saveToAlbum(this, fileName = "save_wallhaven_rdyyjm.jpg", null) } ?: return Toast.makeText(this, uri.toString(), Toast.LENGTH_SHORT).show() }

/** * 保存 Bitmap 到相册的 Pictures 文件夹 * * @param context 上下文 * @param fileName 文件名,需要带后缀 * @param relativePath 相对于 Pictures 的路径 * @param quality 图片质量 */ fun Bitmap.saveToAlbum( context: Context, fileName: String, relativePath: String? = null, quality: Int = 75 ): Uri? { val resolver = context.contentResolver val outputFile = OutputFileTaker() // 插入图片信息 val imageUri = resolver.insertMediaImage(fileName, relativePath, outputFile) if (imageUri == null) { Log.w(TAG, "insert: error: uri == null") return null } // 通过 Uri 打开输出流并写入图片 (imageUri.outputStream(resolver) ?: return null).use { val format = fileName.getBitmapFormat() [email protected](format, quality, it) // 更新 IS_PENDING 状态 imageUri.finishPending(context, resolver, outputFile.file) } return imageUri }

private fun ContentResolver.insertMediaImage( fileName: String, relativePath: String?, outputFileTaker: OutputFileTaker? = null ): Uri? { val imageValues = ContentValues().apply { val mimeType = fileName.getMimeType() if (mimeType != null) { put(MediaStore.Images.Media.MIME_TYPE, mimeType) } val date = System.currentTimeMillis() / 1000 put(MediaStore.Images.Media.DATE_ADDED, date) put(MediaStore.Images.Media.DATE_MODIFIED, date) } val collection: Uri if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { val path = if (relativePath != null) "${ALBUM_DIR}/${relativePath}" else ALBUM_DIR imageValues.apply { put(MediaStore.Images.Media.DISPLAY_NAME, fileName) put(MediaStore.Images.Media.RELATIVE_PATH, path) put(MediaStore.Images.Media.IS_PENDING, 1) } collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) } else { val pictures = Environment.getExternalStoragePublicDirectory(ALBUM_DIR) val saveDir = if (relativePath != null) File(pictures, relativePath) else pictures if (!saveDir.exists() && !saveDir.mkdirs()) { Log.e(TAG, "save: error: can't create Pictures directory") return null } var imageFile = File(saveDir, fileName) val fileNameWithoutExtension = imageFile.nameWithoutExtension val fileExtension = imageFile.extension var queryUri = this.queryMediaImage28(imageFile.absolutePath) var suffix = 1 while (queryUri != null) { val newName = "${fileNameWithoutExtension}(${suffix++}).${fileExtension}" imageFile = File(saveDir, newName) queryUri = this.queryMediaImage28(imageFile.absolutePath) } imageValues.apply { put(MediaStore.Images.Media.DISPLAY_NAME, imageFile.name) val imagePath = imageFile.absolutePath Log.v(TAG, "save file: $imagePath") put(MediaStore.Images.Media.DATA, imagePath) } outputFileTaker?.file = imageFile collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI } return this.insert(collection, imageValues) }

private fun ContentResolver.queryMediaImage28(imagePath: String): Uri? { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) return null val imageFile = File(imagePath) if (imageFile.canRead() && imageFile.exists()) { Log.v(TAG, "query: path: $imagePath exists") return Uri.fromFile(imageFile) } val collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI val query = this.query( collection, arrayOf(MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA), "${MediaStore.Images.Media.DATA} = ?", arrayOf(imagePath), null ) query?.use { while (it.moveToNext()) { val idColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media._ID) val id = it.getLong(idColumn) val existsUri = ContentUris.withAppendedId(collection, id) Log.v(TAG, "query: path: $imagePath exists uri: $existsUri") return existsUri } } return null }

private fun Uri.finishPending( context: Context, resolver: ContentResolver, outputFile: File? ) { val imageValues = ContentValues() if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (outputFile != null) { imageValues.put(MediaStore.Images.Media.SIZE, outputFile.length()) } resolver.update(this, imageValues, null, null) val intent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, this) context.sendBroadcast(intent) } else { imageValues.put(MediaStore.Images.Media.IS_PENDING, 0) resolver.update(this, imageValues, null, null) } }

private fun shareImageInternal() { val uri = assets.open("wallhaven_rdyyjm.jpg").use { it.saveToAlbum(this, fileName = "save_wallhaven_rdyyjm.jpg", null) } ?: return val intent = Intent(Intent.ACTION_SEND) .putExtra(Intent.EXTRA_STREAM, uri) .setType("image/*") startActivity(Intent.createChooser(intent, null)) }

The article also clarifies when a FileProvider is actually needed: only when sharing files that reside in the app’s private sandbox (e.g., sharing an APK or a private image to WeChat). Images saved to the system gallery are already in a public location, so a FileProvider is unnecessary and can be omitted to avoid extra initialization overhead.

References are provided for the MediaStore demo repository and official Android documentation.

Mobile DevelopmentAndroidKotlinFileProviderMediaStoreImage Saving
Sohu Tech Products
Written by

Sohu Tech Products

A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.