From b625851dd98a9103dc6cbe4e1b4d127bc5429013 Mon Sep 17 00:00:00 2001 From: OS-pedrogustavobilro Date: Thu, 16 Apr 2026 13:16:25 +0100 Subject: [PATCH 1/6] fix: Move pdf check from handleLoadUrl to DownloadListener This was causing an issue on ASPX and Traditional Web Apps where multiple requests were triggering "Preparation" server action more than once when loading said web page in a WebView. Given that they weren't trying to load PDFs to begin with, this shows how checking the pdf type before loading the webpage is probably not the way to go. Instead, we use webview.setDownloadListener to listen when we receive a file that should be downloaded in the webview, and remove the checks from handleLoadUrl References: - https://outsystemsrd.atlassian.net/browse/RMET-5141 - https://success.outsystems.com/documentation/11/building_apps/application_logic/actions_in_web_applications/#preparation-actions - https://outsystems.slack.com/archives/C04ND35707P/p1776336737181489 - https://developer.android.com/reference/android/webkit/DownloadListener --- .../views/OSIABWebViewActivity.kt | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) 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 59ec090..54ab69d 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 @@ -218,6 +218,29 @@ class OSIABWebViewActivity : AppCompatActivity() { enableThirdPartyCookies() setupWebView() + + webView.setDownloadListener { url, userAgent, contentDisposition, mimetype, contentLength -> + Log.e("downloadlistener", "mimeType=$mimetype, url=$url") + if (mimetype == "application/pdf" && !url.startsWith(PDF_VIEWER_URL_PREFIX)) { + lifecycleScope.launch(Dispatchers.IO) { + val pdfFile = try { + OSIABPdfHelper.downloadPdfToCache(this@OSIABWebViewActivity, url) + } catch (_: IOException) { + null + } + if (pdfFile != null) { + withContext(Dispatchers.Main) { + webView.stopLoading() + originalUrl = url + val pdfJsUrl = + PDF_VIEWER_URL_PREFIX + Uri.encode("file://${pdfFile.absolutePath}") + webView.loadUrl(pdfJsUrl) + } + } + } + } + } + if (urlToOpen != null) { handleLoadUrl(urlToOpen, customHeaders) showLoadingScreen() @@ -253,25 +276,7 @@ class OSIABWebViewActivity : AppCompatActivity() { } private fun handleLoadUrl(url: String, additionalHttpHeaders: Map? = null) { - lifecycleScope.launch(Dispatchers.IO) { - if (OSIABPdfHelper.isContentTypeApplicationPdf(url)) { - val pdfFile = try { OSIABPdfHelper.downloadPdfToCache(this@OSIABWebViewActivity, url) } catch (_: IOException) { null } - if (pdfFile != null) { - withContext(Dispatchers.Main) { - webView.stopLoading() - originalUrl = url - val pdfJsUrl = - PDF_VIEWER_URL_PREFIX + Uri.encode("file://${pdfFile.absolutePath}") - webView.loadUrl(pdfJsUrl) - } - return@launch - } - } - - withContext(Dispatchers.Main) { - webView.loadUrl(url, additionalHttpHeaders ?: emptyMap()) - } - } + webView.loadUrl(url, additionalHttpHeaders ?: emptyMap()) } From ebb34a514a7a15027db94f79f2a59f3bba72221a Mon Sep 17 00:00:00 2001 From: OS-pedrogustavobilro Date: Thu, 16 Apr 2026 17:55:07 +0100 Subject: [PATCH 2/6] chore: Check content disposition like OSIABPdfHelper --- .../osinappbrowserlib/views/OSIABWebViewActivity.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 54ab69d..47beaf5 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 @@ -219,9 +219,9 @@ class OSIABWebViewActivity : AppCompatActivity() { setupWebView() - webView.setDownloadListener { url, userAgent, contentDisposition, mimetype, contentLength -> - Log.e("downloadlistener", "mimeType=$mimetype, url=$url") - if (mimetype == "application/pdf" && !url.startsWith(PDF_VIEWER_URL_PREFIX)) { + webView.setDownloadListener { url, _, contentDisposition, mimeType, _ -> + Log.e("downloadlistener", "mimeType=$mimeType, contentDisposition=$contentDisposition url=$url") + if ((mimeType == "application/pdf" || contentDisposition.contains(".pdf")) && !url.startsWith(PDF_VIEWER_URL_PREFIX)) { lifecycleScope.launch(Dispatchers.IO) { val pdfFile = try { OSIABPdfHelper.downloadPdfToCache(this@OSIABWebViewActivity, url) From eb1cfff21e44f6d902bc7852adc8992f9ec38dda Mon Sep 17 00:00:00 2001 From: OS-pedrogustavobilro Date: Thu, 16 Apr 2026 18:15:56 +0100 Subject: [PATCH 3/6] refactor: extract listener to separate method --- .../views/OSIABWebViewActivity.kt | 64 ++++++++++++------- 1 file changed, 41 insertions(+), 23 deletions(-) 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 47beaf5..1f063ba 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 @@ -219,28 +219,6 @@ class OSIABWebViewActivity : AppCompatActivity() { setupWebView() - webView.setDownloadListener { url, _, contentDisposition, mimeType, _ -> - Log.e("downloadlistener", "mimeType=$mimeType, contentDisposition=$contentDisposition url=$url") - if ((mimeType == "application/pdf" || contentDisposition.contains(".pdf")) && !url.startsWith(PDF_VIEWER_URL_PREFIX)) { - lifecycleScope.launch(Dispatchers.IO) { - val pdfFile = try { - OSIABPdfHelper.downloadPdfToCache(this@OSIABWebViewActivity, url) - } catch (_: IOException) { - null - } - if (pdfFile != null) { - withContext(Dispatchers.Main) { - webView.stopLoading() - originalUrl = url - val pdfJsUrl = - PDF_VIEWER_URL_PREFIX + Uri.encode("file://${pdfFile.absolutePath}") - webView.loadUrl(pdfJsUrl) - } - } - } - } - } - if (urlToOpen != null) { handleLoadUrl(urlToOpen, customHeaders) showLoadingScreen() @@ -332,6 +310,14 @@ class OSIABWebViewActivity : AppCompatActivity() { options.showURL && options.showToolbar ) webView.webChromeClient = customWebChromeClient() + + webView.setDownloadListener { url, _, contentDisposition, mimeType, _ -> + handleWebViewDownload( + url = url, + mimeType = mimeType, + contentDisposition = contentDisposition + ) + } } /** @@ -351,6 +337,38 @@ class OSIABWebViewActivity : AppCompatActivity() { return OSIABWebChromeClient() } + /** + * Implement the WebKit DownloadListener and handle downloading and previewing PDF files + */ + private fun handleWebViewDownload( + url: String?, + mimeType: String?, + contentDisposition: String? + ) { + if ((mimeType == "application/pdf" || (contentDisposition?.contains(".pdf") == true)) && + (!url.isNullOrEmpty() && !url.startsWith(PDF_VIEWER_URL_PREFIX)) + ) { + lifecycleScope.launch(Dispatchers.IO) { + val pdfFile = try { + OSIABPdfHelper.downloadPdfToCache(this@OSIABWebViewActivity, url) + } catch (_: IOException) { + // can happen if we try to press the "save" button in pdf viewer + // which returns a blob url that we won't be able to download + null + } + if (pdfFile != null) { + withContext(Dispatchers.Main) { + webView.stopLoading() + originalUrl = url + val pdfJsUrl = + PDF_VIEWER_URL_PREFIX + Uri.encode("file://${pdfFile.absolutePath}") + webView.loadUrl(pdfJsUrl) + } + } + } + } + } + /** * Handle permission requests */ @@ -819,7 +837,7 @@ class OSIABWebViewActivity : AppCompatActivity() { if (!showNavigationButtons) { navigationView.removeView(nav) } else defineNavigationButtons(isLeftRight, content) - + if (!showURL) navigationView.removeView(urlText) else defineURLView(url, showNavigationButtons, navigationView, toolbarPosition, isLeftRight) From cb1e5f31eee4452ede89a279fe87b0042a92ed93 Mon Sep 17 00:00:00 2001 From: OS-pedrogustavobilro Date: Thu, 16 Apr 2026 18:32:17 +0100 Subject: [PATCH 4/6] refactor: Remove unnecessary logic from OSIABPdfHelper.kt --- .../helpers/OSIABPdfHelper.kt | 45 +---- .../views/OSIABWebViewActivity.kt | 2 +- .../helpers/OSIABPdfHelperTest.kt | 156 ++---------------- 3 files changed, 20 insertions(+), 183 deletions(-) diff --git a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABPdfHelper.kt b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABPdfHelper.kt index d73feb6..2a44503 100644 --- a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABPdfHelper.kt +++ b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABPdfHelper.kt @@ -3,53 +3,12 @@ package com.outsystems.plugins.inappbrowser.osinappbrowserlib.helpers import android.content.Context import java.io.File import java.io.IOException -import java.net.HttpURLConnection import java.net.URL object OSIABPdfHelper { - interface UrlFactory { - fun create(url: String): URL - } - - private class DefaultUrlFactory : UrlFactory { - override fun create(url: String): URL = URL(url) - } - - fun isContentTypeApplicationPdf(urlString: String): Boolean { - return try { - // Try to identify if the URL is a PDF using a HEAD request. - // If the server does not implement HEAD correctly or does not return the expected content-type, - // fall back to a GET request, since some servers only return the correct type for GET. - if (checkPdfByRequest(urlString, method = "HEAD")) { - true - } else { - checkPdfByRequest(urlString, method = "GET") - } - } catch (_: Exception) { - false - } - } - - fun checkPdfByRequest(urlString: String, method: String, urlFactory: UrlFactory = DefaultUrlFactory()): Boolean { - var conn: HttpURLConnection? = null - return try { - conn = (urlFactory.create(urlString).openConnection() as? HttpURLConnection) - conn?.run { - instanceFollowRedirects = true - requestMethod = method - if (method == "GET") { - setRequestProperty("Range", "bytes=0-0") - } - connect() - val type = contentType?.lowercase() - val disposition = getHeaderField("Content-Disposition")?.lowercase() - type == "application/pdf" || - (type.isNullOrEmpty() && disposition?.contains(".pdf") == true) - } ?: false - } finally { - conn?.disconnect() - } + fun isPdf(mimeType: String?, contentDisposition: String?): Boolean { + return mimeType == "application/pdf" || contentDisposition?.contains(".pdf") == true } @Throws(IOException::class) 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 1f063ba..198ad02 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 @@ -345,7 +345,7 @@ class OSIABWebViewActivity : AppCompatActivity() { mimeType: String?, contentDisposition: String? ) { - if ((mimeType == "application/pdf" || (contentDisposition?.contains(".pdf") == true)) && + if (OSIABPdfHelper.isPdf(mimeType, contentDisposition) && (!url.isNullOrEmpty() && !url.startsWith(PDF_VIEWER_URL_PREFIX)) ) { lifecycleScope.launch(Dispatchers.IO) { diff --git a/src/test/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABPdfHelperTest.kt b/src/test/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABPdfHelperTest.kt index 60bfcbf..55bfe61 100644 --- a/src/test/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABPdfHelperTest.kt +++ b/src/test/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABPdfHelperTest.kt @@ -3,175 +3,51 @@ package com.outsystems.plugins.inappbrowser.osinappbrowserlib.helpers import android.content.Context import io.mockk.every import io.mockk.mockk -import io.mockk.mockkObject -import io.mockk.unmockkObject -import io.mockk.verify import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test -import java.net.HttpURLConnection import java.net.ServerSocket import java.net.Socket -import java.net.URL import java.nio.file.Files import kotlin.concurrent.thread class OSIABPdfHelperTest { - @Test - fun `isContentTypeApplicationPdf returns true if HEAD is PDF`() { - mockkObject(OSIABPdfHelper) - every { OSIABPdfHelper.checkPdfByRequest(any(), "HEAD", any()) } returns true - - val result = OSIABPdfHelper.isContentTypeApplicationPdf("http://example.com") - - assertTrue(result) - verify { OSIABPdfHelper.checkPdfByRequest(any(), "HEAD", any()) } - verify(exactly = 0) { OSIABPdfHelper.checkPdfByRequest(any(), "GET", any()) } - unmockkObject(OSIABPdfHelper) - } + // region isPdf @Test - fun `isContentTypeApplicationPdf falls back to GET if HEAD fails`() { - mockkObject(OSIABPdfHelper) - every { OSIABPdfHelper.checkPdfByRequest(any(), "HEAD", any()) } returns false - every { OSIABPdfHelper.checkPdfByRequest(any(), "GET", any()) } returns true - - val result = OSIABPdfHelper.isContentTypeApplicationPdf("http://example.com") - - assertTrue(result) - verify { OSIABPdfHelper.checkPdfByRequest(any(), "HEAD", any()) } - verify { OSIABPdfHelper.checkPdfByRequest(any(), "GET", any()) } - unmockkObject(OSIABPdfHelper) + fun `isPdf returns true when mimeType is application pdf`() { + assertTrue(OSIABPdfHelper.isPdf("application/pdf", null)) } @Test - fun `isContentTypeApplicationPdf returns false if both HEAD and GET fail`() { - mockkObject(OSIABPdfHelper) - every { OSIABPdfHelper.checkPdfByRequest(any(), "HEAD", any()) } returns false - every { OSIABPdfHelper.checkPdfByRequest(any(), "GET", any()) } returns false - - val result = OSIABPdfHelper.isContentTypeApplicationPdf("http://example.com") - - assertFalse(result) - verify { OSIABPdfHelper.checkPdfByRequest(any(), "HEAD", any()) } - verify { OSIABPdfHelper.checkPdfByRequest(any(), "GET", any()) } - unmockkObject(OSIABPdfHelper) + fun `isPdf returns true when contentDisposition contains pdf extension`() { + assertTrue(OSIABPdfHelper.isPdf(null, "attachment; filename=test.pdf")) } @Test - fun `isContentTypeApplicationPdf returns false if exception occurs`() { - mockkObject(OSIABPdfHelper) - every { - OSIABPdfHelper.checkPdfByRequest( - any(), - any(), - any() - ) - } throws RuntimeException("Network error") - - val result = OSIABPdfHelper.isContentTypeApplicationPdf("http://example.com") - - assertFalse(result) - verify { OSIABPdfHelper.checkPdfByRequest(any(), "HEAD", any()) } - unmockkObject(OSIABPdfHelper) + fun `isPdf returns true when both mimeType and contentDisposition indicate pdf`() { + assertTrue(OSIABPdfHelper.isPdf("application/pdf", "attachment; filename=test.pdf")) } @Test - fun `returns true when content type is application_pdf`() { - val urlFactory = mockk() - val url = mockk() - val conn = mockk(relaxed = true) - every { urlFactory.create(any()) } returns url - every { url.openConnection() } returns conn - every { conn.contentType } returns "application/pdf" - every { conn.connect() } returns Unit - - val result = OSIABPdfHelper.checkPdfByRequest("http://example.com/test.pdf", "HEAD", urlFactory) - - assertTrue(result) - verify { conn.connect() } - verify { conn.disconnect() } + fun `isPdf returns true when mimeType is not pdf but contentDisposition contains pdf extension`() { + assertTrue(OSIABPdfHelper.isPdf("text/html", "attachment; filename=report.pdf")) } @Test - fun `returns true when disposition header contains pdf and content type is empty`() { - val urlFactory = mockk() - val url = mockk() - val conn = mockk(relaxed = true) - every { urlFactory.create(any()) } returns url - every { url.openConnection() } returns conn - every { conn.contentType } returns null - every { conn.getHeaderField("Content-Disposition") } returns "attachment; filename=test.pdf" - every { conn.connect() } returns Unit - - val result = OSIABPdfHelper.checkPdfByRequest("http://example.com/test.pdf", "HEAD", urlFactory) - - assertTrue(result) - verify { conn.connect() } - verify { conn.disconnect() } + fun `isPdf returns false when neither mimeType nor contentDisposition indicate pdf`() { + assertFalse(OSIABPdfHelper.isPdf("text/html", "inline")) } @Test - fun `returns false when neither content type nor disposition indicate pdf`() { - val urlFactory = mockk() - val url = mockk() - val conn = mockk(relaxed = true) - every { urlFactory.create(any()) } returns url - every { url.openConnection() } returns conn - every { conn.contentType } returns "text/html" - every { conn.getHeaderField("Content-Disposition") } returns "inline" - every { conn.connect() } returns Unit - - val result = OSIABPdfHelper.checkPdfByRequest("http://example.com/test.pdf", "HEAD", urlFactory) - - assertFalse(result) - verify { conn.connect() } - verify { conn.disconnect() } + fun `isPdf returns false when both are null`() { + assertFalse(OSIABPdfHelper.isPdf(null, null)) } - @Test - fun `sets Range header for GET method`() { - val urlFactory = mockk() - val url = mockk() - val conn = mockk(relaxed = true) - every { urlFactory.create(any()) } returns url - every { url.openConnection() } returns conn - every { conn.contentType } returns "application/pdf" - every { conn.connect() } returns Unit - - OSIABPdfHelper.checkPdfByRequest("http://example.com/test.pdf", "GET", urlFactory) - - verify { conn.setRequestProperty("Range", "bytes=0-0") } - verify { conn.connect() } - verify { conn.disconnect() } - } - - @Test - fun `returns false if connection is null`() { - val urlFactory = mockk() - val url = mockk() - every { urlFactory.create(any()) } returns url - every { url.openConnection() } returns null + // endregion - val result = OSIABPdfHelper.checkPdfByRequest("http://example.com/test.pdf", "HEAD", urlFactory) - - assertFalse(result) - } - - @Test - fun `returns false if exception is thrown`() { - val urlFactory = mockk() - every { urlFactory.create(any()) } throws RuntimeException("Network error") - - val result = try { - OSIABPdfHelper.checkPdfByRequest("http://example.com/test.pdf", "HEAD", urlFactory) - } catch (_: Exception) { - false - } - - assertFalse(result) - } + // region downloadPdfToCache @Test fun `downloadPdfToCache creates file with content`() { @@ -205,4 +81,6 @@ class OSIABPdfHelperTest { file.delete() cacheDir.deleteRecursively() } + + // endregion } From 81ef976a462713e24066b213049d62663dcdd1d4 Mon Sep 17 00:00:00 2001 From: OS-pedrogustavobilro Date: Thu, 16 Apr 2026 18:44:24 +0100 Subject: [PATCH 5/6] chore(release): Prepare to release 1.6.2 --- CHANGELOG.md | 6 ++++++ pom.xml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fcac34..2c5ed89 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). +## [1.6.2] + +### Fixes + +- Replace HEAD / GET request for checking if file is PDF, with WebKit's [DownloadListener](https://developer.android.com/reference/android/webkit/DownloadListener). This makes sure that for non-PDF urls, no extra request is done [RMET-5141](https://outsystemsrd.atlassian.net/browse/RMET-5141) / [RPM-6744](https://outsystemsrd.atlassian.net/browse/RPM-6744) + ## [1.6.1] ### Fixes diff --git a/pom.xml b/pom.xml index 3ab5e59..b7ba517 100644 --- a/pom.xml +++ b/pom.xml @@ -6,5 +6,5 @@ 4.0.0 io.ionic.libs ioninappbrowser-android - 1.6.1 + 1.6.2 From 2d376a9b0cff66766b1967ac3e94be2b3286cc7c Mon Sep 17 00:00:00 2001 From: Pedro Bilro Date: Fri, 17 Apr 2026 09:28:07 +0100 Subject: [PATCH 6/6] chore: Only send http headers to loadUrl if available Following PR comments https://github.com/OutSystems/OSInAppBrowserLib-Android/pull/56#pullrequestreview-4123529461 Co-authored-by: OS-ruimoreiramendes --- .../osinappbrowserlib/views/OSIABWebViewActivity.kt | 6 +++++- 1 file changed, 5 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 198ad02..cbb63bf 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 @@ -254,7 +254,11 @@ class OSIABWebViewActivity : AppCompatActivity() { } private fun handleLoadUrl(url: String, additionalHttpHeaders: Map? = null) { - webView.loadUrl(url, additionalHttpHeaders ?: emptyMap()) + if (additionalHttpHeaders.isNullOrEmpty()) { + webView.loadUrl(url) + } else { + webView.loadUrl(url, additionalHttpHeaders) + } }