Skip to content

Commit 0299b92

Browse files
committed
feat: add support for pdf
1 parent 67e46a1 commit 0299b92

3 files changed

Lines changed: 192 additions & 20 deletions

File tree

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,7 @@ lint/tmp/
8787
.DS_Store
8888
Gemfile.lock
8989

90-
scripts/tmp
90+
scripts/tmp
91+
92+
# PDF.js files
93+
src/main/assets/pdfjs/

build.gradle

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,86 @@ dependencies {
141141

142142
if (System.getenv("SHOULD_PUBLISH") == "true") {
143143
apply from: file("./scripts/publish-module.gradle")
144-
}
144+
}
145+
146+
import java.net.URL
147+
148+
def pdfJsVersion = "5.4.54"
149+
def pdfJsUrl = "https://github.com/mozilla/pdf.js/releases/download/v${pdfJsVersion}/pdfjs-${pdfJsVersion}-dist.zip"
150+
def pdfJsDestDir = "$projectDir/src/main/assets/pdfjs"
151+
def pdfJsCacheDir = new File(gradle.gradleUserHomeDir, "pdfjs-cache/${pdfJsVersion}")
152+
153+
tasks.register("downloadPdfJs") {
154+
description = "Downloads and caches PDF.js distribution files"
155+
group = "build setup"
156+
157+
// Define inputs/outputs for up-to-date checking
158+
inputs.property("pdfJsVersion", pdfJsVersion)
159+
outputs.dir(pdfJsDestDir)
160+
161+
doLast {
162+
def destDir = file(pdfJsDestDir)
163+
164+
// Check if destination already has files
165+
if (destDir.exists() && destDir.listFiles()?.size() > 0) {
166+
logger.info("PDF.js files already exist in ${destDir}")
167+
return
168+
}
169+
170+
// Check if cache has files and copy them
171+
if (pdfJsCacheDir.exists() && pdfJsCacheDir.listFiles()?.size() > 0) {
172+
logger.info("Copying PDF.js from cache: ${pdfJsCacheDir}")
173+
copy {
174+
from pdfJsCacheDir
175+
into destDir
176+
}
177+
return
178+
}
179+
180+
// Download PDF.js
181+
logger.info("Downloading PDF.js v${pdfJsVersion} from ${pdfJsUrl}")
182+
def zipFile = file("$buildDir/pdfjs-${pdfJsVersion}.zip")
183+
184+
try {
185+
// Ensure build directory exists
186+
zipFile.parentFile.mkdirs()
187+
188+
// Download with better error handling
189+
new URL(pdfJsUrl).withInputStream { inputStream ->
190+
zipFile.withOutputStream { outputStream ->
191+
outputStream << inputStream
192+
}
193+
}
194+
195+
logger.info("Downloaded PDF.js to: ${zipFile}")
196+
197+
// Extract to cache
198+
pdfJsCacheDir.mkdirs()
199+
copy {
200+
from zipTree(zipFile)
201+
into pdfJsCacheDir
202+
}
203+
204+
// Copy from cache to destination
205+
if (destDir.exists()) {
206+
delete destDir
207+
}
208+
destDir.mkdirs()
209+
copy {
210+
from pdfJsCacheDir
211+
into destDir
212+
}
213+
214+
logger.info("PDF.js extracted to: ${destDir}")
215+
216+
// Clean up downloaded zip
217+
delete zipFile
218+
219+
} catch (Exception e) {
220+
logger.error("Failed to download PDF.js: ${e.message}")
221+
throw new GradleException("Could not download PDF.js from ${pdfJsUrl}", e)
222+
}
223+
}
224+
}
225+
226+
preBuild.dependsOn(downloadPdfJs)

src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/views/OSIABWebViewActivity.kt

