Skip to content

Conversation

@hermannakos
Copy link
Collaborator

Implements support for Studio embed videos to open in immersive view with "Open in Detail View" buttons. This provides better video viewing experience for Studio content embedded in pages and assignments.

Changes:

  • Detect Studio embed URLs (thumbnail_embed and learn_embed types)
  • Detect Studio media URLs (media_attachments_iframe)
  • Transform embed URLs to immersive view format with required parameters
  • Add "Open in Detail View" buttons below Studio iframes
  • Route immersive view URLs to InternalWebviewFragment without LTI auth
  • Extract and pass video titles to immersive view

Known limitation:

  • Buttons inside iframes cannot be intercepted due to WebView cross-origin isolation. Users must use the "Open in Detail View" button below the iframe.
  • Working with web team on postMessage solution for better UX.

Test plan:

  • Verify Studio embed iframes show "Open in Detail View" button
  • Verify button navigates to immersive view in InternalWebviewFragment
  • Verify video title displays correctly in immersive view
  • Verify standard media uploads continue to work with Expand button

refs: MBL-19552
affects: Student
release note: none

🤖 Generated with Claude Code

Implements support for Studio embed videos to open in immersive view with
"Open in Detail View" buttons. This provides better video viewing experience
for Studio content embedded in pages and assignments.

Changes:
- Detect Studio embed URLs (thumbnail_embed and learn_embed types)
- Detect Studio media URLs (media_attachments_iframe)
- Transform embed URLs to immersive view format with required parameters
- Add "Open in Detail View" buttons below Studio iframes
- Route immersive view URLs to InternalWebviewFragment without LTI auth
- Extract and pass video titles to immersive view

Known limitation:
- Buttons inside iframes cannot be intercepted due to WebView cross-origin
  isolation. Users must use the "Open in Detail View" button below the iframe.
- Working with web team on postMessage solution for better UX.

Test plan:
- Verify Studio embed iframes show "Open in Detail View" button
- Verify button navigates to immersive view in InternalWebviewFragment
- Verify video title displays correctly in immersive view
- Verify standard media uploads continue to work with Expand button

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@github-actions
Copy link

📊 Code Coverage Report

⚠️ Student

  • PR Coverage: 42.50%
  • Master Coverage: 42.61%
  • Delta: -0.10%

✅ Teacher

  • PR Coverage: 25.44%
  • Master Coverage: 25.44%
  • Delta: +0.00%

✅ Pandautils

  • PR Coverage: 22.59%
  • Master Coverage: 22.59%
  • Delta: +0.00%

📈 Overall Average

  • PR Coverage: 30.18%
  • Master Coverage: 30.21%
  • Delta: -0.03%

@github-actions
Copy link

github-actions bot commented Nov 21, 2025

Student Install Page

Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review - Update

I've reviewed the latest changes to this PR. Here's my assessment:

What Changed

This update includes improvements to handle Studio embed URLs in both the RouteMatcher and HtmlContentFormatter:

  1. RouteMatcher.kt: Added early detection logic for Studio embed immersive view URLs to prevent them from being caught by the generic LTI route matcher
  2. HtmlContentFormatter.kt:
    • Restructured iframe handling logic to check for Studio embeds before generic external tools
    • Added new convertStudioEmbedToImmersiveView() function for URL transformation
    • Fixed HTML entity escaping in generated buttons (security improvement)
    • Added hasStudioEmbedUrl() helper function

Issues Found

🔴 Must Fix

  • Debug logging in production code (RouteMatcher.kt:564-581): Multiple Logger.e() calls should be removed or changed to debug level
  • Null-safety concern (RouteMatcher.kt:571): Force-unwrap operator !! used after insufficient null checks
  • Missing error handling (HtmlContentFormatter.kt:219): URL decoding operations could fail and should be wrapped in try-catch

🟡 Should Improve

  • Performance (RouteMatcher.kt:564-567): Repeated toString() calls - extract to variable
  • Code complexity (HtmlContentFormatter.kt:219-276): Complex URL manipulation logic could benefit from simplification and better error handling

❓ Questions

  • Test coverage: Are there unit tests for the new hasStudioEmbedUrl() pattern matching with various encoding scenarios?

Positive Changes

✅ HTML entity escaping fix prevents potential XSS issues
✅ Improved control flow logic prevents routing conflicts
✅ Good separation of concerns between Studio embed types

Please address the must-fix issues before merging.

val title = uri.getQueryParameter("title") ?: activity.getString(R.string.immersiveView)
route.arguments.putString(Const.ACTION_BAR_TITLE, title)

Logger.e("RouteMatcher - Routing to InternalWebviewFragment with URL: $urlString")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: Debug logging left in production code

Multiple Logger.e() calls are present which should be removed or changed to debug level:

  • Line 564: Logger.e("RouteMatcher - Checking route: ${route?.uri?.toString()}")
  • Line 568: Logger.e("RouteMatcher - Detected Studio embed immersive view URL in route()")
  • Line 581: Logger.e("RouteMatcher - Routing to InternalWebviewFragment with URL: $urlString")

These appear to be debugging statements that should either be removed or changed to Logger.d() (debug level) before merging to production. Error-level logging should be reserved for actual errors.

