package me.eternal.purrfect.core.features.impl.experiments import android.annotation.SuppressLint import android.app.Activity import android.content.ContentUris import android.content.ContentResolver import android.content.ContentValues import android.content.Intent import android.database.Cursor import android.database.CursorWrapper import android.media.MediaExtractor import android.media.MediaFormat import android.media.MediaMetadataRetriever import android.media.MediaMuxer import android.net.Uri import android.os.Build import android.os.ParcelFileDescriptor import android.provider.MediaStore import android.webkit.MimeTypeMap import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CheckCircleOutline import androidx.compose.material.icons.filled.Crop import androidx.compose.material.icons.filled.Error import androidx.compose.material.icons.filled.Upload import androidx.compose.material3.Icon import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import kotlinx.coroutines.delay import kotlinx.coroutines.launch import me.eternal.purrfect.common.data.FileType import me.eternal.purrfect.common.ui.createComposeView import me.eternal.purrfect.common.util.ktx.getLongOrNull import me.eternal.purrfect.common.util.ktx.getTypeArguments import me.eternal.purrfect.core.event.events.impl.ActivityResultEvent import me.eternal.purrfect.core.event.events.impl.AddViewEvent import me.eternal.purrfect.core.features.Feature import me.eternal.purrfect.core.ui.PurrfectOverlayPalette import me.eternal.purrfect.core.ui.PurrfectOverlayTheme import me.eternal.purrfect.core.util.dataBuilder import me.eternal.purrfect.core.util.hook.Hooker import me.eternal.purrfect.core.util.hook.HookStage import me.eternal.purrfect.core.util.hook.hook import me.eternal.purrfect.core.util.ktx.getObjectFieldOrNull import me.eternal.purrfect.mapper.impl.ChatMediaDrawerMapper import java.io.File import java.io.InputStream import java.lang.reflect.Method import java.nio.ByteBuffer import kotlin.random.Random class MediaFilePicker : Feature("Media File Picker") { companion object { private const val SNAP_CHUNK_DURATION_MS = 10_000L private val queuedSplitItems = ArrayDeque() private val queuedSplitItemIds = ArrayDeque() private val queuedSplitCleanupUris = mutableMapOf() private var originalUnsplitItem: Any? = null private var reusableOriginalItem: Any? = null private var queuedOverrideType: String? = null private var bypassSplitOnce = false private var sendSingleItemHandler: ((Any) -> Boolean)? = null private var cleanupItemHandler: ((String) -> Unit)? = null fun hasQueuedSplitItems(): Boolean = queuedSplitItems.isNotEmpty() fun hasPendingSplitCleanup(): Boolean = queuedSplitItemIds.isNotEmpty() fun hasOriginalUnsplitItem(): Boolean = originalUnsplitItem != null fun hasReusableOriginalItem(): Boolean = reusableOriginalItem != null fun setQueuedOverrideType(value: String?) { queuedOverrideType = value } fun getQueuedOverrideType(): String? = queuedOverrideType fun clearQueuedSplitItems(deleteTempItems: Boolean = true) { if (deleteTempItems) { val cleanup = cleanupItemHandler queuedSplitCleanupUris.values.toList().forEach { uri -> cleanup?.invoke(uri) } } queuedSplitItems.clear() queuedSplitItemIds.clear() queuedSplitCleanupUris.clear() originalUnsplitItem = null queuedOverrideType = null } fun sendReusableOriginalItem(): Boolean { val item = reusableOriginalItem ?: return false bypassSplitOnce = true val sender = sendSingleItemHandler ?: return false return sender(item) } private fun queueSplitItems(items: List, preparedItems: List, originalItem: Any?) { clearQueuedSplitItems(deleteTempItems = false) originalUnsplitItem = originalItem items.drop(1).forEach { queuedSplitItems.addLast(it) } preparedItems.forEach { queuedSplitItemIds.addLast(it.itemId) queuedSplitCleanupUris[it.itemId] = it.uri } } fun sendOriginalUnsplitItem(): Boolean { val item = originalUnsplitItem ?: return false val overrideType = queuedOverrideType clearQueuedSplitItems(deleteTempItems = true) queuedOverrideType = overrideType bypassSplitOnce = true val sender = sendSingleItemHandler ?: return false return sender(item) } fun handleCurrentQueuedItemSuccess(): Boolean { queuedSplitItemIds.removeFirstOrNull()?.let { itemId -> queuedSplitCleanupUris.remove(itemId)?.let { uri -> cleanupItemHandler?.invoke(uri) } } if (queuedSplitItems.isEmpty()) { queuedOverrideType = null return false } val next = queuedSplitItems.removeFirstOrNull() ?: run { queuedOverrideType = null return false } val sender = sendSingleItemHandler ?: return false val result = sender(next) if (!result) { queuedSplitItems.addFirst(next) } return result } } var lastMediaDuration: Long? = null private set private data class PreparedMediaItem( val itemId: String, val durationMs: Long, val uri: String ) private fun splitVideoIntoChunks( inputFile: File, chunkDurationMs: Long = SNAP_CHUNK_DURATION_MS ): List { val durationMs = extractMediaDuration(Uri.fromFile(inputFile)) ?: return emptyList() if (durationMs <= chunkDurationMs) return listOf(inputFile) val retriever = MediaMetadataRetriever() val rotation = runCatching { retriever.setDataSource(inputFile.absolutePath) retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)?.toIntOrNull() ?: 0 }.getOrDefault(0).also { runCatching { retriever.release() } } val outputFiles = mutableListOf() var chunkStartMs = 0L var chunkIndex = 0 while (chunkStartMs < durationMs) { val chunkEndMs = minOf(chunkStartMs + chunkDurationMs, durationMs) val outputFile = File.createTempFile("purrfect_chunk_${chunkIndex}_", ".mp4", context.androidContext.cacheDir) val extractor = MediaExtractor() val muxer = MediaMuxer(outputFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) val trackMap = mutableMapOf() val chunkStartUs = chunkStartMs * 1000 val chunkEndUs = chunkEndMs * 1000 var muxerStarted = false var wroteAnySample = false try { extractor.setDataSource(inputFile.absolutePath) repeat(extractor.trackCount) { trackIndex -> val format = extractor.getTrackFormat(trackIndex) val mime = format.getString(MediaFormat.KEY_MIME) ?: return@repeat if (!mime.startsWith("video/") && !mime.startsWith("audio/")) return@repeat extractor.selectTrack(trackIndex) trackMap[trackIndex] = muxer.addTrack(format) } if (rotation != 0) { muxer.setOrientationHint(rotation) } val maxBufferSize = (0 until extractor.trackCount).maxOfOrNull { trackIndex -> extractor.getTrackFormat(trackIndex).let { format -> if (format.containsKey(MediaFormat.KEY_MAX_INPUT_SIZE)) { format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE) } else { 1024 * 1024 } } } ?: (1024 * 1024) val buffer = ByteBuffer.allocateDirect(maxBufferSize) val bufferInfo = android.media.MediaCodec.BufferInfo() muxer.start() muxerStarted = true extractor.seekTo(chunkStartUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC) while (true) { bufferInfo.offset = 0 bufferInfo.size = extractor.readSampleData(buffer, 0) if (bufferInfo.size < 0) break val sampleTimeUs = extractor.sampleTime if (sampleTimeUs < 0) break if (sampleTimeUs < chunkStartUs) { extractor.advance() continue } if (sampleTimeUs >= chunkEndUs) break val sampleTrackIndex = extractor.sampleTrackIndex val muxerTrackIndex = trackMap[sampleTrackIndex] if (muxerTrackIndex != null) { bufferInfo.presentationTimeUs = sampleTimeUs - chunkStartUs bufferInfo.flags = extractor.sampleFlags muxer.writeSampleData(muxerTrackIndex, buffer, bufferInfo) wroteAnySample = true } extractor.advance() } if (wroteAnySample) { outputFiles += outputFile } else { outputFile.delete() } } catch (throwable: Throwable) { outputFile.delete() outputFiles.forEach { it.delete() } throw throwable } finally { if (muxerStarted) { runCatching { muxer.stop() } } runCatching { muxer.release() } runCatching { extractor.release() } } chunkStartMs += chunkDurationMs chunkIndex++ } return outputFiles } private fun registerTemporaryVideo(file: File, displayName: String): PreparedMediaItem { val resolver = context.androidContext.contentResolver val values = ContentValues().apply { put(MediaStore.Video.Media.DISPLAY_NAME, displayName) put(MediaStore.Video.Media.MIME_TYPE, "video/mp4") put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/.Purrfect") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { put(MediaStore.Video.Media.IS_PENDING, 1) } } val uri = resolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values) ?: error("Failed to create MediaStore entry") runCatching { resolver.openOutputStream(uri)?.use { output -> file.inputStream().use { input -> input.copyTo(output) } } ?: error("Failed to open MediaStore output stream") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { resolver.update(uri, ContentValues().apply { put(MediaStore.Video.Media.IS_PENDING, 0) }, null, null) } }.onFailure { resolver.delete(uri, null, null) throw it } val durationMs = extractMediaDuration(uri) ?: 0L val itemId = uri.lastPathSegment ?: error("Failed to resolve MediaStore item id") context.coroutineScope.launch { delay(120_000) runCatching { resolver.delete(uri, null, null) } } return PreparedMediaItem(itemId = itemId, durationMs = durationMs, uri = uri.toString()) } private fun buildDrawerItems(itemClass: Any, mediaItems: List): List { return mediaItems.mapIndexedNotNull { index, mediaItem -> itemClass.dataBuilder { from("_item") { set("_cameraRollSource", "Snapchat") set("_contentUri", "") set("_durationMs", mediaItem.durationMs.toDouble()) set("_disabled", false) set("_imageRotation", 0.0) set("_width", 1080.0) set("_height", 1920.0) set("_timestampMs", (System.currentTimeMillis() + index).toDouble()) from("_itemId") { set("_itemId", mediaItem.itemId) set("_type", "VIDEO") } } set("_order", index.toDouble()) } } } private fun prepareChunkedItemsFromMediaStoreId(itemId: String, durationMs: Long): List? { val numericId = itemId.toLongOrNull() ?: return null val effectiveDurationMs = durationMs.takeIf { it > 0 } ?: extractMediaDuration( ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, numericId) ) ?: return null if (effectiveDurationMs <= SNAP_CHUNK_DURATION_MS) return null val sourceUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, numericId) val sourceFile = File.createTempFile("purrfect_gallery_source_", ".mp4", context.androidContext.cacheDir) return runCatching { context.androidContext.contentResolver.openInputStream(sourceUri)?.use { input -> sourceFile.outputStream().use { output -> input.copyTo(output) } } ?: error("Failed to open source gallery video") val chunkFiles = splitVideoIntoChunks(sourceFile, SNAP_CHUNK_DURATION_MS) val preparedItems = chunkFiles.mapIndexed { index, file -> registerTemporaryVideo(file, "purrfect_gallery_chunk_${System.currentTimeMillis()}_$index.mp4") } chunkFiles.forEach { if (it != sourceFile) it.delete() } preparedItems }.also { sourceFile.delete() }.getOrElse { context.log.error("Failed to prepare split gallery items", it) null } } private fun extractMediaDuration(uri: Uri): Long? { val retriever = MediaMetadataRetriever() return runCatching { retriever.setDataSource(context.androidContext, uri) retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLongOrNull() }.getOrNull().also { runCatching { retriever.release() } } } private fun resolveInputExtension(uri: Uri, mimeType: String?): String { val extensionFromMime = mimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) }?.lowercase() val extensionFromUri = MimeTypeMap.getFileExtensionFromUrl(uri.toString()).takeIf { !it.isNullOrBlank() }?.lowercase() return when (extensionFromMime ?: extensionFromUri ?: mimeType) { "video/mp4", "audio/mp4", "application/mp4", "mp4", "m4v" -> "mp4" "video/quicktime", "mov", "qt" -> "mov" "video/webm", "webm" -> "webm" "video/x-matroska", "video/mkv", "mkv" -> "mkv" "video/avi", "video/x-msvideo", "avi" -> "avi" "audio/mpeg", "audio/mp3", "mp3" -> "mp3" "audio/aac", "aac" -> "aac" "audio/ogg", "audio/opus", "opus", "ogg" -> "opus" "audio/wav", "audio/x-wav", "wav" -> "wav" "audio/mp4a-latm", "audio/x-m4a", "m4a" -> "m4a" else -> FileType.fromString(extensionFromMime ?: extensionFromUri).fileExtension ?: "mp4" } } @SuppressLint("Recycle") override fun init() { if (!context.config.experimental.mediaFilePicker.get()) return onNextActivityCreate(defer = true) { lateinit var chatMediaDrawerActionHandler: Any var sendItemsMethod: Method? = null var drawerViewClass: Class<*>? = null var sendItemsListItemClassFallback: Class<*>? = null var sendItemsHookedHandler: Any? = null context.mappings.useMapper(ChatMediaDrawerMapper::class) { val drawerCls = chatMediaDrawerClass.getAsClass() ?: return@useMapper val actionHandlerCls = actionHandlerClass.getAsClass() ?: return@useMapper val sendItemsName = sendItemsMethodName.getAsString() ?: "sendItems" drawerViewClass = drawerCls sendItemsListItemClassFallback = sendItemsListItemClass.getAsClass() val contextType = drawerCls.genericSuperclass?.getTypeArguments()?.getOrNull(1) ?: return@useMapper val handlerParamMethod = contextType.methods.firstOrNull { method -> method.parameterTypes.size == 1 && ( method.parameterTypes[0].name.endsWith("ChatMediaDrawerActionHandler") || actionHandlerCls.isAssignableFrom(method.parameterTypes[0]) ) } ?: return@useMapper val sendItems = handlerParamMethod.parameterTypes[0].methods.firstOrNull { it.name == sendItemsName } ?: return@useMapper sendItemsMethod = sendItems handlerParamMethod.hook(HookStage.AFTER) { chatMediaDrawerActionHandler = it.arg(0) val handlerInstance = chatMediaDrawerActionHandler sendSingleItemHandler = sendSingleItem@{ item -> runCatching { sendItemsMethod?.invoke(chatMediaDrawerActionHandler, listOf(), listOf(item)) true }.getOrElse { throwable -> context.log.error("MediaFilePicker: Failed to send queued split item", throwable) false } } cleanupItemHandler = { uriString -> runCatching { context.androidContext.contentResolver.delete(Uri.parse(uriString), null, null) }.onFailure { context.log.warn("MediaFilePicker: Failed to delete temp split media: ${it.message}") } } if (sendItemsHookedHandler === handlerInstance) return@hook sendItemsHookedHandler = handlerInstance Hooker.hookObjectMethod( handlerInstance::class.java, handlerInstance, sendItemsName, HookStage.BEFORE ) { param -> if (bypassSplitOnce) { bypassSplitOnce = false return@hookObjectMethod } val currentItems = (param.argNullable(1) as? List<*>)?.filterNotNull() ?: return@hookObjectMethod if (currentItems.isEmpty()) return@hookObjectMethod reusableOriginalItem = currentItems.firstOrNull() val itemClass = sendItems.genericParameterTypes.getOrNull(1)?.getTypeArguments()?.firstOrNull() ?: sendItemsListItemClassFallback ?: currentItems.firstOrNull()?.javaClass ?: return@hookObjectMethod val preparedExpandedItems = mutableListOf() var didExpand = false val expandedItems = currentItems.flatMap { item -> val baseItem = item.getObjectFieldOrNull("_item") ?: return@flatMap listOf(item) val durationMs = ((baseItem.getObjectFieldOrNull("_durationMs") as? Double)?.toLong()) ?: ((baseItem.getObjectFieldOrNull("_durationMs") as? Long)) ?: 0L val itemId = baseItem.getObjectFieldOrNull("_itemId") ?.getObjectFieldOrNull("_itemId") ?.toString() ?: return@flatMap listOf(item) val splitItems = prepareChunkedItemsFromMediaStoreId(itemId, durationMs) if (splitItems.isNullOrEmpty()) { listOf(item) } else { didExpand = true preparedExpandedItems.addAll(splitItems) buildDrawerItems(itemClass, splitItems) } } if (didExpand && expandedItems.isNotEmpty()) { queueSplitItems(expandedItems, preparedExpandedItems, currentItems.firstOrNull()) param.setArg(1, listOf(expandedItems.first())) } } } } var requestCode: Int? = null var firstVideoId: Long? = null var mediaInputStream: InputStream? = null ContentResolver::class.java.apply { hook("query", HookStage.AFTER) { param -> val uri = param.arg(0) if (!uri.toString().endsWith(firstVideoId.toString())) return@hook param.setResult(object : CursorWrapper(param.getResult() as Cursor) { override fun getLong(columnIndex: Int): Long { if (getColumnName(columnIndex) == "duration") { return lastMediaDuration ?: -1 } return super.getLong(columnIndex) } }) } hook("openInputStream", HookStage.BEFORE) { param -> val uri = param.arg(0) if (uri.toString().endsWith(firstVideoId.toString())) { param.setResult(mediaInputStream) mediaInputStream = null } } } context.event.subscribe(ActivityResultEvent::class) { event -> if (sendItemsMethod == null || event.requestCode != requestCode || event.resultCode != Activity.RESULT_OK) return@subscribe requestCode = null firstVideoId = context.androidContext.contentResolver.query( MediaStore.Video.Media.EXTERNAL_CONTENT_URI, arrayOf(MediaStore.Video.Media._ID), null, null, "${MediaStore.Video.Media.DATE_TAKEN} DESC" )?.use { cursor -> if (cursor.moveToFirst()) { cursor.getLongOrNull("_id") } else { null } } if (firstVideoId == null) { context.inAppOverlay.showStatusToast( Icons.Default.Upload, "Must have a video in gallery to upload." ) return@subscribe } fun sendMedia(items: List? = null) { val method = sendItemsMethod ?: return val itemClass = method.genericParameterTypes.getOrNull(1)?.getTypeArguments()?.firstOrNull() ?: sendItemsListItemClassFallback if (itemClass == null) { context.log.warn("MediaFilePicker: sendItems second parameter type has no generic info (type erasure). genericParameterTypes[1]=${method.genericParameterTypes.getOrNull(1)}") context.inAppOverlay.showStatusToast(Icons.Default.Error, "Failed to send media (incompatible version).") return } val mediaItems = items ?: listOf(PreparedMediaItem(firstVideoId.toString(), lastMediaDuration ?: 0L, "")) val builtItems = buildDrawerItems(itemClass, mediaItems) if (builtItems.size != mediaItems.size) { context.inAppOverlay.showStatusToast(Icons.Default.Error, "Failed to build media item.") return } method.invoke(chatMediaDrawerActionHandler, listOf(), builtItems) } fun startConversion(audioOnly: Boolean) { context.coroutineScope.launch { val pickedUri = event.intent?.data ?: run { context.inAppOverlay.showStatusToast(Icons.Default.Error, "No media was selected.") return@launch } val mimeType = context.androidContext.contentResolver.getType(pickedUri) val inputExtension = resolveInputExtension(pickedUri, mimeType) val outputExtension = if (audioOnly || mimeType?.startsWith("audio/") == true) "m4a" else "mp4" lastMediaDuration = extractMediaDuration(pickedUri) context.inAppOverlay.showStatusToast(Icons.Default.Crop, "Converting media...", durationMs = 3000) val pickedFileDescriptor = context.androidContext.contentResolver.openFileDescriptor(pickedUri, "r") if (pickedFileDescriptor == null) { context.inAppOverlay.showStatusToast(Icons.Default.Error, "Failed to open selected media.") return@launch } val pfd = context.bridgeClient.convertMedia( pickedFileDescriptor, inputExtension, outputExtension, "aac", if (!audioOnly) "libx264" else null ) if (pfd == null) { context.inAppOverlay.showStatusToast(Icons.Default.Error, "Failed to convert media.") return@launch } context.inAppOverlay.showStatusToast(Icons.Default.CheckCircleOutline, "Media converted successfully.") runCatching { if (!audioOnly && (lastMediaDuration ?: 0L) > 10_000L) { val convertedFile = File.createTempFile("purrfect_source_", ".$outputExtension", context.androidContext.cacheDir) ParcelFileDescriptor.AutoCloseInputStream(pfd).use { input -> convertedFile.outputStream().use { output -> input.copyTo(output) } } val chunkFiles = splitVideoIntoChunks(convertedFile) val preparedItems = chunkFiles.mapIndexed { index, file -> registerTemporaryVideo(file, "purrfect_chunk_${System.currentTimeMillis()}_$index.mp4") } chunkFiles.forEach { if (it != convertedFile) it.delete() } convertedFile.delete() sendMedia(preparedItems) } else { mediaInputStream = ParcelFileDescriptor.AutoCloseInputStream(pfd) sendMedia() } }.onFailure { mediaInputStream = null context.log.error(it) context.inAppOverlay.showStatusToast(Icons.Default.Error, "Failed to send media.") } } } val pickedUri = event.intent?.data ?: return@subscribe val isAudio = context.androidContext.contentResolver.getType(pickedUri)?.startsWith("audio/") == true if (isAudio || context.config.messaging.galleryMediaSendOverride.mode.getNullable() == null) { startConversion(isAudio) return@subscribe } android.app.AlertDialog.Builder(context.mainActivity!!) .setTitle("Convert video file") .setItems(arrayOf("Send as video/audio", "Send as audio only")) { _, which -> startConversion(which == 1) } .setNegativeButton("Cancel") { dialog, _ -> dialog.dismiss() }.show() } val buttonTag = Random.nextInt(0, 65535) context.event.subscribe(AddViewEvent::class) { event -> if (event.parent !is FrameLayout || drawerViewClass?.isInstance(event.view) != true) return@subscribe event.view.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(v: View) { if (event.parent.findViewWithTag(buttonTag)?.run { visibility = View.VISIBLE bringToFront() } != null) return event.parent.addView( createComposeView(context.mainActivity!!) { PurrfectOverlayTheme { Row( modifier = Modifier .fillMaxWidth() .padding(end = 10.dp, top = 8.dp), horizontalArrangement = Arrangement.End ) { val shape = RoundedCornerShape(18.dp) Box( modifier = Modifier .size(54.dp) .shadow( elevation = 16.dp, shape = shape, spotColor = PurrfectOverlayPalette.glowPrimary.copy(alpha = 0.28f), ambientColor = PurrfectOverlayPalette.glowSecondary.copy(alpha = 0.20f) ) .clip(shape) .background( brush = Brush.linearGradient( listOf( PurrfectOverlayPalette.glowPrimary.copy(alpha = 0.35f), PurrfectOverlayPalette.glowSecondary.copy(alpha = 0.18f) ) ), shape = shape ) .border( 1.dp, Brush.linearGradient( listOf( PurrfectOverlayPalette.glowPrimary.copy(alpha = 0.7f), PurrfectOverlayPalette.glowSecondary.copy(alpha = 0.55f) ) ), shape = shape ) .clickable { requestCode = Random.nextInt(0, 65535) this@MediaFilePicker.context.mainActivity!!.startActivityForResult( Intent(Intent.ACTION_OPEN_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = "video/*" putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("video/*", "audio/*")) }, requestCode!! ) }, contentAlignment = androidx.compose.ui.Alignment.Center ) { Icon( imageVector = Icons.Default.Upload, contentDescription = "Upload media", tint = Color.White, modifier = Modifier.size(24.dp) ) } } } }.apply { tag = buttonTag layoutParams = FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ) } ) } override fun onViewDetachedFromWindow(v: View) { event.parent.findViewWithTag(buttonTag)?.visibility = View.GONE } }) } } } }