Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />

<queries>
<intent>
Expand Down
1 change: 1 addition & 0 deletions course/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ dependencies {
implementation "androidx.media3:media3-ui:$media3_version"
implementation "androidx.media3:media3-cast:$media3_version"
implementation "me.saket.extendedspans:extendedspans:$extented_spans_version"
implementation "androidx.activity:activity-compose:$activity_compose_version"

testImplementation "junit:junit:$junit_version"
androidTestImplementation "androidx.test.ext:junit:$test_ext_version"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package org.openedx.course.presentation.unit.html

import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Bitmap
import android.net.Uri
Expand All @@ -10,12 +12,16 @@
import android.view.LayoutInflater
import android.view.ViewGroup
import android.webkit.JavascriptInterface
import android.webkit.PermissionRequest
import android.webkit.WebChromeClient
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
Expand Down Expand Up @@ -46,6 +52,7 @@
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.zIndex
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -272,6 +279,28 @@

val isDarkTheme = isSystemInDarkTheme()

// Holds the in-flight PermissionRequest from the WebView until the Android permission
// dialog resolves. Storing it in a MutableState lets the launcher callback reach it
// without needing a mutable var captured inside the factory lambda.
val pendingWebPermissionRequest = remember { mutableStateOf<PermissionRequest?>(null) }

val permissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { grants ->
val req = pendingWebPermissionRequest.value ?: return@rememberLauncherForActivityResult
pendingWebPermissionRequest.value = null
val grantedResources = req.resources.filter { resource ->
when (resource) {
PermissionRequest.RESOURCE_AUDIO_CAPTURE ->
grants[Manifest.permission.RECORD_AUDIO] == true
// Non-dangerous resources (e.g. RESOURCE_PROTECTED_MEDIA_ID) are granted
// without a corresponding Android permission.
else -> true
}
}.toTypedArray()
if (grantedResources.isNotEmpty()) req.grant(grantedResources) else req.deny()
}

AndroidView(
modifier = Modifier
.then(screenWidth)
Expand Down Expand Up @@ -362,6 +391,32 @@
super.onReceivedError(view, request, error)
}
}
// Grant camera/microphone access requested by web content (e.g. WebRTC, LTI
// tools, or real-time features). The corresponding Android permissions
// (RECORD_AUDIO, CAMERA) must also be declared in the manifest.
webChromeClient = object : WebChromeClient() {
override fun onPermissionRequest(request: PermissionRequest) {
if (pendingWebPermissionRequest.value != null) {
// Another request is already being resolved; deny immediately.
request.deny()
return
}
val androidPerms = buildList {
if (PermissionRequest.RESOURCE_AUDIO_CAPTURE in request.resources) {
add(Manifest.permission.RECORD_AUDIO)
}
}
if (androidPerms.isEmpty() || androidPerms.all {
ContextCompat.checkSelfPermission(context, it) ==
PackageManager.PERMISSION_GRANTED
}) {

Check warning

Code scanning / detekt

Reports missing newlines (e.g. between parentheses of a multi-line function call Warning

Missing newline before ")"
request.grant(request.resources)
} else {
pendingWebPermissionRequest.value = request
permissionLauncher.launch(androidPerms.toTypedArray())
}
}
}
with(settings) {
javaScriptEnabled = true
loadWithOverviewMode = true
Expand All @@ -373,6 +428,9 @@
allowContentAccess = true
useWideViewPort = true
cacheMode = WebSettings.LOAD_NO_CACHE
// Allow audio and video elements to play without a prior user gesture.
// Required for LTI tools and real-time features that produce sound on load.
mediaPlaybackRequiresUserGesture = false
}
isVerticalScrollBarEnabled = false
isHorizontalScrollBarEnabled = false
Expand Down
Loading