Logger.e("RouteMatcher - Checking route: ${route?.uri?.toString()}")
if (route?.uri?.toString()?.contains("external_tools/retrieve") == true &&
route.uri?.toString()?.contains("custom_arc_launch_type") == true &&
route.uri?.toString()?.contains("immersive_view") == true) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: Inefficient repeated string conversions

The code calls route?.uri?.toString() multiple times (lines 564, 566, 567) which is inefficient. Consider extracting this to a variable:

val uriString = route?.uri?.toString()
if (uriString?.contains("external_tools/retrieve") == true &&
    uriString.contains("custom_arc_launch_type") &&
    uriString.contains("immersive_view")) {

This improves both performance and readability.

Logger.e("RouteMatcher - Detected Studio embed immersive view URL in route()")
// Handle Studio embed immersive view - pass the full URL and title to InternalWebviewFragment
val uri = route.uri!!
val urlString = uri.toString()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: Potential null pointer exception

On line 571, route.uri is force-unwrapped with !! after only checking that the string representation contains certain values. However, the null-safety checks on lines 564-567 use ?.toString() which could theoretically be non-null string "null" if the URI itself is null.

While unlikely, the safer approach would be:

val uri = route.uri ?: return
val urlString = uri.toString()

This ensures uri is non-null before proceeding and avoids the force-unwrap operator.


private fun convertStudioEmbedToImmersiveView(srcUrl: String, title: String?): String {
// Normalize HTML entities before processing
val normalizedUrl = srcUrl.replace("&amp;", "&")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: Missing null-safety check in URL normalization

On line 221, the code normalizes the URL by replacing &amp; with &, but there's no null check before the replace operation. While srcUrl is checked earlier, it's better to be explicit:

val normalizedUrl = srcUrl.replace("&amp;", "&")

This is actually safe since srcUrl is guaranteed non-null by the regex match, but the pattern should be consistent with line 283 where you do check for null.

private fun iframeWithStudioButton(immersiveUrl: String, iframe: String, context: Context): String {
val buttonText = context.getString(R.string.openInDetailView)
val htmlButton = "</br><p><div class=\"lti_button\" onClick=\"location.href='$immersiveUrl'\">$buttonText</div></p>"
val escapedUrl = immersiveUrl.replace("&", "&amp;")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good: HTML entity escaping fix

This is an important security fix! Properly escaping the & characters in the URL prevents potential XSS issues when the HTML is rendered. The change from location.href='$immersiveUrl' to using $escapedUrl with proper entity encoding is the correct approach.

return newHTML
}

private fun convertStudioEmbedToImmersiveView(srcUrl: String, title: String?): String {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: Complex URL manipulation could be simplified

The convertStudioEmbedToImmersiveView() function performs multiple string replacements and encoding/decoding operations. Consider:

  1. Error handling: What happens if URL decoding fails? Should wrap in try-catch
  2. URL encoding consistency: Using both java.net.URLDecoder and URLEncoder - ensure charset is consistent
  3. String building: Consider using StringBuilder or buildString {} for constructing the final URL

Example improvement:

private fun convertStudioEmbedToImmersiveView(srcUrl: String, title: String?): String {
    try {
        val normalizedUrl = srcUrl.replace("&amp;", "&")
        // ... rest of logic
    } catch (e: Exception) {
        Logger.e("Failed to convert Studio embed URL: ${e.message}")
        return srcUrl // Return original on failure
    }
}

// Handle the LTI case
// Check Studio embed URLs first (before generic external_tools check)
// because Studio embeds also contain "external_tools" in their URL
if (hasStudioEmbedUrl(srcUrl)) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good: Improved control flow logic

The restructured if-else chain is much better! Checking for Studio embed URLs before the generic hasExternalTools() check prevents the wrong handler from being applied. This fix addresses the routing issue mentioned in the PR description where Studio embeds were being incorrectly caught by the LTI route matcher.

fun hasKalturaUrl(text: String?) = text?.contains("kaltura.com").orDefault()
fun hasExternalTools(text: String?) = text?.contains("external_tools").orDefault()
fun hasStudioMediaUrl(text: String?) = text?.contains("media_attachments_iframe").orDefault()
fun hasStudioEmbedUrl(text: String?): Boolean {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Test coverage for new URL patterns

The new hasStudioEmbedUrl() function checks for encoded URL parameters (custom_arc_launch_type%3Dthumbnail_embed). Are there unit tests covering:

  1. URLs with &amp; entities vs plain &
  2. Different encoding scenarios (single vs double encoded)
  3. Edge cases where these parameters might appear in different positions

This is complex string matching logic that would benefit from comprehensive test coverage.

@github-actions
Copy link

🧪 Unit Test Results

✅ 📱 Student App

  • Tests: 1241 total, 0 failed, 0 skipped
  • Duration: 0.000s
  • Success Rate: 100%

✅ 🌅 Horizon

  • Tests: 449 total, 0 failed, 0 skipped
  • Duration: 35.705s
  • Success Rate: 100%

✅ 📦 Submodules

  • Tests: 2515 total, 0 failed, 0 skipped
  • Duration: 57.787s
  • Success Rate: 100%

📊 Summary

  • Total Tests: 4205
  • Failed: 0
  • Skipped: 0
  • Status: ✅ All tests passed!

Last updated: Mon, 12 Jan 2026 15:28:53 GMT

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants