From 1c5407c29aaf362997613044a244fe1886fdd4c3 Mon Sep 17 00:00:00 2001 From: Yogesh Bhagat Date: Tue, 4 Nov 2025 13:17:21 +0530 Subject: [PATCH 1/3] Implement youtube video webview player for landscape mode & handle current timestamp while transitioning --- .../unit/video/YoutubeVideoUnitFragment.kt | 201 ++++++++++++++---- .../fragment_youtube_video_unit.xml | 6 + .../fragment_youtube_video_unit.xml | 6 + 3 files changed, 172 insertions(+), 41 deletions(-) diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/YoutubeVideoUnitFragment.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/YoutubeVideoUnitFragment.kt index 088d51553..2d62e9e34 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/video/YoutubeVideoUnitFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/video/YoutubeVideoUnitFragment.kt @@ -56,6 +56,11 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) private var blockId = "" + // Track if we're using WebView fallback (persists across rotations) + private var isUsingWebViewFallback = false + + // Track last known WebView playback position (persists across rotations) + private var webViewLastPlaybackPosition: Float = 0f private val youtubeTrackerListener = YouTubePlayerTracker() @@ -72,6 +77,12 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) blockId = getString(ARG_BLOCK_ID, "") } viewModel.downloadSubtitles() + + savedInstanceState?.let { + isUsingWebViewFallback = it.getBoolean(KEY_USING_WEBVIEW_FALLBACK, false) + webViewLastPlaybackPosition = it.getFloat(KEY_WEBVIEW_PLAYBACK_POSITION, 0f) + android.util.Log.d("YoutubeVideoUnit", "Restored state - isUsingWebViewFallback: $isUsingWebViewFallback, playback position: $webViewLastPlaybackPosition sec") + } } override fun onCreateView( @@ -141,12 +152,19 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) binding.connectionError.isVisible = !viewModel.hasInternetConnection - // Hide WebView initially - it's only a fallback - binding.fallbackWebview?.visibility = View.GONE - binding.youtubePlayerView.visibility = View.VISIBLE - - // Initialize YouTube player library (original approach) - initializeYoutubePlayer() + if (isUsingWebViewFallback) { + // Hide YouTube player, show WebView + binding.youtubePlayerView.visibility = View.GONE + binding.fallbackWebview.visibility = View.VISIBLE + // Re-setup WebView + setupWebViewPlayer() + } else { + // Hide WebView initially - it's only a fallback + binding.fallbackWebview.visibility = View.GONE + binding.youtubePlayerView.visibility = View.VISIBLE + // Initialize YouTube player library (original approach) + initializeYoutubePlayer() + } } private fun initializeYoutubePlayer() { @@ -258,11 +276,11 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) } private fun switchToWebViewFallback() { - android.util.Log.w("YoutubePlayer", "Switching to WebView fallback") + isUsingWebViewFallback = true // Hide YouTube player, show WebView binding.youtubePlayerView.visibility = View.GONE - binding.fallbackWebview?.visibility = View.VISIBLE + binding.fallbackWebview.visibility = View.VISIBLE // Setup and load WebView setupWebViewPlayer() @@ -273,6 +291,30 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) _youTubePlayer?.pause() } + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + // Save state before rotation + outState.putBoolean(KEY_USING_WEBVIEW_FALLBACK, isUsingWebViewFallback) + + // If using WebView, get current playback position via JavaScript + if (isUsingWebViewFallback && _binding != null) { + binding.fallbackWebview?.evaluateJavascript( + "(function() { try { return player ? player.getCurrentTime() : 0; } catch(e) { return 0; } })()" + ) { result -> + try { + val position = result?.toFloatOrNull() ?: 0f + webViewLastPlaybackPosition = position + android.util.Log.d("YoutubeVideoUnit", "Saved WebView playback position: $position sec") + } catch (e: Exception) { + android.util.Log.e("YoutubeVideoUnit", "Error saving playback position", e) + } + } + } + + outState.putFloat(KEY_WEBVIEW_PLAYBACK_POSITION, webViewLastPlaybackPosition) + android.util.Log.d("YoutubeVideoUnit", "Saving state - isUsingWebViewFallback: $isUsingWebViewFallback, position: $webViewLastPlaybackPosition sec") + } + override fun onDestroyView() { _youTubePlayer = null super.onDestroyView() @@ -281,19 +323,16 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) private fun setupWebViewPlayer() { val videoId = extractYouTubeVideoId(viewModel.videoUrl) ?: run { - android.util.Log.e("YoutubePlayer", "Failed to extract video ID from: ${viewModel.videoUrl}") + android.util.Log.e("YoutubeVideoUnit", "Failed to extract video ID from: ${viewModel.videoUrl}") return } - android.util.Log.d("YoutubePlayer", "Setting up WebView for video: $videoId") - android.util.Log.d("YoutubePlayer", "Full URL: ${viewModel.videoUrl}") - // Hide YouTube player library view, show WebView binding.youtubePlayerView.visibility = View.GONE - binding.fallbackWebview?.visibility = View.VISIBLE + binding.fallbackWebview.visibility = View.VISIBLE // Configure WebView with better settings for video playback - binding.fallbackWebview?.apply { + binding.fallbackWebview.apply { settings.javaScriptEnabled = true settings.domStorageEnabled = true settings.mediaPlaybackRequiresUserGesture = false @@ -312,6 +351,9 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) // Set black background like native player setBackgroundColor(android.graphics.Color.BLACK) + // Add JavaScript interface to track playback position + addJavascriptInterface(WebAppInterface(), "Android") + // Add WebChromeClient for fullscreen support webChromeClient = object : WebChromeClient() { private var customView: View? = null @@ -336,10 +378,10 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) } else { @Suppress("DEPRECATION") activity.window.decorView.systemUiVisibility = ( - View.SYSTEM_UI_FLAG_FULLSCREEN - or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - ) + View.SYSTEM_UI_FLAG_FULLSCREEN + or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + ) } val contentView = activity.findViewById(android.R.id.content) @@ -395,12 +437,12 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) // Check if it's trying to navigate to YouTube website, channel, or external links when { url.contains("youtube.com/channel") || - url.contains("youtube.com/user") || - url.contains("youtube.com/c/") || - url.contains("youtube.com/@") || - url.contains("youtube.com/watch") || - url.contains("youtube.com/playlist") || - url.contains("youtu.be") -> { + url.contains("youtube.com/user") || + url.contains("youtube.com/c/") || + url.contains("youtube.com/@") || + url.contains("youtube.com/watch") || + url.contains("youtube.com/playlist") || + url.contains("youtu.be") -> { android.util.Log.w("YoutubePlayer", "Blocked YouTube navigation attempt") return true // Block navigation } @@ -424,12 +466,12 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) when { url.contains("youtube.com/channel") || - url.contains("youtube.com/user") || - url.contains("youtube.com/c/") || - url.contains("youtube.com/@") || - url.contains("youtube.com/watch") || - url.contains("youtube.com/playlist") || - url.contains("youtu.be") -> { + url.contains("youtube.com/user") || + url.contains("youtube.com/c/") || + url.contains("youtube.com/@") || + url.contains("youtube.com/watch") || + url.contains("youtube.com/playlist") || + url.contains("youtu.be") -> { android.util.Log.w("YoutubePlayer", "Blocked YouTube navigation attempt") return true } @@ -449,8 +491,15 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) } private fun loadYouTubeEmbed(videoId: String) { - // Calculate start time in seconds - val startTime = (viewModel.getCurrentVideoTime() / 1000).toInt() + // Use restored position if available (after rotation), otherwise use ViewModel's position + val startTime = if (webViewLastPlaybackPosition > 0) { + android.util.Log.d("YoutubeVideoUnit", "Using restored position: $webViewLastPlaybackPosition sec") + webViewLastPlaybackPosition.toInt() + } else { + val vmTime = (viewModel.getCurrentVideoTime() / 1000).toInt() + android.util.Log.d("YoutubeVideoUnit", "Using ViewModel position: $vmTime sec") + vmTime + } // Create HTML with YouTube IFrame Player API for native-like experience val embedHtml = """ @@ -519,6 +568,7 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) @@ -602,8 +709,6 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) "UTF-8", null ) - - android.util.Log.d("YoutubePlayer", "✅ WebView player loaded with YouTube IFrame API") } @@ -649,6 +754,18 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) } } + /** + * JavaScript interface to receive playback position updates from WebView + */ + inner class WebAppInterface { + @android.webkit.JavascriptInterface + fun updatePlaybackPosition(position: Float) { + webViewLastPlaybackPosition = position + // Also update ViewModel's position for consistency + viewModel.setCurrentVideoTime((position * 1000f).toLong()) + } + } + companion object { private const val ARG_VIDEO_URL = "videoUrl" @@ -656,6 +773,8 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) private const val ARG_BLOCK_ID = "blockId" private const val ARG_COURSE_ID = "courseId" private const val ARG_TITLE = "blockTitle" + private const val KEY_USING_WEBVIEW_FALLBACK = "usingWebViewFallback" + private const val KEY_WEBVIEW_PLAYBACK_POSITION = "webViewPlaybackPosition" const val VIDEO_COMPLETION_THRESHOLD = 0.8f const val RATE_DIALOG_THRESHOLD = 0.99f diff --git a/course/src/main/res/layout-land/fragment_youtube_video_unit.xml b/course/src/main/res/layout-land/fragment_youtube_video_unit.xml index b8d5984c2..e1802be71 100644 --- a/course/src/main/res/layout-land/fragment_youtube_video_unit.xml +++ b/course/src/main/res/layout-land/fragment_youtube_video_unit.xml @@ -23,6 +23,12 @@ android:layout_height="match_parent" app:enableAutomaticInitialization="false" /> + + + + Date: Tue, 4 Nov 2025 15:08:33 +0530 Subject: [PATCH 2/3] fix issue of app crash when access offline --- .../presentation/NativeDiscoveryFragment.kt | 4 +-- .../presentation/NativeDiscoveryViewModel.kt | 29 ++++++++++++++----- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryFragment.kt b/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryFragment.kt index 569337b8c..8c1c45b27 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryFragment.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryFragment.kt @@ -39,7 +39,7 @@ class NativeDiscoveryFragment : Fragment() { OpenEdXTheme { val windowSize = rememberWindowSize() - val uiState by viewModel.uiState.observeAsState() + val uiState by viewModel.uiState.observeAsState(DiscoveryUIState.Loading) val uiMessage by viewModel.uiMessage.observeAsState() val canLoadMore by viewModel.canLoadMore.observeAsState(false) val refreshing by viewModel.isUpdating.observeAsState(false) @@ -50,7 +50,7 @@ class NativeDiscoveryFragment : Fragment() { DiscoveryScreen( windowSize = windowSize, - state = uiState!!, + state = uiState, uiMessage = uiMessage, apiHostUrl = viewModel.apiHostUrl, canLoadMore = canLoadMore, diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryViewModel.kt b/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryViewModel.kt index a8c80678e..1fd7de8f1 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryViewModel.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/NativeDiscoveryViewModel.kt @@ -99,17 +99,21 @@ class NativeDiscoveryViewModel( page = -1 } coursesList.addAll(response.results) + _uiState.value = DiscoveryUIState.Courses( + courses = ArrayList(coursesList), + numCourses = response.pagination.count + ) + if (_organizations.value.isNullOrEmpty()) { + fetchOrganizations() + } } else { val cachedList = interactor.getCoursesListFromCache() _canLoadMore.value = false page = -1 coursesList.addAll(cachedList) - } - val totalCount = response?.pagination?.count - _uiState.value = totalCount?.let { - DiscoveryUIState.Courses( + _uiState.value = DiscoveryUIState.Courses( courses = ArrayList(coursesList), - numCourses = it + numCourses = coursesList.size ) } } catch (e: Exception) { @@ -161,6 +165,9 @@ class NativeDiscoveryViewModel( numCourses = it ) } + if (_organizations.value.isNullOrEmpty() && networkConnection.isOnline()) { + fetchOrganizations() + } } catch (e: Exception) { if (e.isInternetError()) { _uiMessage.value = @@ -224,10 +231,16 @@ class NativeDiscoveryViewModel( fun fetchOrganizations() { viewModelScope.launch { try { - val orgs = repository.getOrganizations() - _organizations.value = orgs + if (networkConnection.isOnline()) { + val orgs = repository.getOrganizations() + _organizations.value = orgs + } else { + // Set empty list when offline to avoid crashes + _organizations.value = emptyList() + } } catch (e: Exception) { - Log.e("DiscoveryViewModel", "Failed to load orgs", e) + // Set empty list on error to avoid crashes + _organizations.value = emptyList() } } } From f1dbabc9a33c15998ef3ff258a5be85b95bd24fa Mon Sep 17 00:00:00 2001 From: Yogesh Bhagat Date: Tue, 4 Nov 2025 18:25:07 +0530 Subject: [PATCH 3/3] prevent app from prompting to open facebook links --- app/src/main/AndroidManifest.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 65c64e538..2d3f8ed74 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -121,6 +121,12 @@ + + + +