Skip to content

Commit 3db6518

Browse files
feat: html unit file chooser (#471)
1 parent a8ddde8 commit 3db6518

File tree

1 file changed

+101
-1
lines changed

1 file changed

+101
-1
lines changed

course/src/main/java/org/openedx/course/presentation/unit/html/HtmlUnitFragment.kt

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
11
package org.openedx.course.presentation.unit.html
22

33
import android.annotation.SuppressLint
4+
import android.app.Activity
5+
import android.content.ActivityNotFoundException
46
import android.content.Intent
57
import android.content.res.Configuration
68
import android.graphics.Bitmap
79
import android.net.Uri
10+
import android.os.Build
811
import android.os.Bundle
912
import android.util.Log
1013
import android.view.LayoutInflater
1114
import android.view.ViewGroup
1215
import android.webkit.JavascriptInterface
16+
import android.webkit.ValueCallback
17+
import android.webkit.WebChromeClient
1318
import android.webkit.WebResourceError
1419
import android.webkit.WebResourceRequest
1520
import android.webkit.WebResourceResponse
1621
import android.webkit.WebSettings
1722
import android.webkit.WebView
1823
import android.webkit.WebViewClient
24+
import androidx.activity.result.contract.ActivityResultContracts
1925
import androidx.compose.foundation.background
2026
import androidx.compose.foundation.isSystemInDarkTheme
2127
import 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

Comments
 (0)