From 3f0bf0f742e08f76859cc46ac2fdb65e02f080e7 Mon Sep 17 00:00:00 2001 From: Brad Date: Fri, 3 Jul 2026 20:47:34 -0400 Subject: [PATCH] feat: add media permissions and Media Store utility - added `android.permission.READ_MEDIA_IMAGES` and `android.permission.READ_MEDIA_VIDEO` to enable media file access - added `LightMediaStore.kt` as a util wrapper SDK users to leverage. Currently supports listing via cursor and reading via stream --- .../thelightphone/plugin/LightToolMetadata.kt | 2 + .../com/thelightphone/sdk/LightMediaStore.kt | 91 +++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 sdk/client/src/main/kotlin/com/thelightphone/sdk/LightMediaStore.kt diff --git a/plugin/src/main/kotlin/com/thelightphone/plugin/LightToolMetadata.kt b/plugin/src/main/kotlin/com/thelightphone/plugin/LightToolMetadata.kt index f1c6f30d..7b172695 100644 --- a/plugin/src/main/kotlin/com/thelightphone/plugin/LightToolMetadata.kt +++ b/plugin/src/main/kotlin/com/thelightphone/plugin/LightToolMetadata.kt @@ -141,6 +141,8 @@ object LightToolPolicy { "android.permission.RECORD_AUDIO", "android.permission.ACCESS_FINE_LOCATION", "android.permission.ACCESS_COARSE_LOCATION", + "android.permission.READ_MEDIA_IMAGES", + "android.permission.READ_MEDIA_VIDEO", ) /** diff --git a/sdk/client/src/main/kotlin/com/thelightphone/sdk/LightMediaStore.kt b/sdk/client/src/main/kotlin/com/thelightphone/sdk/LightMediaStore.kt new file mode 100644 index 00000000..e1ed1d7d --- /dev/null +++ b/sdk/client/src/main/kotlin/com/thelightphone/sdk/LightMediaStore.kt @@ -0,0 +1,91 @@ +package com.thelightphone.sdk + +import android.content.ContentUris +import android.content.Context +import android.net.Uri +import android.provider.MediaStore +import java.io.InputStream + +class LightMediaStore internal constructor(private val androidContext: Context) { + + enum class MediaType(internal val mediaStoreValue: Int) { + IMAGE(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE), + VIDEO(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO), + } + + data class MediaItem( + val uri: Uri, + val displayName: String, + val size: Long, + val relativePath: String, + val dateTakenMillis: Long?, + val mimeType: String, + val type: MediaType, + ) + + fun list(types: Set = setOf(MediaType.IMAGE, MediaType.VIDEO)): List { + require(types.isNotEmpty()) { "types must not be empty" } + + val collection = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL) + val projection = arrayOf( + MediaStore.Files.FileColumns._ID, + MediaStore.Files.FileColumns.DISPLAY_NAME, + MediaStore.Files.FileColumns.SIZE, + MediaStore.Files.FileColumns.DATE_TAKEN, + MediaStore.Files.FileColumns.MIME_TYPE, + MediaStore.Files.FileColumns.RELATIVE_PATH, + MediaStore.Files.FileColumns.MEDIA_TYPE, + ) + + val typePlaceholders = types.joinToString(",") { "?" } + val selection = + "${MediaStore.Files.FileColumns.MEDIA_TYPE} IN ($typePlaceholders) AND " + + "${MediaStore.Files.FileColumns.RELATIVE_PATH} LIKE ?" + val selectionArgs = types.map { it.mediaStoreValue.toString() }.toTypedArray() + "DCIM/%" + val sortOrder = "${MediaStore.Files.FileColumns.DATE_TAKEN} DESC" + + val results = mutableListOf() + + androidContext.contentResolver.query( + collection, + projection, + selection, + selectionArgs, + sortOrder, + )?.use { cursor -> + val idCol = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID) + val nameCol = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DISPLAY_NAME) + val sizeCol = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.SIZE) + val dateCol = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATE_TAKEN) + val mimeCol = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MIME_TYPE) + val pathCol = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.RELATIVE_PATH) + val typeCol = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE) + + while (cursor.moveToNext()) { + val id = cursor.getLong(idCol) + val typeValue = cursor.getInt(typeCol) + val mediaType = MediaType.entries.firstOrNull { it.mediaStoreValue == typeValue } + ?: continue + results += MediaItem( + uri = ContentUris.withAppendedId(collection, id), + displayName = cursor.getString(nameCol).orEmpty(), + size = cursor.getLong(sizeCol), + relativePath = cursor.getString(pathCol).orEmpty(), + dateTakenMillis = if (cursor.isNull(dateCol)) null else cursor.getLong(dateCol), + mimeType = cursor.getString(mimeCol).orEmpty(), + type = mediaType, + ) + } + } + + return results + } + + fun listImages(): List = list(setOf(MediaType.IMAGE)) + + fun listVideos(): List = list(setOf(MediaType.VIDEO)) + + fun read(item: MediaItem, block: (InputStream) -> T): T? { + return androidContext.contentResolver.openInputStream(item.uri)?.use(block) + } +}