11package org.openedx.course.presentation.unit.html
22
33import android.annotation.SuppressLint
4+ import android.app.Activity
5+ import android.content.ActivityNotFoundException
46import android.content.Intent
57import android.content.res.Configuration
68import android.graphics.Bitmap
79import android.net.Uri
10+ import android.os.Build
811import android.os.Bundle
912import android.util.Log
1013import android.view.LayoutInflater
1114import android.view.ViewGroup
1215import android.webkit.JavascriptInterface
16+ import android.webkit.ValueCallback
17+ import android.webkit.WebChromeClient
1318import android.webkit.WebResourceError
1419import android.webkit.WebResourceRequest
1520import android.webkit.WebResourceResponse
1621import android.webkit.WebSettings
1722import android.webkit.WebView
1823import android.webkit.WebViewClient
24+ import androidx.activity.result.contract.ActivityResultContracts
1925import androidx.compose.foundation.background
2026import androidx.compose.foundation.isSystemInDarkTheme
2127import androidx.compose.foundation.layout.Box
@@ -76,6 +82,15 @@ class HtmlUnitFragment : Fragment() {
7682 private var offlineUrl: String = " "
7783 private var lastModified: String = " "
7884 private var fromDownloadedContent: Boolean = false
85+ private var filePathCallback: ValueCallback <Array <Uri >>? = null
86+
87+ private val fileChooserLauncher =
88+ registerForActivityResult(ActivityResultContracts .StartActivityForResult ()) { result ->
89+ val uris = WebChromeClient .FileChooserParams .parseResult(result.resultCode, result.data)
90+ ? : extractUrisFromResult(result.resultCode, result.data)
91+ filePathCallback?.onReceiveValue(uris)
92+ filePathCallback = null
93+ }
7994
8095 override fun onCreate (savedInstanceState : Bundle ? ) {
8196 super .onCreate(savedInstanceState)
@@ -97,11 +112,84 @@ class HtmlUnitFragment : Fragment() {
97112 blockUrl = blockUrl,
98113 offlineUrl = offlineUrl,
99114 fromDownloadedContent = fromDownloadedContent,
100- isFragmentAdded = isAdded
115+ isFragmentAdded = isAdded,
116+ onShowFileChooser = ::openFileChooser
101117 )
102118 }
103119 }
104120
121+ override fun onDestroyView () {
122+ filePathCallback?.onReceiveValue(null )
123+ filePathCallback = null
124+ super .onDestroyView()
125+ }
126+
127+ private fun openFileChooser (
128+ callback : ValueCallback <Array <Uri >>,
129+ fileChooserParams : WebChromeClient .FileChooserParams ? ,
130+ ): Boolean {
131+ filePathCallback?.onReceiveValue(null )
132+ filePathCallback = callback
133+ val intent = try {
134+ fileChooserParams?.createIntent()
135+ } catch (_: Exception ) {
136+ null
137+ } ? : Intent (Intent .ACTION_GET_CONTENT ).apply {
138+ addCategory(Intent .CATEGORY_OPENABLE )
139+ val mimeTypes = fileChooserParams?.acceptTypes
140+ ?.filter { it.isNotBlank() }
141+ ?.toTypedArray()
142+ if (! mimeTypes.isNullOrEmpty()) {
143+ type = mimeTypes.first()
144+ putExtra(Intent .EXTRA_MIME_TYPES , mimeTypes)
145+ } else {
146+ type = " */*"
147+ }
148+ putExtra(
149+ Intent .EXTRA_ALLOW_MULTIPLE ,
150+ fileChooserParams?.mode == WebChromeClient .FileChooserParams .MODE_OPEN_MULTIPLE
151+ )
152+ }
153+
154+ intent.addFlags(Intent .FLAG_GRANT_READ_URI_PERMISSION )
155+ if (intent.action == Intent .ACTION_CHOOSER ) {
156+ val extraIntent = if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .TIRAMISU ) {
157+ intent.getParcelableExtra(Intent .EXTRA_INTENT , Intent ::class .java)
158+ } else {
159+ @Suppress(" DEPRECATION" )
160+ intent.getParcelableExtra(Intent .EXTRA_INTENT )
161+ }
162+ extraIntent?.addFlags(Intent .FLAG_GRANT_READ_URI_PERMISSION )
163+ }
164+
165+ return try {
166+ fileChooserLauncher.launch(intent)
167+ true
168+ } catch (_: ActivityNotFoundException ) {
169+ filePathCallback?.onReceiveValue(null )
170+ filePathCallback = null
171+ false
172+ }
173+ }
174+
175+ private fun extractUrisFromResult (resultCode : Int , data : Intent ? ): Array <Uri >? {
176+ if (resultCode != Activity .RESULT_OK || data == null ) return null
177+
178+ val clipUris = data.clipData?.let { clipData ->
179+ (0 until clipData.itemCount).mapNotNull { clipData.getItemAt(it)?.uri }
180+ }.orEmpty()
181+
182+ val singleUri = data.data
183+
184+ val result = when {
185+ clipUris.isNotEmpty() -> clipUris.toTypedArray()
186+ singleUri != null -> arrayOf(singleUri)
187+ else -> null
188+ }
189+
190+ return result
191+ }
192+
105193 companion object {
106194 private const val ARG_BLOCK_ID = " blockId"
107195 private const val ARG_COURSE_ID = " courseId"
@@ -135,6 +223,7 @@ fun HtmlUnitView(
135223 offlineUrl : String ,
136224 fromDownloadedContent : Boolean ,
137225 isFragmentAdded : Boolean ,
226+ onShowFileChooser : (ValueCallback <Array <Uri >>, WebChromeClient .FileChooserParams ? ) -> Boolean ,
138227) {
139228 OpenEdXTheme {
140229 val context = LocalContext .current
@@ -216,6 +305,7 @@ fun HtmlUnitView(
216305 saveXBlockProgress = { jsonProgress ->
217306 viewModel.saveXBlockProgress(jsonProgress)
218307 },
308+ onShowFileChooser = onShowFileChooser
219309 )
220310 } else {
221311 viewModel.onWebPageLoadError()
@@ -257,6 +347,7 @@ private fun HTMLContentView(
257347 onWebPageLoaded : () -> Unit ,
258348 onWebPageLoadError : () -> Unit ,
259349 saveXBlockProgress : (String ) -> Unit ,
350+ onShowFileChooser : (ValueCallback <Array <Uri >>, WebChromeClient .FileChooserParams ? ) -> Boolean ,
260351) {
261352 val coroutineScope = rememberCoroutineScope()
262353 val context = LocalContext .current
@@ -299,6 +390,15 @@ private fun HTMLContentView(
299390 ),
300391 " AndroidBridge"
301392 )
393+ webChromeClient = object : WebChromeClient () {
394+ override fun onShowFileChooser (
395+ view : WebView ? ,
396+ filePathCallback : ValueCallback <Array <Uri >>,
397+ fileChooserParams : FileChooserParams ?
398+ ): Boolean {
399+ return onShowFileChooser(filePathCallback, fileChooserParams)
400+ }
401+ }
302402 webViewClient = object : WebViewClient () {
303403
304404 override fun onPageStarted (view : WebView ? , url : String? , favicon : Bitmap ? ) {
0 commit comments