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 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 59ec090..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 @@ -218,6 +218,7 @@ class OSIABWebViewActivity : AppCompatActivity() { enableThirdPartyCookies() setupWebView() + if (urlToOpen != null) { handleLoadUrl(urlToOpen, customHeaders) showLoadingScreen() @@ -253,24 +254,10 @@ 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()) - } + if (additionalHttpHeaders.isNullOrEmpty()) { + webView.loadUrl(url) + } else { + webView.loadUrl(url, additionalHttpHeaders) } } @@ -327,6 +314,14 @@ class OSIABWebViewActivity : AppCompatActivity() { options.showURL && options.showToolbar ) webView.webChromeClient = customWebChromeClient() + + webView.setDownloadListener { url, _, contentDisposition, mimeType, _ -> + handleWebViewDownload( + url = url, + mimeType = mimeType, + contentDisposition = contentDisposition + ) + } } /** @@ -346,6 +341,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 (OSIABPdfHelper.isPdf(mimeType, contentDisposition) && + (!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 */ @@ -814,7 +841,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) 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 }