From 536e8544d040973e94c0df4732202da1d5b0b3e9 Mon Sep 17 00:00:00 2001 From: kaaviya98 Date: Tue, 14 Oct 2025 15:32:40 +0530 Subject: [PATCH 1/5] fix: Expose cached AI pdf data url to webview --- .../course/fragments/AIChatPdfFragment.kt | 21 +++++++++ .../fragments/DocumentViewerFragment.kt | 1 + .../course/util/CachedPdfPathProvider.kt | 46 +++++++++++++++++++ .../course/util/PDFDownloadManager.kt | 4 ++ .../core/TestpressCoreSampleActivity.java | 4 +- 5 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 course/src/main/java/in/testpress/course/util/CachedPdfPathProvider.kt diff --git a/course/src/main/java/in/testpress/course/fragments/AIChatPdfFragment.kt b/course/src/main/java/in/testpress/course/fragments/AIChatPdfFragment.kt index 25e41e33a..f894d5e59 100644 --- a/course/src/main/java/in/testpress/course/fragments/AIChatPdfFragment.kt +++ b/course/src/main/java/in/testpress/course/fragments/AIChatPdfFragment.kt @@ -1,6 +1,7 @@ package `in`.testpress.course.fragments import `in`.testpress.course.R +import `in`.testpress.course.util.CachedPdfPathProvider import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -18,6 +19,7 @@ class AIChatPdfFragment : Fragment() { companion object { private const val ARG_CONTENT_ID = "contentId" private const val ARG_COURSE_ID = "courseId" + private const val ARG_PDF_PATH = "pdfPath" } override fun onCreateView( @@ -47,6 +49,12 @@ class AIChatPdfFragment : Fragment() { putBoolean(IS_AUTHENTICATION_REQUIRED, true) } + webViewFragment.setListener(object : WebViewFragment.Listener { + override fun onWebViewInitializationSuccess() { + setupJavaScriptInterface(webViewFragment) + } + }) + childFragmentManager.beginTransaction() .replace(R.id.aiPdf_view_fragment, webViewFragment) .commit() @@ -59,4 +67,17 @@ class AIChatPdfFragment : Fragment() { ?: throw IllegalStateException("Base URL not configured.") return "$baseUrl/courses/$courseId/contents/$contentId/?content_detail_v2=true" } + + private fun setupJavaScriptInterface(webViewFragment: WebViewFragment) { + val pdfPath = requireArguments().getString(ARG_PDF_PATH, "") + webViewFragment.webView.settings.apply { + allowFileAccessFromFileURLs = true + allowUniversalAccessFromFileURLs = true + } + + webViewFragment.addJavascriptInterface( + CachedPdfPathProvider(requireActivity(), pdfPath), + "AndroidPdfCache" + ) + } } diff --git a/course/src/main/java/in/testpress/course/fragments/DocumentViewerFragment.kt b/course/src/main/java/in/testpress/course/fragments/DocumentViewerFragment.kt index b67ff31d8..77bf4f6b9 100644 --- a/course/src/main/java/in/testpress/course/fragments/DocumentViewerFragment.kt +++ b/course/src/main/java/in/testpress/course/fragments/DocumentViewerFragment.kt @@ -119,6 +119,7 @@ class DocumentViewerFragment : BaseContentDetailFragment(), PdfDownloadListener, val args = Bundle() args.putLong("contentId", contentId) args.putLong("courseId", content.courseId ?: -1L) + args.putString("pdfPath", pdfDownloadManager.getCachedPdfPath()) aiChatFragment?.arguments = args } diff --git a/course/src/main/java/in/testpress/course/util/CachedPdfPathProvider.kt b/course/src/main/java/in/testpress/course/util/CachedPdfPathProvider.kt new file mode 100644 index 000000000..471949d2a --- /dev/null +++ b/course/src/main/java/in/testpress/course/util/CachedPdfPathProvider.kt @@ -0,0 +1,46 @@ +package `in`.testpress.course.util + +import android.app.Activity +import `in`.testpress.util.BaseJavaScriptInterface +import android.webkit.JavascriptInterface +import java.io.File +import java.io.FileInputStream +import android.util.Base64 + +class CachedPdfPathProvider( + activity: Activity, + private val pdfPath: String +) : BaseJavaScriptInterface(activity) { + + @JavascriptInterface + fun isPDFCached(): Boolean { + return !pdfPath.isEmpty() && File(pdfPath).exists() + } + + @JavascriptInterface + fun getBase64PdfData(): String { + return if (isPDFCached()) { + try { + val file = File(pdfPath) + val inputStream = FileInputStream(file) + val bytes = inputStream.readBytes() + inputStream.close() + Base64.encodeToString(bytes, Base64.DEFAULT) + } catch (e: Exception) { + "" + } + } else { + "" + } + } + + @JavascriptInterface + fun getBase64PdfDataUrl(): String { + val base64Data = getBase64PdfData() + return if (base64Data.isNotEmpty()) { + "data:application/pdf;base64,$base64Data" + } else { + "" + } + } +} diff --git a/course/src/main/java/in/testpress/course/util/PDFDownloadManager.kt b/course/src/main/java/in/testpress/course/util/PDFDownloadManager.kt index e72ea3571..fbe1bbdcc 100644 --- a/course/src/main/java/in/testpress/course/util/PDFDownloadManager.kt +++ b/course/src/main/java/in/testpress/course/util/PDFDownloadManager.kt @@ -73,6 +73,10 @@ open class PDFDownloadManager( fun get(): File { return fileEncryptAndDecryptUtil.decrypt() } + + fun getCachedPdfPath(): String { + return if (isDownloaded()) get().absolutePath else "" + } } interface PdfDownloadListener { diff --git a/samples/src/main/java/in/testpress/samples/core/TestpressCoreSampleActivity.java b/samples/src/main/java/in/testpress/samples/core/TestpressCoreSampleActivity.java index 9ba2a1ce7..d02d20506 100644 --- a/samples/src/main/java/in/testpress/samples/core/TestpressCoreSampleActivity.java +++ b/samples/src/main/java/in/testpress/samples/core/TestpressCoreSampleActivity.java @@ -119,8 +119,8 @@ public void onClick(View view) { } private void authenticate(String userId, String accessToken, TestpressSdk.Provider provider) { - InstituteSettings instituteSettings = new InstituteSettings("https://sandbox.testpress.in"); - instituteSettings.setWhiteLabeledHostUrl("https://sandbox.testpress.in"); + InstituteSettings instituteSettings = new InstituteSettings("https://staging.testpress.in"); + instituteSettings.setWhiteLabeledHostUrl("https://staging.testpress.in"); instituteSettings.setAndroidSentryDns("https://35dcf0dbd28045628831e62dd959ae4b@sentry.testpress.in/5"); instituteSettings.setEnableOfflineExam(true); instituteSettings.setUseNewDiscountFeat(false); From 201c27b230dee41657c1898cb3bd5617c6506605 Mon Sep 17 00:00:00 2001 From: kaaviya98 Date: Tue, 14 Oct 2025 15:40:51 +0530 Subject: [PATCH 2/5] add-logging --- .../course/fragments/AIChatPdfFragment.kt | 5 ++++ .../course/util/CachedPdfPathProvider.kt | 25 ++++++++++++++++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/course/src/main/java/in/testpress/course/fragments/AIChatPdfFragment.kt b/course/src/main/java/in/testpress/course/fragments/AIChatPdfFragment.kt index f894d5e59..efb76f069 100644 --- a/course/src/main/java/in/testpress/course/fragments/AIChatPdfFragment.kt +++ b/course/src/main/java/in/testpress/course/fragments/AIChatPdfFragment.kt @@ -3,6 +3,7 @@ package `in`.testpress.course.fragments import `in`.testpress.course.R import `in`.testpress.course.util.CachedPdfPathProvider import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -70,6 +71,8 @@ class AIChatPdfFragment : Fragment() { private fun setupJavaScriptInterface(webViewFragment: WebViewFragment) { val pdfPath = requireArguments().getString(ARG_PDF_PATH, "") + Log.d("AIChatPdfFragment", "Setting up JavaScript interface with PDF path: $pdfPath") + webViewFragment.webView.settings.apply { allowFileAccessFromFileURLs = true allowUniversalAccessFromFileURLs = true @@ -79,5 +82,7 @@ class AIChatPdfFragment : Fragment() { CachedPdfPathProvider(requireActivity(), pdfPath), "AndroidPdfCache" ) + + Log.d("AIChatPdfFragment", "JavaScript interface 'AndroidPdfCache' added successfully") } } diff --git a/course/src/main/java/in/testpress/course/util/CachedPdfPathProvider.kt b/course/src/main/java/in/testpress/course/util/CachedPdfPathProvider.kt index 471949d2a..e37b4e45a 100644 --- a/course/src/main/java/in/testpress/course/util/CachedPdfPathProvider.kt +++ b/course/src/main/java/in/testpress/course/util/CachedPdfPathProvider.kt @@ -6,41 +6,60 @@ import android.webkit.JavascriptInterface import java.io.File import java.io.FileInputStream import android.util.Base64 +import android.util.Log class CachedPdfPathProvider( activity: Activity, private val pdfPath: String ) : BaseJavaScriptInterface(activity) { + init { + Log.d("CachedPdfPathProvider", "Initialized with PDF path: $pdfPath") + Log.d("CachedPdfPathProvider", "PDF file exists: ${File(pdfPath).exists()}") + Log.d("CachedPdfPathProvider", "PDF file size: ${if (File(pdfPath).exists()) File(pdfPath).length() else "N/A"} bytes") + } + @JavascriptInterface fun isPDFCached(): Boolean { - return !pdfPath.isEmpty() && File(pdfPath).exists() + val exists = !pdfPath.isEmpty() && File(pdfPath).exists() + Log.d("CachedPdfPathProvider", "isPDFCached() called - Result: $exists") + return exists } @JavascriptInterface fun getBase64PdfData(): String { + Log.d("CachedPdfPathProvider", "getBase64PdfData() called") return if (isPDFCached()) { try { val file = File(pdfPath) val inputStream = FileInputStream(file) val bytes = inputStream.readBytes() inputStream.close() - Base64.encodeToString(bytes, Base64.DEFAULT) + val base64Data = Base64.encodeToString(bytes, Base64.DEFAULT) + Log.d("CachedPdfPathProvider", "Base64 data length: ${base64Data.length} characters") + Log.d("CachedPdfPathProvider", "Base64 data preview: ${base64Data.take(100)}...") + return base64Data } catch (e: Exception) { + Log.e("CachedPdfPathProvider", "Error reading PDF file", e) "" } } else { + Log.d("CachedPdfPathProvider", "PDF not cached, returning empty string") "" } } @JavascriptInterface fun getBase64PdfDataUrl(): String { + Log.d("CachedPdfPathProvider", "getBase64PdfDataUrl() called") val base64Data = getBase64PdfData() - return if (base64Data.isNotEmpty()) { + val dataUrl = if (base64Data.isNotEmpty()) { "data:application/pdf;base64,$base64Data" } else { "" } + Log.d("CachedPdfPathProvider", "Data URL length: ${dataUrl.length} characters") + Log.d("CachedPdfPathProvider", "Data URL preview: ${dataUrl.take(150)}...") + return dataUrl } } From 454ff56a527a20ca08fe49cd86179c3014cc946d Mon Sep 17 00:00:00 2001 From: kaaviya98 Date: Tue, 14 Oct 2025 16:36:54 +0530 Subject: [PATCH 3/5] add-streaming --- .../course/fragments/AIChatPdfFragment.kt | 44 ++++++- .../course/util/CachedPdfPathProvider.kt | 65 ---------- .../course/util/EnhancedPdfProvider.kt | 53 ++++++++ .../testpress/course/util/PdfCacheManager.kt | 121 ++++++++++++++++++ .../testpress/course/util/PdfWebViewClient.kt | 118 +++++++++++++++++ 5 files changed, 330 insertions(+), 71 deletions(-) delete mode 100644 course/src/main/java/in/testpress/course/util/CachedPdfPathProvider.kt create mode 100644 course/src/main/java/in/testpress/course/util/EnhancedPdfProvider.kt create mode 100644 course/src/main/java/in/testpress/course/util/PdfCacheManager.kt create mode 100644 course/src/main/java/in/testpress/course/util/PdfWebViewClient.kt diff --git a/course/src/main/java/in/testpress/course/fragments/AIChatPdfFragment.kt b/course/src/main/java/in/testpress/course/fragments/AIChatPdfFragment.kt index efb76f069..37a72771e 100644 --- a/course/src/main/java/in/testpress/course/fragments/AIChatPdfFragment.kt +++ b/course/src/main/java/in/testpress/course/fragments/AIChatPdfFragment.kt @@ -1,7 +1,9 @@ package `in`.testpress.course.fragments import `in`.testpress.course.R -import `in`.testpress.course.util.CachedPdfPathProvider +import `in`.testpress.course.util.PdfCacheManager +import `in`.testpress.course.util.PdfWebViewClient +import `in`.testpress.course.util.EnhancedPdfProvider import android.os.Bundle import android.util.Log import android.view.LayoutInflater @@ -23,6 +25,9 @@ class AIChatPdfFragment : Fragment() { private const val ARG_PDF_PATH = "pdfPath" } + private lateinit var pdfCacheManager: PdfCacheManager + private var pdfId: String? = null + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -36,11 +41,26 @@ class AIChatPdfFragment : Fragment() { val contentId = requireArguments().getLong(ARG_CONTENT_ID, -1L) val courseId = requireArguments().getLong(ARG_COURSE_ID, -1L) + val pdfPath = requireArguments().getString(ARG_PDF_PATH, "") if (contentId == -1L || courseId == -1L) { throw IllegalArgumentException("Required arguments (contentId, courseId) are missing or invalid.") } + // Initialize PDF cache manager + pdfCacheManager = PdfCacheManager.getInstance(requireContext()) + + // Register PDF for streaming if path is provided + if (pdfPath.isNotEmpty()) { + try { + pdfId = pdfCacheManager.registerPdf(pdfPath) + Log.d("AIChatPdfFragment", "Registered PDF for streaming with ID: $pdfId") + } catch (e: Exception) { + Log.e("AIChatPdfFragment", "Failed to register PDF for streaming", e) + // Fall back to original URL-based approach + } + } + val webViewFragment = WebViewFragment() val pdfUrl = getPdfUrl(courseId, contentId) @@ -52,6 +72,7 @@ class AIChatPdfFragment : Fragment() { webViewFragment.setListener(object : WebViewFragment.Listener { override fun onWebViewInitializationSuccess() { + setupWebViewClient(webViewFragment) setupJavaScriptInterface(webViewFragment) } }) @@ -69,6 +90,12 @@ class AIChatPdfFragment : Fragment() { return "$baseUrl/courses/$courseId/contents/$contentId/?content_detail_v2=true" } + private fun setupWebViewClient(webViewFragment: WebViewFragment) { + // Replace the default WebViewClient with our PDF streaming client + webViewFragment.webView.webViewClient = PdfWebViewClient(webViewFragment, requireContext()) + Log.d("AIChatPdfFragment", "PDF streaming WebViewClient set up successfully") + } + private fun setupJavaScriptInterface(webViewFragment: WebViewFragment) { val pdfPath = requireArguments().getString(ARG_PDF_PATH, "") Log.d("AIChatPdfFragment", "Setting up JavaScript interface with PDF path: $pdfPath") @@ -78,11 +105,16 @@ class AIChatPdfFragment : Fragment() { allowUniversalAccessFromFileURLs = true } - webViewFragment.addJavascriptInterface( - CachedPdfPathProvider(requireActivity(), pdfPath), - "AndroidPdfCache" - ) + // Create enhanced JavaScript interface that provides both streaming URL and base64 fallback + val jsInterface = EnhancedPdfProvider(requireActivity(), pdfPath, pdfId) + webViewFragment.addJavascriptInterface(jsInterface, "AndroidPdfCache") - Log.d("AIChatPdfFragment", "JavaScript interface 'AndroidPdfCache' added successfully") + Log.d("AIChatPdfFragment", "Enhanced JavaScript interface 'AndroidPdfCache' added successfully") + } + + override fun onDestroy() { + super.onDestroy() + // Clean up registered PDF + pdfId?.let { pdfCacheManager.unregisterPdf(it) } } } diff --git a/course/src/main/java/in/testpress/course/util/CachedPdfPathProvider.kt b/course/src/main/java/in/testpress/course/util/CachedPdfPathProvider.kt deleted file mode 100644 index e37b4e45a..000000000 --- a/course/src/main/java/in/testpress/course/util/CachedPdfPathProvider.kt +++ /dev/null @@ -1,65 +0,0 @@ -package `in`.testpress.course.util - -import android.app.Activity -import `in`.testpress.util.BaseJavaScriptInterface -import android.webkit.JavascriptInterface -import java.io.File -import java.io.FileInputStream -import android.util.Base64 -import android.util.Log - -class CachedPdfPathProvider( - activity: Activity, - private val pdfPath: String -) : BaseJavaScriptInterface(activity) { - - init { - Log.d("CachedPdfPathProvider", "Initialized with PDF path: $pdfPath") - Log.d("CachedPdfPathProvider", "PDF file exists: ${File(pdfPath).exists()}") - Log.d("CachedPdfPathProvider", "PDF file size: ${if (File(pdfPath).exists()) File(pdfPath).length() else "N/A"} bytes") - } - - @JavascriptInterface - fun isPDFCached(): Boolean { - val exists = !pdfPath.isEmpty() && File(pdfPath).exists() - Log.d("CachedPdfPathProvider", "isPDFCached() called - Result: $exists") - return exists - } - - @JavascriptInterface - fun getBase64PdfData(): String { - Log.d("CachedPdfPathProvider", "getBase64PdfData() called") - return if (isPDFCached()) { - try { - val file = File(pdfPath) - val inputStream = FileInputStream(file) - val bytes = inputStream.readBytes() - inputStream.close() - val base64Data = Base64.encodeToString(bytes, Base64.DEFAULT) - Log.d("CachedPdfPathProvider", "Base64 data length: ${base64Data.length} characters") - Log.d("CachedPdfPathProvider", "Base64 data preview: ${base64Data.take(100)}...") - return base64Data - } catch (e: Exception) { - Log.e("CachedPdfPathProvider", "Error reading PDF file", e) - "" - } - } else { - Log.d("CachedPdfPathProvider", "PDF not cached, returning empty string") - "" - } - } - - @JavascriptInterface - fun getBase64PdfDataUrl(): String { - Log.d("CachedPdfPathProvider", "getBase64PdfDataUrl() called") - val base64Data = getBase64PdfData() - val dataUrl = if (base64Data.isNotEmpty()) { - "data:application/pdf;base64,$base64Data" - } else { - "" - } - Log.d("CachedPdfPathProvider", "Data URL length: ${dataUrl.length} characters") - Log.d("CachedPdfPathProvider", "Data URL preview: ${dataUrl.take(150)}...") - return dataUrl - } -} diff --git a/course/src/main/java/in/testpress/course/util/EnhancedPdfProvider.kt b/course/src/main/java/in/testpress/course/util/EnhancedPdfProvider.kt new file mode 100644 index 000000000..f90d04afb --- /dev/null +++ b/course/src/main/java/in/testpress/course/util/EnhancedPdfProvider.kt @@ -0,0 +1,53 @@ +package `in`.testpress.course.util + +import android.app.Activity +import `in`.testpress.util.BaseJavaScriptInterface +import android.webkit.JavascriptInterface +import android.util.Log +import java.io.File + +class EnhancedPdfProvider( + activity: Activity, + private val pdfPath: String, + private val pdfId: String? +) : BaseJavaScriptInterface(activity) { + + companion object { + private const val TAG = "EnhancedPdfProvider" + } + + init { + Log.d(TAG, "Initialized with PDF path: $pdfPath, PDF ID: $pdfId") + } + + @JavascriptInterface + fun isPDFCached(): Boolean { + val exists = !pdfPath.isEmpty() && File(pdfPath).exists() + Log.d(TAG, "isPDFCached() called - Result: $exists") + return exists + } + + @JavascriptInterface + fun getStreamingUrl(): String { + return if (pdfId != null && isPDFCached()) { + val url = "https://local.pdf/$pdfId" + Log.d(TAG, "getStreamingUrl() called - Result: $url") + url + } else { + Log.d(TAG, "getStreamingUrl() called - No PDF ID available, returning empty") + "" + } + } + + @JavascriptInterface + fun getPdfInfo(): String { + val info = mapOf( + "isCached" to isPDFCached(), + "hasStreamingUrl" to (pdfId != null), + "streamingUrl" to getStreamingUrl(), + "fileSize" to if (isPDFCached()) File(pdfPath).length() else 0L + ) + Log.d(TAG, "getPdfInfo() called - Result: $info") + return info.toString() + } +} diff --git a/course/src/main/java/in/testpress/course/util/PdfCacheManager.kt b/course/src/main/java/in/testpress/course/util/PdfCacheManager.kt new file mode 100644 index 000000000..b2d740ee6 --- /dev/null +++ b/course/src/main/java/in/testpress/course/util/PdfCacheManager.kt @@ -0,0 +1,121 @@ +package `in`.testpress.course.util + +import android.content.Context +import android.util.Log +import java.io.File +import java.security.MessageDigest +import java.util.concurrent.ConcurrentHashMap + +class PdfCacheManager private constructor(private val context: Context) { + + companion object { + private const val TAG = "PdfCacheManager" + private const val CACHE_DIR_NAME = "pdf-cache" + + @Volatile + private var INSTANCE: PdfCacheManager? = null + + fun getInstance(context: Context): PdfCacheManager { + return INSTANCE ?: synchronized(this) { + INSTANCE ?: PdfCacheManager(context.applicationContext).also { INSTANCE = it } + } + } + } + + private val pdfIdToPathMap = ConcurrentHashMap() + private val cacheDir: File by lazy { + File(context.cacheDir, CACHE_DIR_NAME).apply { + if (!exists()) { + mkdirs() + } + } + } + + /** + * Register a PDF file with a unique ID for local streaming + * @param pdfPath The actual file path of the PDF + * @return A unique PDF ID that can be used in https://local.pdf/{pdfId} URLs + */ + fun registerPdf(pdfPath: String): String { + val pdfFile = File(pdfPath) + if (!pdfFile.exists() || !pdfFile.isFile) { + throw IllegalArgumentException("PDF file does not exist: $pdfPath") + } + + // Generate a unique ID based on file path and modification time + val uniqueId = generateUniqueId(pdfPath, pdfFile.lastModified()) + + // Copy file to cache directory if not already there + val cachedFile = File(cacheDir, "$uniqueId.pdf") + if (!cachedFile.exists()) { + try { + pdfFile.copyTo(cachedFile, overwrite = true) + Log.d(TAG, "Cached PDF: $pdfPath -> ${cachedFile.absolutePath}") + } catch (e: Exception) { + Log.e(TAG, "Failed to cache PDF: $pdfPath", e) + throw RuntimeException("Failed to cache PDF file", e) + } + } + + pdfIdToPathMap[uniqueId] = cachedFile.absolutePath + Log.d(TAG, "Registered PDF with ID: $uniqueId") + + return uniqueId + } + + /** + * Get the local PDF URL for a registered PDF ID + */ + fun getLocalPdfUrl(pdfId: String): String { + return "https://local.pdf/$pdfId" + } + + /** + * Check if a PDF ID is registered + */ + fun isPdfRegistered(pdfId: String): Boolean { + return pdfIdToPathMap.containsKey(pdfId) + } + + /** + * Get the cached file path for a PDF ID + */ + fun getCachedFilePath(pdfId: String): String? { + return pdfIdToPathMap[pdfId] + } + + /** + * Unregister a PDF ID (cleanup) + */ + fun unregisterPdf(pdfId: String) { + val cachedPath = pdfIdToPathMap.remove(pdfId) + if (cachedPath != null) { + try { + File(cachedPath).delete() + Log.d(TAG, "Unregistered and deleted PDF: $pdfId") + } catch (e: Exception) { + Log.w(TAG, "Failed to delete cached PDF: $cachedPath", e) + } + } + } + + /** + * Clear all cached PDFs + */ + fun clearCache() { + try { + cacheDir.listFiles()?.forEach { it.delete() } + pdfIdToPathMap.clear() + Log.d(TAG, "Cleared PDF cache") + } catch (e: Exception) { + Log.e(TAG, "Failed to clear PDF cache", e) + } + } + + private fun generateUniqueId(pdfPath: String, lastModified: Long): String { + val input = "$pdfPath:$lastModified" + val digest = MessageDigest.getInstance("SHA-256") + val hash = digest.digest(input.toByteArray()) + return hash.joinToString("") { "%02x".format(it) }.take(16) + } +} diff --git a/course/src/main/java/in/testpress/course/util/PdfWebViewClient.kt b/course/src/main/java/in/testpress/course/util/PdfWebViewClient.kt new file mode 100644 index 000000000..5452cd788 --- /dev/null +++ b/course/src/main/java/in/testpress/course/util/PdfWebViewClient.kt @@ -0,0 +1,118 @@ +package `in`.testpress.course.util + +import `in`.testpress.fragments.WebViewFragment +import `in`.testpress.util.webview.CustomWebViewClient +import android.content.Context +import android.util.Log +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import java.util.regex.Pattern + +class PdfWebViewClient( + fragment: WebViewFragment, + private val context: Context +) : CustomWebViewClient(fragment) { + + companion object { + private const val TAG = "PdfWebViewClient" + private const val LOCAL_PDF_SCHEME = "https://local.pdf/" + private val PDF_ID_PATTERN = Pattern.compile("^https://local\\.pdf/([^/]+)$") + private val SAFE_ID_PATTERN = Pattern.compile("[^a-zA-Z0-9._-]") + } + + override fun shouldInterceptRequest( + view: android.webkit.WebView?, + request: WebResourceRequest? + ): WebResourceResponse? { + val url = request?.url?.toString() ?: return super.shouldInterceptRequest(view, request) + + Log.d(TAG, "Intercepting request: $url") + + // Check if this is a local PDF request + if (!url.startsWith(LOCAL_PDF_SCHEME)) { + return super.shouldInterceptRequest(view, request) + } + + // Extract PDF ID from URL + val matcher = PDF_ID_PATTERN.matcher(url) + if (!matcher.matches()) { + Log.w(TAG, "Invalid local PDF URL format: $url") + return createErrorResponse(400, "Bad Request") + } + + val pdfId = matcher.group(1) + Log.d(TAG, "Extracted PDF ID: $pdfId") + + // Sanitize PDF ID to prevent path traversal + val safeId = SAFE_ID_PATTERN.matcher(pdfId).replaceAll("_") + Log.d(TAG, "Sanitized PDF ID: $safeId") + + // Resolve cached file location + val cacheDir = File(context.cacheDir, "pdf-cache") + if (!cacheDir.exists()) { + cacheDir.mkdirs() + } + + val pdfFile = File(cacheDir, "$safeId.pdf") + Log.d(TAG, "Looking for PDF file: ${pdfFile.absolutePath}") + + return try { + if (pdfFile.exists() && pdfFile.isFile) { + Log.d(TAG, "Serving PDF file: ${pdfFile.absolutePath} (${pdfFile.length()} bytes)") + createPdfResponse(pdfFile) + } else { + Log.w(TAG, "PDF file not found: ${pdfFile.absolutePath}") + createErrorResponse(404, "Not Found") + } + } catch (e: Exception) { + Log.e(TAG, "Error serving PDF file", e) + createErrorResponse(500, "Internal Error") + } + } + + private fun createPdfResponse(pdfFile: File): WebResourceResponse { + return try { + val inputStream = FileInputStream(pdfFile) + val headers = mapOf( + "Cache-Control" to "private, max-age=3600", + "Content-Length" to pdfFile.length().toString(), + "Content-Type" to "application/pdf" + ) + + WebResourceResponse( + "application/pdf", + "binary", + 200, + "OK", + headers, + inputStream + ) + } catch (e: IOException) { + Log.e(TAG, "Error creating PDF response", e) + createErrorResponse(500, "Internal Error") + } + } + + private fun createErrorResponse(status: Int, message: String): WebResourceResponse { + val statusText = when (status) { + 400 -> "Bad Request" + 404 -> "Not Found" + 500 -> "Internal Server Error" + else -> "Error" + } + + Log.d(TAG, "Creating error response: $status $statusText - $message") + + return WebResourceResponse( + "text/plain", + "utf-8", + status, + statusText, + mapOf("Content-Type" to "text/plain"), + message.byteInputStream() + ) + } +} From 3210750d17069caafed1272e0945c08f753840ca Mon Sep 17 00:00:00 2001 From: kaaviya98 Date: Tue, 14 Oct 2025 16:45:16 +0530 Subject: [PATCH 4/5] add-check --- .../util/webview/CustomWebViewClient.kt | 107 ++++++++++++++++ .../course/fragments/AIChatPdfFragment.kt | 10 +- .../testpress/course/util/PdfWebViewClient.kt | 118 ------------------ 3 files changed, 108 insertions(+), 127 deletions(-) delete mode 100644 course/src/main/java/in/testpress/course/util/PdfWebViewClient.kt diff --git a/core/src/main/java/in/testpress/util/webview/CustomWebViewClient.kt b/core/src/main/java/in/testpress/util/webview/CustomWebViewClient.kt index c3f6f06ef..9a713c97f 100644 --- a/core/src/main/java/in/testpress/util/webview/CustomWebViewClient.kt +++ b/core/src/main/java/in/testpress/util/webview/CustomWebViewClient.kt @@ -3,12 +3,25 @@ package `in`.testpress.util.webview import `in`.testpress.core.TestpressException import `in`.testpress.fragments.WebViewFragment import `in`.testpress.util.extension.openUrlInBrowser +import android.content.Context import android.graphics.Bitmap +import android.util.Log import android.webkit.* +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import java.util.regex.Pattern class CustomWebViewClient(val fragment: WebViewFragment) : WebViewClient() { private var errorList = linkedMapOf() + + companion object { + private const val TAG = "CustomWebViewClient" + private const val LOCAL_PDF_SCHEME = "https://local.pdf/" + private val PDF_ID_PATTERN = Pattern.compile("^https://local\\.pdf/([^/]+)$") + private val SAFE_ID_PATTERN = Pattern.compile("[^a-zA-Z0-9._-]") + } override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { val url = request?.url.toString() @@ -25,6 +38,57 @@ class CustomWebViewClient(val fragment: WebViewFragment) : WebViewClient() { } } + override fun shouldInterceptRequest( + view: WebView?, + request: WebResourceRequest? + ): WebResourceResponse? { + val url = request?.url?.toString() ?: return super.shouldInterceptRequest(view, request) + + Log.d(TAG, "Intercepting request: $url") + + // Check if this is a local PDF request + if (!url.startsWith(LOCAL_PDF_SCHEME)) { + return super.shouldInterceptRequest(view, request) + } + + // Extract PDF ID from URL + val matcher = PDF_ID_PATTERN.matcher(url) + if (!matcher.matches()) { + Log.w(TAG, "Invalid local PDF URL format: $url") + return createErrorResponse(400, "Bad Request") + } + + val pdfId = matcher.group(1) + Log.d(TAG, "Extracted PDF ID: $pdfId") + + // Sanitize PDF ID to prevent path traversal + val safeId = SAFE_ID_PATTERN.matcher(pdfId).replaceAll("_") + Log.d(TAG, "Sanitized PDF ID: $safeId") + + // Resolve cached file location + val context = fragment.requireContext() + val cacheDir = File(context.cacheDir, "pdf-cache") + if (!cacheDir.exists()) { + cacheDir.mkdirs() + } + + val pdfFile = File(cacheDir, "$safeId.pdf") + Log.d(TAG, "Looking for PDF file: ${pdfFile.absolutePath}") + + return try { + if (pdfFile.exists() && pdfFile.isFile) { + Log.d(TAG, "Serving PDF file: ${pdfFile.absolutePath} (${pdfFile.length()} bytes)") + createPdfResponse(pdfFile) + } else { + Log.w(TAG, "PDF file not found: ${pdfFile.absolutePath}") + createErrorResponse(404, "Not Found") + } + } catch (e: Exception) { + Log.e(TAG, "Error serving PDF file", e) + createErrorResponse(500, "Internal Error") + } + } + private fun isPDFUrl(url: String?) = url?.contains(".pdf") ?: false private fun shouldLoadInWebView(url: String?):Boolean { @@ -80,4 +144,47 @@ class CustomWebViewClient(val fragment: WebViewFragment) : WebViewClient() { } } } + + private fun createPdfResponse(pdfFile: File): WebResourceResponse { + return try { + val inputStream = FileInputStream(pdfFile) + val headers = mapOf( + "Cache-Control" to "private, max-age=3600", + "Content-Length" to pdfFile.length().toString(), + "Content-Type" to "application/pdf" + ) + + WebResourceResponse( + "application/pdf", + "binary", + 200, + "OK", + headers, + inputStream + ) + } catch (e: IOException) { + Log.e(TAG, "Error creating PDF response", e) + createErrorResponse(500, "Internal Error") + } + } + + private fun createErrorResponse(status: Int, message: String): WebResourceResponse { + val statusText = when (status) { + 400 -> "Bad Request" + 404 -> "Not Found" + 500 -> "Internal Server Error" + else -> "Error" + } + + Log.d(TAG, "Creating error response: $status $statusText - $message") + + return WebResourceResponse( + "text/plain", + "utf-8", + status, + statusText, + mapOf("Content-Type" to "text/plain"), + message.byteInputStream() + ) + } } \ No newline at end of file diff --git a/course/src/main/java/in/testpress/course/fragments/AIChatPdfFragment.kt b/course/src/main/java/in/testpress/course/fragments/AIChatPdfFragment.kt index 37a72771e..1820953f4 100644 --- a/course/src/main/java/in/testpress/course/fragments/AIChatPdfFragment.kt +++ b/course/src/main/java/in/testpress/course/fragments/AIChatPdfFragment.kt @@ -2,7 +2,6 @@ package `in`.testpress.course.fragments import `in`.testpress.course.R import `in`.testpress.course.util.PdfCacheManager -import `in`.testpress.course.util.PdfWebViewClient import `in`.testpress.course.util.EnhancedPdfProvider import android.os.Bundle import android.util.Log @@ -72,7 +71,6 @@ class AIChatPdfFragment : Fragment() { webViewFragment.setListener(object : WebViewFragment.Listener { override fun onWebViewInitializationSuccess() { - setupWebViewClient(webViewFragment) setupJavaScriptInterface(webViewFragment) } }) @@ -90,12 +88,6 @@ class AIChatPdfFragment : Fragment() { return "$baseUrl/courses/$courseId/contents/$contentId/?content_detail_v2=true" } - private fun setupWebViewClient(webViewFragment: WebViewFragment) { - // Replace the default WebViewClient with our PDF streaming client - webViewFragment.webView.webViewClient = PdfWebViewClient(webViewFragment, requireContext()) - Log.d("AIChatPdfFragment", "PDF streaming WebViewClient set up successfully") - } - private fun setupJavaScriptInterface(webViewFragment: WebViewFragment) { val pdfPath = requireArguments().getString(ARG_PDF_PATH, "") Log.d("AIChatPdfFragment", "Setting up JavaScript interface with PDF path: $pdfPath") @@ -105,7 +97,7 @@ class AIChatPdfFragment : Fragment() { allowUniversalAccessFromFileURLs = true } - // Create enhanced JavaScript interface that provides both streaming URL and base64 fallback + // Create enhanced JavaScript interface that provides streaming URL val jsInterface = EnhancedPdfProvider(requireActivity(), pdfPath, pdfId) webViewFragment.addJavascriptInterface(jsInterface, "AndroidPdfCache") diff --git a/course/src/main/java/in/testpress/course/util/PdfWebViewClient.kt b/course/src/main/java/in/testpress/course/util/PdfWebViewClient.kt deleted file mode 100644 index 5452cd788..000000000 --- a/course/src/main/java/in/testpress/course/util/PdfWebViewClient.kt +++ /dev/null @@ -1,118 +0,0 @@ -package `in`.testpress.course.util - -import `in`.testpress.fragments.WebViewFragment -import `in`.testpress.util.webview.CustomWebViewClient -import android.content.Context -import android.util.Log -import android.webkit.WebResourceRequest -import android.webkit.WebResourceResponse -import java.io.File -import java.io.FileInputStream -import java.io.IOException -import java.util.regex.Pattern - -class PdfWebViewClient( - fragment: WebViewFragment, - private val context: Context -) : CustomWebViewClient(fragment) { - - companion object { - private const val TAG = "PdfWebViewClient" - private const val LOCAL_PDF_SCHEME = "https://local.pdf/" - private val PDF_ID_PATTERN = Pattern.compile("^https://local\\.pdf/([^/]+)$") - private val SAFE_ID_PATTERN = Pattern.compile("[^a-zA-Z0-9._-]") - } - - override fun shouldInterceptRequest( - view: android.webkit.WebView?, - request: WebResourceRequest? - ): WebResourceResponse? { - val url = request?.url?.toString() ?: return super.shouldInterceptRequest(view, request) - - Log.d(TAG, "Intercepting request: $url") - - // Check if this is a local PDF request - if (!url.startsWith(LOCAL_PDF_SCHEME)) { - return super.shouldInterceptRequest(view, request) - } - - // Extract PDF ID from URL - val matcher = PDF_ID_PATTERN.matcher(url) - if (!matcher.matches()) { - Log.w(TAG, "Invalid local PDF URL format: $url") - return createErrorResponse(400, "Bad Request") - } - - val pdfId = matcher.group(1) - Log.d(TAG, "Extracted PDF ID: $pdfId") - - // Sanitize PDF ID to prevent path traversal - val safeId = SAFE_ID_PATTERN.matcher(pdfId).replaceAll("_") - Log.d(TAG, "Sanitized PDF ID: $safeId") - - // Resolve cached file location - val cacheDir = File(context.cacheDir, "pdf-cache") - if (!cacheDir.exists()) { - cacheDir.mkdirs() - } - - val pdfFile = File(cacheDir, "$safeId.pdf") - Log.d(TAG, "Looking for PDF file: ${pdfFile.absolutePath}") - - return try { - if (pdfFile.exists() && pdfFile.isFile) { - Log.d(TAG, "Serving PDF file: ${pdfFile.absolutePath} (${pdfFile.length()} bytes)") - createPdfResponse(pdfFile) - } else { - Log.w(TAG, "PDF file not found: ${pdfFile.absolutePath}") - createErrorResponse(404, "Not Found") - } - } catch (e: Exception) { - Log.e(TAG, "Error serving PDF file", e) - createErrorResponse(500, "Internal Error") - } - } - - private fun createPdfResponse(pdfFile: File): WebResourceResponse { - return try { - val inputStream = FileInputStream(pdfFile) - val headers = mapOf( - "Cache-Control" to "private, max-age=3600", - "Content-Length" to pdfFile.length().toString(), - "Content-Type" to "application/pdf" - ) - - WebResourceResponse( - "application/pdf", - "binary", - 200, - "OK", - headers, - inputStream - ) - } catch (e: IOException) { - Log.e(TAG, "Error creating PDF response", e) - createErrorResponse(500, "Internal Error") - } - } - - private fun createErrorResponse(status: Int, message: String): WebResourceResponse { - val statusText = when (status) { - 400 -> "Bad Request" - 404 -> "Not Found" - 500 -> "Internal Server Error" - else -> "Error" - } - - Log.d(TAG, "Creating error response: $status $statusText - $message") - - return WebResourceResponse( - "text/plain", - "utf-8", - status, - statusText, - mapOf("Content-Type" to "text/plain"), - message.byteInputStream() - ) - } -} From 95fb455645a59fd6c89922a69b88bc0dcce3a1cd Mon Sep 17 00:00:00 2001 From: kaaviya98 Date: Tue, 14 Oct 2025 16:58:51 +0530 Subject: [PATCH 5/5] Add-check --- .../util/webview/CustomWebViewClient.kt | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/in/testpress/util/webview/CustomWebViewClient.kt b/core/src/main/java/in/testpress/util/webview/CustomWebViewClient.kt index 9a713c97f..6d98f8474 100644 --- a/core/src/main/java/in/testpress/util/webview/CustomWebViewClient.kt +++ b/core/src/main/java/in/testpress/util/webview/CustomWebViewClient.kt @@ -51,6 +51,12 @@ class CustomWebViewClient(val fragment: WebViewFragment) : WebViewClient() { return super.shouldInterceptRequest(view, request) } + // Handle OPTIONS requests (CORS preflight) + if (request?.method == "OPTIONS") { + Log.d(TAG, "Handling CORS preflight request for: $url") + return createCorsPreflightResponse() + } + // Extract PDF ID from URL val matcher = PDF_ID_PATTERN.matcher(url) if (!matcher.matches()) { @@ -148,18 +154,22 @@ class CustomWebViewClient(val fragment: WebViewFragment) : WebViewClient() { private fun createPdfResponse(pdfFile: File): WebResourceResponse { return try { val inputStream = FileInputStream(pdfFile) - val headers = mapOf( - "Cache-Control" to "private, max-age=3600", - "Content-Length" to pdfFile.length().toString(), - "Content-Type" to "application/pdf" - ) + val corsHeaders = mutableMapOf().apply { + put("Access-Control-Allow-Origin", "*") // Allow all origins for WebView + put("Access-Control-Allow-Methods", "GET, OPTIONS") + put("Access-Control-Allow-Headers", "Content-Type, Range") + put("Access-Control-Expose-Headers", "Content-Length, Accept-Ranges") + put("Cache-Control", "private, max-age=3600") + put("Content-Length", pdfFile.length().toString()) + put("Content-Type", "application/pdf") + } WebResourceResponse( "application/pdf", "binary", 200, "OK", - headers, + corsHeaders, inputStream ) } catch (e: IOException) { @@ -168,6 +178,26 @@ class CustomWebViewClient(val fragment: WebViewFragment) : WebViewClient() { } } + private fun createCorsPreflightResponse(): WebResourceResponse { + val corsHeaders = mutableMapOf().apply { + put("Access-Control-Allow-Origin", "*") + put("Access-Control-Allow-Methods", "GET, OPTIONS") + put("Access-Control-Allow-Headers", "Content-Type, Range") + put("Access-Control-Max-Age", "86400") // 24 hours + } + + Log.d(TAG, "Creating CORS preflight response") + + return WebResourceResponse( + "text/plain", + "utf-8", + 200, + "OK", + corsHeaders, + "".byteInputStream() + ) + } + private fun createErrorResponse(status: Int, message: String): WebResourceResponse { val statusText = when (status) { 400 -> "Bad Request"