Lines changed: 105 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import android.Manifest
44
import android.app.Activity
55
import android.content.Intent
66
import android.content.pm.PackageManager
7+
import android.graphics.Bitmap
78
import android.net.Uri
89
import android.os.Build
910
import android.os.Bundle
10-
import android.view.Gravity
1111
import android.util.Log
12-
import android.graphics.Bitmap
12+
import android.view.Gravity
1313
import android.view.View
1414
import android.webkit.CookieManager
1515
import android.webkit.GeolocationPermissions
@@ -40,7 +40,13 @@ import com.outsystems.plugins.inappbrowser.osinappbrowserlib.OSIABEvents.OSIABWe
4040
import com.outsystems.plugins.inappbrowser.osinappbrowserlib.R
4141
import com.outsystems.plugins.inappbrowser.osinappbrowserlib.models.OSIABToolbarPosition
4242
import com.outsystems.plugins.inappbrowser.osinappbrowserlib.models.OSIABWebViewOptions
43+
import kotlinx.coroutines.Dispatchers
4344
import kotlinx.coroutines.launch
45+
import kotlinx.coroutines.withContext
46+
import java.io.File
47+
import java.io.IOException
48+
import java.net.HttpURLConnection
49+
import java.net.URL
4450

4551
class OSIABWebViewActivity : AppCompatActivity() {
4652

@@ -88,6 +94,11 @@ class OSIABWebViewActivity : AppCompatActivity() {
8894
// for back navigation
8995
private lateinit var onBackPressedCallback: OnBackPressedCallback
9096

97+
private val PDF_VIEWER_URL_PREFIX = "file:///android_asset/pdfjs/web/viewer.html?file="
98+
// the original URL of the PDF file, used to display it correctly in the view
99+
// and to send the correct URL in the browserPageNavigationCompleted event
100+
private var originalUrl: String? = null
101+
91102
companion object {
92103
const val WEB_VIEW_URL_EXTRA = "WEB_VIEW_URL_EXTRA"
93104
const val WEB_VIEW_OPTIONS_EXTRA = "WEB_VIEW_OPTIONS_EXTRA"
@@ -173,7 +184,7 @@ class OSIABWebViewActivity : AppCompatActivity() {
173184

174185
setupWebView()
175186
if (urlToOpen != null) {
176-
webView.loadUrl(urlToOpen, customHeaders ?: emptyMap())
187+
handleLoadUrl(urlToOpen, customHeaders)
177188
showLoadingScreen()
178189
}
179190

@@ -206,6 +217,71 @@ class OSIABWebViewActivity : AppCompatActivity() {
206217
}
207218
}
208219

220+
private fun handleLoadUrl(url: String, additionalHttpHeaders: Map<String, String>? = null) {
221+
lifecycleScope.launch(Dispatchers.IO) {
222+
if (isContentTypeApplicationPdf(url)) {
223+
val pdfFile = try { downloadPdfToCache(url) } catch (_: IOException) { null }
224+
if (pdfFile != null) {
225+
withContext(Dispatchers.Main) {
226+
webView.stopLoading()
227+
originalUrl = url
228+
val pdfJsUrl =
229+
PDF_VIEWER_URL_PREFIX + Uri.encode("file://${pdfFile.absolutePath}")
230+
webView.loadUrl(pdfJsUrl)
231+
}
232+
return@launch
233+
}
234+
}
235+
236+
withContext(Dispatchers.Main) {
237+
webView.loadUrl(url, additionalHttpHeaders ?: emptyMap())
238+
}
239+
}
240+
}
241+
242+
fun isContentTypeApplicationPdf(urlString: String): Boolean {
243+
return try {
244+
if (checkPdfByRequest(urlString, method = "HEAD")) {
245+
true
246+
} else {
247+
checkPdfByRequest(urlString, method = "GET")
248+
}
249+
} catch (_: Exception) {
250+
false
251+
}
252+
}
253+
254+
private fun checkPdfByRequest(urlString: String, method: String): Boolean {
255+
var conn: HttpURLConnection? = null
256+
return try {
257+
conn = (URL(urlString).openConnection() as? HttpURLConnection)
258+
conn?.run {
259+
instanceFollowRedirects = true
260+
requestMethod = method
261+
if (method == "GET") {
262+
setRequestProperty("Range", "bytes=0-0")
263+
}
264+
connect()
265+
val type = contentType?.lowercase()
266+
val disposition = getHeaderField("Content-Disposition")?.lowercase()
267+
type == "application/pdf" ||
268+
(type.isNullOrEmpty() && disposition?.contains(".pdf") == true)
269+
} ?: false
270+
} finally {
271+
conn?.disconnect()
272+
}
273+
}
274+
275+
private fun downloadPdfToCache(url: String): File {
276+
val pdfFile = File(cacheDir, "temp_${System.currentTimeMillis()}.pdf")
277+
URL(url).openStream().use { input ->
278+
pdfFile.outputStream().use { output ->
279+
input.copyTo(output)
280+
}
281+
}
282+
return pdfFile
283+
}
284+
209285
/**
210286
* Helper function to update navigation button states
211287
*/
@@ -232,19 +308,24 @@ class OSIABWebViewActivity : AppCompatActivity() {
232308
* It also deals with URLs that are opened withing the WebView.
233309
*/
234310
private fun setupWebView() {
235-
webView.settings.javaScriptEnabled = true
236-
webView.settings.javaScriptCanOpenWindowsAutomatically = true
237-
webView.settings.databaseEnabled = true
238-
webView.settings.domStorageEnabled = true
239-
webView.settings.loadWithOverviewMode = true
240-
webView.settings.useWideViewPort = true
311+
webView.settings.apply {
312+
javaScriptEnabled = true
313+
javaScriptCanOpenWindowsAutomatically = true
314+
databaseEnabled = true
315+
domStorageEnabled = true
316+
loadWithOverviewMode = true
317+
useWideViewPort = true
318+
allowFileAccess = true
319+
allowFileAccessFromFileURLs = true
320+
allowUniversalAccessFromFileURLs = true
241321

242-
if (!options.customUserAgent.isNullOrEmpty())
243-
webView.settings.userAgentString = options.customUserAgent
322+
if (!options.customUserAgent.isNullOrEmpty())
323+
userAgentString = options.customUserAgent
244324

245-
// get webView settings that come from options
246-
webView.settings.builtInZoomControls = options.allowZoom
247-
webView.settings.mediaPlaybackRequiresUserGesture = options.mediaPlaybackRequiresUserAction
325+
// get webView settings that come from options
326+
builtInZoomControls = options.allowZoom
327+
mediaPlaybackRequiresUserGesture = options.mediaPlaybackRequiresUserAction
328+
}
248329

249330
// setup WebViewClient and WebChromeClient
250331
webView.webViewClient =
@@ -321,11 +402,17 @@ class OSIABWebViewActivity : AppCompatActivity() {
321402
}
322403

323404
override fun onPageFinished(view: WebView?, url: String?) {
405+
val resolvedUrl = when {
406+
url == null -> null
407+
url.startsWith(PDF_VIEWER_URL_PREFIX) && originalUrl != null -> originalUrl
408+
else -> url
409+
}
410+
324411
if (isFirstLoad && !hasLoadError) {
325412
sendWebViewEvent(OSIABEvents.BrowserPageLoaded(browserId))
326413
isFirstLoad = false
327414
} else if (!hasLoadError) {
328-
sendWebViewEvent(OSIABEvents.BrowserPageNavigationCompleted(browserId, url))
415+
sendWebViewEvent(OSIABEvents.BrowserPageNavigationCompleted(browserId, resolvedUrl))
329416
}
330417

331418
// set back to false so that the next successful load
@@ -335,7 +422,7 @@ class OSIABWebViewActivity : AppCompatActivity() {
335422
// store cookies after page finishes loading
336423
storeCookies()
337424
if (hasNavigationButtons) updateNavigationButtons()
338-
if (showURL) urlText.text = url
425+
if (showURL) urlText.text = resolvedUrl
339426
currentUrl = url
340427
super.onPageFinished(view, url)
341428
}
@@ -368,7 +455,7 @@ class OSIABWebViewActivity : AppCompatActivity() {
368455
}
369456
// handle every http and https link by loading it in the WebView
370457
urlString.startsWith("http:") || urlString.startsWith("https:") -> {
371-
view?.loadUrl(urlString)
458+
handleLoadUrl(urlString)
372459
if (showURL) urlText.text = urlString
373460
true
374461
}
@@ -646,7 +733,7 @@ class OSIABWebViewActivity : AppCompatActivity() {
646733
return findViewById<Button?>(R.id.reload_button).apply {
647734
setOnClickListener {
648735
currentUrl?.let {
649-
webView.loadUrl(it)
736+
handleLoadUrl(it)
650737
showLoadingScreen()
651738
}
652739
}

0 commit comments

Comments
 (0)