From ab1831105fa2e50f56a458c642d4a993205f00d9 Mon Sep 17 00:00:00 2001 From: OS-pedrogustavobilro Date: Mon, 31 Aug 2026 18:12:50 +0100 Subject: [PATCH 1/4] fix: preserve accept MIME types in WebView file chooser References: - https://outsystemsrd.atlassian.net/browse/RPM-7140 - https://outsystemsrd.atlassian.net/browse/RMET-5438 --- CHANGELOG.md | 6 + pom.xml | 2 +- .../helpers/OSIABFileChooserHelper.kt | 131 +++++++++++++++ .../views/OSIABWebViewActivity.kt | 46 ++++-- .../helpers/OSIABFileChooserHelperTest.kt | 155 ++++++++++++++++++ 5 files changed, 323 insertions(+), 17 deletions(-) create mode 100644 src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABFileChooserHelper.kt create mode 100644 src/test/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABFileChooserHelperTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 6284d6d..1e1c2e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.0.3] + +### Fixes + +- Preserve all `accept` MIME types and extensions from the WebView file chooser instead of collapsing them into a single guessed category, so inputs like `accept="image/*,application/pdf,application/msword"` correctly offer every accepted type in the picker instead of silently dropping non-image/video types. Also fixes a bug where combining `` with a documents-only `accept` list would silently cancel the file chooser instead of opening it ([RMET-5438](https://outsystemsrd.atlassian.net/browse/RMET-5438)). + ## [2.0.2] ### Fixes diff --git a/pom.xml b/pom.xml index 259c045..41e4cf8 100644 --- a/pom.xml +++ b/pom.xml @@ -6,5 +6,5 @@ 4.0.0 io.ionic.libs ioninappbrowser-android - 2.0.2 + 2.0.3 diff --git a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABFileChooserHelper.kt b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABFileChooserHelper.kt new file mode 100644 index 0000000..73939fb --- /dev/null +++ b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABFileChooserHelper.kt @@ -0,0 +1,131 @@ +package com.outsystems.plugins.inappbrowser.osinappbrowserlib.helpers + +import androidx.annotation.VisibleForTesting +import android.webkit.MimeTypeMap + +/** + * Resolves the `accept` tokens reported by a WebView's file chooser + * (`WebChromeClient.FileChooserParams#getAcceptTypes`) into concrete MIME types, and + * decides how those MIME types should be applied to an `ACTION_GET_CONTENT` intent. + * + * The `accept` attribute of an HTML `` can list a mix of: + * - MIME types, e.g. `image/png`, `application/pdf`, or an image wildcard type + * - file extensions, e.g. `.pdf`, `.docx` + * + * Android's `ACTION_GET_CONTENT` intent only understands MIME types (via `type` and + * [android.content.Intent.EXTRA_MIME_TYPES]), so extension tokens must be resolved to a + * MIME type first. When the accept list spans more than one disjoint MIME category + * (e.g. images and PDFs), a single `type` string can't represent all of them, so the + * intent must use the wildcard type together with `EXTRA_MIME_TYPES`. + */ +object OSIABFileChooserHelper { + + /** + * MIME type used on an intent to indicate no filtering, i.e. any content is acceptable. + */ + const val WILDCARD_MIME_TYPE = "*/*" + + /** + * The `type` / [android.content.Intent.EXTRA_MIME_TYPES] pair to apply to an + * `ACTION_GET_CONTENT` intent. + * + * @property type the MIME type to set as the intent's `type`. + * @property extraMimeTypes the value to set as [android.content.Intent.EXTRA_MIME_TYPES], + * or `null` when no additional filtering beyond [type] is needed. + */ + data class ChooserMimeConfig( + val type: String, + val extraMimeTypes: List? + ) + + /** + * Resolves raw accept tokens (MIME types and/or file extensions) into a deduplicated + * list of concrete MIME types, using the device's [MimeTypeMap] to resolve extensions. + * Tokens that cannot be resolved (e.g. unknown extensions) are dropped rather than + * causing a failure. + * + * @param acceptTypes raw tokens as provided by `FileChooserParams#getAcceptTypes()`. + * @return the deduplicated list of resolved MIME types, in the same order they were + * first encountered. Empty if no token could be resolved. + */ + fun resolveMimeTypes(acceptTypes: List): List = + resolveMimeTypes(acceptTypes, ::defaultExtensionToMimeType) + + /** + * Overload of [resolveMimeTypes] that takes the extension-to-MIME-type lookup as a + * parameter instead of always using [defaultExtensionToMimeType], so this logic can be + * unit tested without an Android runtime (`MimeTypeMap.getSingleton()` is not available + * in a plain JVM test). Production code should use the single-argument [resolveMimeTypes] + * overload; this one exists for tests only. + * + * @param acceptTypes raw tokens as provided by `FileChooserParams#getAcceptTypes()`. + * @param extensionToMimeType resolves a file extension (without the leading dot) to a + * MIME type, or `null` if it can't be resolved. + * @return the deduplicated list of resolved MIME types, in the same order they were + * first encountered. Empty if no token could be resolved. + */ + @VisibleForTesting + internal fun resolveMimeTypes( + acceptTypes: List, + extensionToMimeType: (String) -> String? + ): List = + acceptTypes.mapNotNull { resolveToken(it, extensionToMimeType) }.distinct() + + /** + * Decides the `type` / [android.content.Intent.EXTRA_MIME_TYPES] pair to use for an + * `ACTION_GET_CONTENT` intent, given an already-resolved list of MIME types (see + * [resolveMimeTypes]). + * + * - No resolvable MIME types: falls back to [WILDCARD_MIME_TYPE], no extra MIME types. + * - All resolved MIME types share a single top-level category (e.g. only an image + * wildcard type, or `image/png` + `image/jpeg`): uses that single type (the shared category's + * wildcard, or the lone MIME type itself), no extra MIME types needed. + * - MIME types span more than one top-level category (e.g. images + PDF + Word): + * uses [WILDCARD_MIME_TYPE] plus `EXTRA_MIME_TYPES`, so the picker offers exactly + * that disjoint set instead of collapsing to a single guessed category. + * + * @param resolvedMimeTypes MIME types as returned by [resolveMimeTypes]. Deduplicated + * internally, so callers do not need to pre-deduplicate. + * @return the [ChooserMimeConfig] to apply to the `ACTION_GET_CONTENT` intent. + */ + fun resolveChooserMimeConfig(resolvedMimeTypes: List): ChooserMimeConfig { + val distinct = resolvedMimeTypes.distinct() + if (distinct.isEmpty()) return ChooserMimeConfig(WILDCARD_MIME_TYPE, null) + val categories = distinct.map { it.substringBefore("/") }.distinct() + return if (categories.size == 1) { + val type = if (distinct.size == 1) distinct[0] else "${categories[0]}/*" + ChooserMimeConfig(type, null) + } else { + ChooserMimeConfig(WILDCARD_MIME_TYPE, distinct) + } + } + + /** + * Resolves a single raw accept token into a MIME type. + * + * @param token a single accept token, either a MIME type (e.g. `"image/png"`) or a file + * extension (e.g. `".pdf"`). + * @param extensionToMimeType resolves a file extension (without the leading dot) to a + * MIME type, or `null` if it can't be resolved. + * @return the resolved MIME type, or `null` if [token] is blank, is neither a MIME type + * nor an extension, or is an extension that [extensionToMimeType] can't resolve. + */ + private fun resolveToken(token: String, extensionToMimeType: (String) -> String?): String? { + val trimmed = token.trim() + return when { + trimmed.isEmpty() -> null + trimmed.contains("/") -> trimmed + trimmed.startsWith(".") -> extensionToMimeType(trimmed.removePrefix(".").lowercase()) + else -> null + } + } + + /** + * Default extension-to-MIME-type lookup, backed by the device's [MimeTypeMap]. + * + * @param extension a file extension without the leading dot, e.g. `"pdf"`. + * @return the resolved MIME type, or `null` if [MimeTypeMap] has no mapping for it. + */ + private fun defaultExtensionToMimeType(extension: String): String? = + MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) +} diff --git a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/views/OSIABWebViewActivity.kt b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/views/OSIABWebViewActivity.kt index 6665832..a3063a7 100644 --- a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/views/OSIABWebViewActivity.kt +++ b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/views/OSIABWebViewActivity.kt @@ -43,6 +43,7 @@ import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import com.outsystems.plugins.inappbrowser.osinappbrowserlib.OSIABEvents import com.outsystems.plugins.inappbrowser.osinappbrowserlib.R +import com.outsystems.plugins.inappbrowser.osinappbrowserlib.helpers.OSIABFileChooserHelper import com.outsystems.plugins.inappbrowser.osinappbrowserlib.helpers.OSIABPdfHelper import com.outsystems.plugins.inappbrowser.osinappbrowserlib.models.OSIABToolbarPosition import com.outsystems.plugins.inappbrowser.osinappbrowserlib.models.OSIABWebViewOptions @@ -619,7 +620,7 @@ open class OSIABWebViewActivity : AppCompatActivity() { private inner class OSIABWebChromeClient : WebChromeClient() { // for handling uploads (photo, video, gallery, files) - private var acceptTypes: String = "" + private var acceptTypes: List = emptyList() private var captureEnabled: Boolean = false // handle standard permissions (e.g. audio, camera) @@ -646,7 +647,7 @@ open class OSIABWebViewActivity : AppCompatActivity() { fileChooserParams: FileChooserParams ): Boolean { this@OSIABWebViewActivity.filePathCallback = filePathCallback - acceptTypes = fileChooserParams.acceptTypes.joinToString() + acceptTypes = fileChooserParams.acceptTypes.toList() captureEnabled = fileChooserParams.isCaptureEnabled // if camera permission is declared in manifest but is not granted, request it @@ -681,7 +682,7 @@ open class OSIABWebViewActivity : AppCompatActivity() { fun cancelFileChooser() { filePathCallback?.onReceiveValue(null) filePathCallback = null - acceptTypes = "" + acceptTypes = emptyList() captureEnabled = false } @@ -692,17 +693,17 @@ open class OSIABWebViewActivity : AppCompatActivity() { e.printStackTrace() cancelFileChooser() } - acceptTypes = "" + acceptTypes = emptyList() captureEnabled = false } - private fun launchFileChooser(acceptTypes: String = "", isCaptureEnabled: Boolean = false) { + private fun launchFileChooser(acceptTypes: List = emptyList(), isCaptureEnabled: Boolean = false) { val intentList = buildPhotoVideoIntents(acceptTypes) val permissionNotDeclaredOrGranted = hasCameraPermissionDeclared().not() || isCameraPermissionGranted() if (isCaptureEnabled && permissionNotDeclaredOrGranted) { // if capture is enabled, we only show the camera and video options - launchCameraChooser(intentList) + launchCameraChooser(intentList, acceptTypes, permissionNotDeclaredOrGranted) } else if (!isCaptureEnabled) { // if capture is not enabled, we show the full chooser launchFullChooser(intentList, acceptTypes, permissionNotDeclaredOrGranted) @@ -714,12 +715,15 @@ open class OSIABWebViewActivity : AppCompatActivity() { } } - private fun buildPhotoVideoIntents(acceptTypes: String): MutableList { + private fun buildPhotoVideoIntents(acceptTypes: List): MutableList { val intentList = mutableListOf() val permissionNotDeclaredOrGranted = hasCameraPermissionDeclared().not() || isCameraPermissionGranted() if (permissionNotDeclaredOrGranted) { - if (acceptTypes.contains("image") || acceptTypes.isEmpty()) { + val resolvedMimeTypes = OSIABFileChooserHelper.resolveMimeTypes(acceptTypes) + val noAcceptSpecified = acceptTypes.none { it.isNotBlank() } + + if (noAcceptSpecified || resolvedMimeTypes.any { it.startsWith("image/") }) { currentPhotoFile = createTempFile(this@OSIABWebViewActivity, "IMG_", ".jpg").also { file -> currentPhotoUri = FileProvider.getUriForFile( this@OSIABWebViewActivity, @@ -733,7 +737,7 @@ open class OSIABWebViewActivity : AppCompatActivity() { } intentList.add(takePictureIntent) } - if (acceptTypes.contains("video") || acceptTypes.isEmpty()) { + if (noAcceptSpecified || resolvedMimeTypes.any { it.startsWith("video/") }) { currentVideoFile = createTempFile(this@OSIABWebViewActivity, "VID_", ".mp4").also { file -> currentVideoFile = file currentVideoUri = FileProvider.getUriForFile( @@ -752,7 +756,18 @@ open class OSIABWebViewActivity : AppCompatActivity() { return intentList } - private fun launchCameraChooser(intentList: List) { + private fun launchCameraChooser( + intentList: List, + acceptTypes: List, + permissionNotDeclaredOrGranted: Boolean + ) { + if (intentList.isEmpty()) { + // nothing capturable for this accept list (e.g. a documents-only accept + // combined with a capture hint) - fall back to the full chooser instead + // of indexing into an empty list + launchFullChooser(intentList, acceptTypes, permissionNotDeclaredOrGranted) + return + } val chooser = if (intentList.size == 1) { intentList[0] } else { @@ -764,14 +779,13 @@ open class OSIABWebViewActivity : AppCompatActivity() { fileChooserLauncher.launch(chooser) } - private fun launchFullChooser(intentList: List, acceptTypes: String, permissionNotDeclaredOrGranted: Boolean) { + private fun launchFullChooser(intentList: List, acceptTypes: List, permissionNotDeclaredOrGranted: Boolean) { + val resolvedMimeTypes = OSIABFileChooserHelper.resolveMimeTypes(acceptTypes) + val mimeConfig = OSIABFileChooserHelper.resolveChooserMimeConfig(resolvedMimeTypes) val contentIntent = Intent(Intent.ACTION_GET_CONTENT).apply { addCategory(Intent.CATEGORY_OPENABLE) - type = when { - acceptTypes.contains("video") -> "video/*" - acceptTypes.contains("image") -> "image/*" - else -> "*/*" - } + type = mimeConfig.type + mimeConfig.extraMimeTypes?.let { putExtra(Intent.EXTRA_MIME_TYPES, it.toTypedArray()) } } val chooser = Intent(Intent.ACTION_CHOOSER).apply { putExtra(Intent.EXTRA_INTENT, contentIntent) diff --git a/src/test/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABFileChooserHelperTest.kt b/src/test/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABFileChooserHelperTest.kt new file mode 100644 index 0000000..74f0904 --- /dev/null +++ b/src/test/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABFileChooserHelperTest.kt @@ -0,0 +1,155 @@ +package com.outsystems.plugins.inappbrowser.osinappbrowserlib.helpers + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +class OSIABFileChooserHelperTest { + + private val fakeExtensionLookup: (String) -> String? = { extension -> + when (extension) { + "pdf" -> "application/pdf" + "docx" -> "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + "xlsx" -> "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + else -> null + } + } + + // region resolveMimeTypes + + @Test + fun `resolveMimeTypes keeps already-MIME tokens as-is`() { + val result = OSIABFileChooserHelper.resolveMimeTypes(listOf("image/*", "application/pdf")) + assertEquals(listOf("image/*", "application/pdf"), result) + } + + @Test + fun `resolveMimeTypes resolves pdf extension token to its MIME type`() { + val result = OSIABFileChooserHelper.resolveMimeTypes(listOf(".pdf"), fakeExtensionLookup) + assertEquals(listOf("application/pdf"), result) + } + + @Test + fun `resolveMimeTypes resolves docx extension token to its MIME type`() { + val result = OSIABFileChooserHelper.resolveMimeTypes(listOf(".docx"), fakeExtensionLookup) + assertEquals( + listOf("application/vnd.openxmlformats-officedocument.wordprocessingml.document"), + result + ) + } + + @Test + fun `resolveMimeTypes resolves xlsx extension token to its MIME type`() { + val result = OSIABFileChooserHelper.resolveMimeTypes(listOf(".xlsx"), fakeExtensionLookup) + assertEquals( + listOf("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"), + result + ) + } + + @Test + fun `resolveMimeTypes resolves mixed MIME and extension tokens`() { + val result = OSIABFileChooserHelper.resolveMimeTypes( + listOf("image/*", ".pdf", ".docx"), + fakeExtensionLookup + ) + assertEquals( + listOf( + "image/*", + "application/pdf", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + ), + result + ) + } + + @Test + fun `resolveMimeTypes deduplicates repeated tokens`() { + val result = OSIABFileChooserHelper.resolveMimeTypes( + listOf("image/*", "image/*", ".pdf", ".pdf"), + fakeExtensionLookup + ) + assertEquals(listOf("image/*", "application/pdf"), result) + } + + @Test + fun `resolveMimeTypes drops unresolvable extension tokens without crashing`() { + val result = OSIABFileChooserHelper.resolveMimeTypes( + listOf(".unknownext", "image/*"), + fakeExtensionLookup + ) + assertEquals(listOf("image/*"), result) + } + + @Test + fun `resolveMimeTypes ignores blank tokens`() { + val result = OSIABFileChooserHelper.resolveMimeTypes(listOf("", " ", "image/*"), fakeExtensionLookup) + assertEquals(listOf("image/*"), result) + } + + @Test + fun `resolveMimeTypes returns empty list when accept list is empty`() { + val result = OSIABFileChooserHelper.resolveMimeTypes(emptyList(), fakeExtensionLookup) + assertTrue(result.isEmpty()) + } + + @Test + fun `resolveMimeTypes drops bare tokens with no slash and no leading dot`() { + val result = OSIABFileChooserHelper.resolveMimeTypes(listOf("pdf", "image/*"), fakeExtensionLookup) + assertEquals(listOf("image/*"), result) + } + + @Test + fun `resolveMimeTypes deduplicates when a MIME token and an extension token resolve to the same type`() { + val lookup: (String) -> String? = { if (it == "pdf") "application/pdf" else null } + val result = OSIABFileChooserHelper.resolveMimeTypes(listOf("application/pdf", ".pdf"), lookup) + assertEquals(listOf("application/pdf"), result) + } + + // endregion + + // region resolveChooserMimeConfig + + @Test + fun `resolveChooserMimeConfig returns wildcard with no extras when list is empty`() { + val config = OSIABFileChooserHelper.resolveChooserMimeConfig(emptyList()) + assertEquals(OSIABFileChooserHelper.WILDCARD_MIME_TYPE, config.type) + assertNull(config.extraMimeTypes) + } + + @Test + fun `resolveChooserMimeConfig returns the single MIME type with no extras`() { + val config = OSIABFileChooserHelper.resolveChooserMimeConfig(listOf("image/*")) + assertEquals("image/*", config.type) + assertNull(config.extraMimeTypes) + } + + @Test + fun `resolveChooserMimeConfig collapses same-category MIME types into a wildcard category type`() { + val config = OSIABFileChooserHelper.resolveChooserMimeConfig(listOf("image/png", "image/jpeg")) + assertEquals("image/*", config.type) + assertNull(config.extraMimeTypes) + } + + @Test + fun `resolveChooserMimeConfig uses wildcard type with extra MIME types for disjoint categories`() { + val config = OSIABFileChooserHelper.resolveChooserMimeConfig( + listOf("image/*", "application/pdf", "application/msword") + ) + assertEquals(OSIABFileChooserHelper.WILDCARD_MIME_TYPE, config.type) + assertEquals( + setOf("image/*", "application/pdf", "application/msword"), + config.extraMimeTypes?.toSet() + ) + } + + @Test + fun `resolveChooserMimeConfig dedupes even when called with a non-distinct list`() { + val config = OSIABFileChooserHelper.resolveChooserMimeConfig(listOf("image/*", "image/*")) + assertEquals("image/*", config.type) + assertNull(config.extraMimeTypes) + } + + // endregion +} From d9054e87f5a2246727a9bc194ccd0ce9b73ff5f4 Mon Sep 17 00:00:00 2001 From: OS-pedrogustavobilro Date: Mon, 31 Aug 2026 18:29:28 +0100 Subject: [PATCH 2/4] fix: mime type resolution with different resolved mime types Selecting only documents was allow zip files too, because they were falling under an incorrectly assumed application/* wildcard --- CHANGELOG.md | 2 +- .../helpers/OSIABFileChooserHelper.kt | 28 +++++++++---------- .../helpers/OSIABFileChooserHelperTest.kt | 20 +++++++++++-- 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e1c2e9..b40c980 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixes -- Preserve all `accept` MIME types and extensions from the WebView file chooser instead of collapsing them into a single guessed category, so inputs like `accept="image/*,application/pdf,application/msword"` correctly offer every accepted type in the picker instead of silently dropping non-image/video types. Also fixes a bug where combining `` with a documents-only `accept` list would silently cancel the file chooser instead of opening it ([RMET-5438](https://outsystemsrd.atlassian.net/browse/RMET-5438)). +- Preserve all `accept` MIME types and extensions from the WebView file chooser instead of collapsing them into a single guessed category, so inputs like `accept="image/*,application/pdf,application/msword"` correctly offer every accepted type in the picker instead of silently dropping non-image/video types. Also fixes a bug where combining `` with a documents-only `accept` list would silently cancel the file chooser instead of opening it ([RMET-5438](https://outsystemsrd.atlassian.net/browse/RMET-5438) + [RPM-7140](https://outsystemsrd.atlassian.net/browse/RPM-7140)). ## [2.0.2] diff --git a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABFileChooserHelper.kt b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABFileChooserHelper.kt index 73939fb..708914c 100644 --- a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABFileChooserHelper.kt +++ b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABFileChooserHelper.kt @@ -23,7 +23,7 @@ object OSIABFileChooserHelper { /** * MIME type used on an intent to indicate no filtering, i.e. any content is acceptable. */ - const val WILDCARD_MIME_TYPE = "*/*" + const val WILDCARD_MIME_TYPE = "OSInAppBrowserLib-debug.aar" /** * The `type` / [android.content.Intent.EXTRA_MIME_TYPES] pair to apply to an @@ -77,12 +77,15 @@ object OSIABFileChooserHelper { * [resolveMimeTypes]). * * - No resolvable MIME types: falls back to [WILDCARD_MIME_TYPE], no extra MIME types. - * - All resolved MIME types share a single top-level category (e.g. only an image - * wildcard type, or `image/png` + `image/jpeg`): uses that single type (the shared category's - * wildcard, or the lone MIME type itself), no extra MIME types needed. - * - MIME types span more than one top-level category (e.g. images + PDF + Word): - * uses [WILDCARD_MIME_TYPE] plus `EXTRA_MIME_TYPES`, so the picker offers exactly - * that disjoint set instead of collapsing to a single guessed category. + * - Exactly one resolved MIME type: uses that type directly (whether it's already a + * wildcard like an image wildcard type, or a specific type like `application/pdf`), + * no extra MIME types needed. + * - More than one resolved MIME type: uses [WILDCARD_MIME_TYPE] plus `EXTRA_MIME_TYPES` + * with the full list, regardless of whether they share a top-level category. Multiple + * distinct subtypes under the same category (e.g. `application/pdf` + `application/msword`) + * are intentionally not collapsed into that category's wildcard, since a top-level + * category wildcard can be far broader than what was actually requested (the + * `application` category wildcard alone also matches zip, octet-stream, JSON, etc). * * @param resolvedMimeTypes MIME types as returned by [resolveMimeTypes]. Deduplicated * internally, so callers do not need to pre-deduplicate. @@ -90,13 +93,10 @@ object OSIABFileChooserHelper { */ fun resolveChooserMimeConfig(resolvedMimeTypes: List): ChooserMimeConfig { val distinct = resolvedMimeTypes.distinct() - if (distinct.isEmpty()) return ChooserMimeConfig(WILDCARD_MIME_TYPE, null) - val categories = distinct.map { it.substringBefore("/") }.distinct() - return if (categories.size == 1) { - val type = if (distinct.size == 1) distinct[0] else "${categories[0]}/*" - ChooserMimeConfig(type, null) - } else { - ChooserMimeConfig(WILDCARD_MIME_TYPE, distinct) + return when (distinct.size) { + 0 -> ChooserMimeConfig(WILDCARD_MIME_TYPE, null) + 1 -> ChooserMimeConfig(distinct[0], null) + else -> ChooserMimeConfig(WILDCARD_MIME_TYPE, distinct) } } diff --git a/src/test/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABFileChooserHelperTest.kt b/src/test/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABFileChooserHelperTest.kt index 74f0904..67a4bc6 100644 --- a/src/test/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABFileChooserHelperTest.kt +++ b/src/test/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABFileChooserHelperTest.kt @@ -126,10 +126,24 @@ class OSIABFileChooserHelperTest { } @Test - fun `resolveChooserMimeConfig collapses same-category MIME types into a wildcard category type`() { + fun `resolveChooserMimeConfig does not collapse same-category MIME types into that category's wildcard`() { + // e.g. application/pdf + application/msword must not become application/*, since + // that top-level wildcard is far broader than requested (also matches zip, etc.) val config = OSIABFileChooserHelper.resolveChooserMimeConfig(listOf("image/png", "image/jpeg")) - assertEquals("image/*", config.type) - assertNull(config.extraMimeTypes) + assertEquals(OSIABFileChooserHelper.WILDCARD_MIME_TYPE, config.type) + assertEquals(setOf("image/png", "image/jpeg"), config.extraMimeTypes?.toSet()) + } + + @Test + fun `resolveChooserMimeConfig uses EXTRA_MIME_TYPES for multiple document subtypes under the application category`() { + val config = OSIABFileChooserHelper.resolveChooserMimeConfig( + listOf("application/pdf", "application/msword", "application/vnd.ms-excel") + ) + assertEquals(OSIABFileChooserHelper.WILDCARD_MIME_TYPE, config.type) + assertEquals( + setOf("application/pdf", "application/msword", "application/vnd.ms-excel"), + config.extraMimeTypes?.toSet() + ) } @Test From 356b2398d05b21e78f95197024b3be42aa9c81a8 Mon Sep 17 00:00:00 2001 From: OS-pedrogustavobilro Date: Mon, 31 Aug 2026 18:45:30 +0100 Subject: [PATCH 3/4] chore: correct wildcard mimetype --- .../osinappbrowserlib/helpers/OSIABFileChooserHelper.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABFileChooserHelper.kt b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABFileChooserHelper.kt index 708914c..b9310d5 100644 --- a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABFileChooserHelper.kt +++ b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABFileChooserHelper.kt @@ -23,7 +23,7 @@ object OSIABFileChooserHelper { /** * MIME type used on an intent to indicate no filtering, i.e. any content is acceptable. */ - const val WILDCARD_MIME_TYPE = "OSInAppBrowserLib-debug.aar" + const val WILDCARD_MIME_TYPE = "*/*" /** * The `type` / [android.content.Intent.EXTRA_MIME_TYPES] pair to apply to an From 243afe28364b6a7e495fee7a90dd9f56a6ae86c5 Mon Sep 17 00:00:00 2001 From: OS-pedrogustavobilro Date: Mon, 31 Aug 2026 18:51:31 +0100 Subject: [PATCH 4/4] fix: treat */* accept as unrestricted so capture shortcuts still work --- .../osinappbrowserlib/views/OSIABWebViewActivity.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/views/OSIABWebViewActivity.kt b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/views/OSIABWebViewActivity.kt index a3063a7..72a250e 100644 --- a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/views/OSIABWebViewActivity.kt +++ b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/views/OSIABWebViewActivity.kt @@ -721,7 +721,8 @@ open class OSIABWebViewActivity : AppCompatActivity() { if (permissionNotDeclaredOrGranted) { val resolvedMimeTypes = OSIABFileChooserHelper.resolveMimeTypes(acceptTypes) - val noAcceptSpecified = acceptTypes.none { it.isNotBlank() } + val noAcceptSpecified = acceptTypes.none { it.isNotBlank() } || + resolvedMimeTypes.contains(OSIABFileChooserHelper.WILDCARD_MIME_TYPE) if (noAcceptSpecified || resolvedMimeTypes.any { it.startsWith("image/") }) { currentPhotoFile = createTempFile(this@OSIABWebViewActivity, "IMG_", ".jpg").also { file ->