diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..c5f3f6b
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "java.configuration.updateBuildConfiguration": "interactive"
+}
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a3c8159
--- /dev/null
+++ b/README.md
@@ -0,0 +1,76 @@
+# Basic Video Player with PiP
+
+A simple video player Android application that demonstrates proper MVVM architecture implementation with Picture-in-Picture (PiP) functionality and dynamic video source loading from a JSON file.
+
+## Features
+
+- **Picture-in-Picture Mode**: Continue watching videos in a small floating window when you navigate away from the app
+- **Dynamic Video Source**: Video URL is loaded from a local JSON file
+- **MVVM Architecture**: Clean separation of concerns with Model-View-ViewModel pattern
+- **Error Handling**: Graceful fallback to local video when remote loading fails
+- **Dependency Injection**: Uses Hilt for clean dependency management
+## Screenshots
+
+| Full Screen Playback | Picture-in-Picture Mode |
+|:---:|:---:|
+|
|
|
+
+
+
+## Implementation Details
+
+### Architecture Components
+
+- **Model**: Handles data operations through JsonDataSource and VideoRepository
+- **View**: UI components in VideoPlayerActivity with lifecycle-aware playback
+- **ViewModel**: Manages business logic and UI state in VideoPlayerViewModel
+
+### Libraries Used
+
+- ExoPlayer for video playback
+- Hilt for dependency injection
+- Gson for JSON parsing
+- Android Lifecycle components for MVVM implementation
+- Kotlin Coroutines for asynchronous operations
+
+## Getting Started
+
+1. Clone the repository
+2. Open the project in Android Studio
+3. Run the app on an emulator or physical device running Android 8.0 (API level 26) or higher for full PiP support
+
+## How It Works
+
+The app reads a video URL from a JSON file in the assets folder and plays it using ExoPlayer. When you press back or navigate away from the app, it automatically enters Picture-in-Picture mode, allowing you to continue watching the video while using other apps.
+
+If there's any issue loading the video from the remote URL, the app will automatically fall back to a local video resource.
+
+### Architecture Flow
+
+1. **Data Loading**: When the app starts, the `JsonDataSource` reads the video URL from the assets folder and passes it to the `VideoRepository`.
+
+2. **Repository Layer**: The `VideoRepository` processes the data and handles any potential errors, ensuring a valid URL is always provided.
+
+3. **ViewModel Processing**: The `VideoPlayerViewModel` requests the URL from the repository and exposes it to the UI through LiveData, along with loading states.
+
+4. **UI Rendering**: The `VideoPlayerActivity` observes the ViewModel's LiveData and updates the UI accordingly, loading the video when the URL is available.
+
+5. **Video Playback**: ExoPlayer handles the actual video playback, with proper lifecycle management to prevent memory leaks.
+
+6. **PiP Functionality**: The `PipHelper` class manages transitions to and from Picture-in-Picture mode, maintaining the playback state during transitions.
+
+### Error Handling
+
+The app implements multiple layers of error handling to ensure a smooth user experience:
+
+- JSON parsing errors are caught in the data source
+- Network errors are handled when loading the video
+- Playback errors trigger fallback to local video
+- UI states reflect loading progress and success states
+
+This multi-layered approach ensures that users always have a working video player experience, even when offline or when the remote video source is unavailable.
+
+## Requirements
+
+- Android 8.0 (API level 26) or higher for PiP functionality
+- Android Studio Arctic Fox or newer
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 19e349a..874bb0c 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,6 +1,8 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
+ id("com.google.dagger.hilt.android")
+ id("kotlin-kapt")
}
android {
@@ -33,6 +35,13 @@ android {
kotlinOptions {
jvmTarget = "11"
}
+ buildFeatures {
+ viewBinding = true
+ }
+ // For Hilt
+ kapt {
+ correctErrorTypes = true
+ }
}
dependencies {
@@ -40,6 +49,31 @@ dependencies {
implementation(libs.androidx.media3.ui)
implementation(libs.androidx.media3.exoplayer)
+ // Hilt for dependency injection
+ implementation("com.google.dagger:hilt-android:2.50")
+ implementation(libs.androidx.tracing.perfetto.handshake)
+ kapt("com.google.dagger:hilt-compiler:2.50")
+
+ // ViewModel and LiveData
+ implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
+ implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0")
+ implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
+ implementation("androidx.hilt:hilt-navigation-fragment:1.1.0")
+
+ // Retrofit for network requests
+ implementation("com.squareup.retrofit2:retrofit:2.9.0")
+ implementation("com.squareup.retrofit2:converter-gson:2.9.0")
+
+ // OkHttp for networking
+ implementation("com.squareup.okhttp3:okhttp:4.12.0")
+ implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
+
+ // Gson for JSON parsing
+ implementation("com.google.code.gson:gson:2.10.1")
+
+ // Coroutines
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
+
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ade8fda..e6ad96b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,7 +2,11 @@
+
+
+
+ android:exported="true"
+ android:supportsPictureInPicture="true"
+ android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|screenLayout|uiMode">
+
+
\ No newline at end of file
diff --git a/app/src/main/assets/video_url.json b/app/src/main/assets/video_url.json
new file mode 100644
index 0000000..a94ad50
--- /dev/null
+++ b/app/src/main/assets/video_url.json
@@ -0,0 +1,3 @@
+{
+ "url": "https://storage.googleapis.com/test-gg-1/Sample%20Video.mp4"
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/pubscale/basicvideoplayer/MainActivity.kt b/app/src/main/java/com/pubscale/basicvideoplayer/MainActivity.kt
index 1de2e04..99830d5 100644
--- a/app/src/main/java/com/pubscale/basicvideoplayer/MainActivity.kt
+++ b/app/src/main/java/com/pubscale/basicvideoplayer/MainActivity.kt
@@ -1,40 +1,28 @@
package com.pubscale.basicvideoplayer
-import android.net.Uri
+import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
-import androidx.media3.common.MediaItem
-import androidx.media3.exoplayer.ExoPlayer
-import androidx.media3.ui.PlayerView
+import com.pubscale.basicvideoplayer.ui.player.VideoPlayerActivity
+import dagger.hilt.android.AndroidEntryPoint
+/**
+ * Entry point of the application.
+ * Simply launches the VideoPlayerActivity and finishes.
+ */
+@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
- private var player: ExoPlayer? = null
- private var playerView: PlayerView? = null
-
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
- setupExoPLayer()
- }
-
- private fun setupExoPLayer() {
- playerView = findViewById(R.id.player_view)
- player = ExoPlayer.Builder(this).build()
- playerView?.player = player
- val videoUri = Uri.parse("android.resource://" + packageName + "/" + R.raw.sample_video)
- val mediaItem = MediaItem.fromUri(videoUri)
- player?.setMediaItem(mediaItem)
- player?.prepare()
- player?.playWhenReady = true
+ startVideoPlayer()
}
-
- override fun onRestart() {
- super.onRestart()
- player?.play()
- }
-
- override fun onStop() {
- super.onStop()
- player?.pause()
+
+ // Launch the video player activity
+ private fun startVideoPlayer() {
+ val intent = Intent(this, VideoPlayerActivity::class.java)
+ startActivity(intent)
+ finish()
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/pubscale/basicvideoplayer/VideoPlayerApplication.kt b/app/src/main/java/com/pubscale/basicvideoplayer/VideoPlayerApplication.kt
new file mode 100644
index 0000000..bd5991e
--- /dev/null
+++ b/app/src/main/java/com/pubscale/basicvideoplayer/VideoPlayerApplication.kt
@@ -0,0 +1,7 @@
+package com.pubscale.basicvideoplayer
+
+import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
+
+@HiltAndroidApp
+class VideoPlayerApplication : Application()
\ No newline at end of file
diff --git a/app/src/main/java/com/pubscale/basicvideoplayer/data/api/JsonDataSource.kt b/app/src/main/java/com/pubscale/basicvideoplayer/data/api/JsonDataSource.kt
new file mode 100644
index 0000000..e582f39
--- /dev/null
+++ b/app/src/main/java/com/pubscale/basicvideoplayer/data/api/JsonDataSource.kt
@@ -0,0 +1,33 @@
+package com.pubscale.basicvideoplayer.data.api
+
+import android.content.Context
+import com.google.gson.Gson
+import com.pubscale.basicvideoplayer.data.model.VideoResponse
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+
+/**
+ * Data source for reading video information from the JSON file.
+ * Uses app context to access assets folder.
+ */
+class JsonDataSource @Inject constructor(
+ private val context: Context,
+ private val gson: Gson
+) {
+
+ companion object {
+ private const val JSON_FILE_NAME = "video_url.json"
+ }
+
+ /**
+ * Reads the JSON file from assets and converts it to VideoResponse.
+ * Uses IO dispatcher to avoid blocking the main thread.
+ */
+ suspend fun getVideoUrl(): VideoResponse {
+ return withContext(Dispatchers.IO) {
+ val jsonString = context.assets.open(JSON_FILE_NAME).bufferedReader().use { it.readText() }
+ gson.fromJson(jsonString, VideoResponse::class.java)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/pubscale/basicvideoplayer/data/model/VideoModel.kt b/app/src/main/java/com/pubscale/basicvideoplayer/data/model/VideoModel.kt
new file mode 100644
index 0000000..b6d7198
--- /dev/null
+++ b/app/src/main/java/com/pubscale/basicvideoplayer/data/model/VideoModel.kt
@@ -0,0 +1,14 @@
+package com.pubscale.basicvideoplayer.data.model
+
+/**
+ * Data class that represents the video information from JSON.
+ * Contains the URL of the video to be played.
+ */
+data class VideoResponse(
+ val url: String
+) {
+ companion object {
+ // Fallback URL to use if JSON reading fails or returns empty URL
+ const val FALLBACK_VIDEO_URL = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/pubscale/basicvideoplayer/data/repository/VideoRepository.kt b/app/src/main/java/com/pubscale/basicvideoplayer/data/repository/VideoRepository.kt
new file mode 100644
index 0000000..1da1784
--- /dev/null
+++ b/app/src/main/java/com/pubscale/basicvideoplayer/data/repository/VideoRepository.kt
@@ -0,0 +1,31 @@
+package com.pubscale.basicvideoplayer.data.repository
+
+import com.pubscale.basicvideoplayer.data.api.JsonDataSource
+import com.pubscale.basicvideoplayer.data.model.VideoResponse
+import javax.inject.Inject
+
+/**
+ * Repository that provides video URL data.
+ * Handles fetching from JSON and provides fallback if needed.
+ */
+class VideoRepository @Inject constructor(
+ private val jsonDataSource: JsonDataSource
+) {
+
+ /**
+ * Gets the video URL from local JSON file or returns fallback URL.
+ * Ensures a valid URL is always returned.
+ */
+ suspend fun getVideoUrl(): String {
+ return try {
+ val response = jsonDataSource.getVideoUrl()
+
+ // Return URL if valid, otherwise use fallback
+ response.url.takeIf { it.isNotEmpty() }
+ ?: VideoResponse.FALLBACK_VIDEO_URL
+ } catch (e: Exception) {
+ // On any error, return the fallback URL
+ VideoResponse.FALLBACK_VIDEO_URL
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/pubscale/basicvideoplayer/di/AppModule.kt b/app/src/main/java/com/pubscale/basicvideoplayer/di/AppModule.kt
new file mode 100644
index 0000000..0a48ab2
--- /dev/null
+++ b/app/src/main/java/com/pubscale/basicvideoplayer/di/AppModule.kt
@@ -0,0 +1,46 @@
+package com.pubscale.basicvideoplayer.di
+
+import android.content.Context
+import com.google.gson.Gson
+import com.pubscale.basicvideoplayer.data.api.JsonDataSource
+import com.pubscale.basicvideoplayer.data.repository.VideoRepository
+import com.pubscale.basicvideoplayer.ui.player.PipHelper
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+/**
+ * Hilt module that provides dependencies for the application.
+ * All dependencies are provided as singletons.
+ */
+@Module
+@InstallIn(SingletonComponent::class)
+object AppModule {
+
+ // Provides the PiP helper
+ @Provides
+ @Singleton
+ fun providePipHelper() = PipHelper()
+
+ // Provides Gson for JSON parsing
+ @Provides
+ @Singleton
+ fun provideGson() = Gson()
+
+ // Provides the data source for JSON operations
+ @Provides
+ @Singleton
+ fun provideJsonDataSource(
+ @ApplicationContext context: Context,
+ gson: Gson
+ ) = JsonDataSource(context, gson)
+
+ // Provides the repository to access video data
+ @Provides
+ @Singleton
+ fun provideVideoRepository(jsonDataSource: JsonDataSource) =
+ VideoRepository(jsonDataSource)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/pubscale/basicvideoplayer/ui/player/PipHelper.kt b/app/src/main/java/com/pubscale/basicvideoplayer/ui/player/PipHelper.kt
new file mode 100644
index 0000000..3cc2127
--- /dev/null
+++ b/app/src/main/java/com/pubscale/basicvideoplayer/ui/player/PipHelper.kt
@@ -0,0 +1,65 @@
+package com.pubscale.basicvideoplayer.ui.player
+
+import android.app.Activity
+import android.app.PictureInPictureParams
+import android.os.Build
+import android.util.Rational
+import android.view.View
+import androidx.media3.common.Player
+import javax.inject.Inject
+
+/**
+ * Helper class to handle Picture-in-Picture mode functionality.
+ * Centralizes PiP logic to keep the activity code clean.
+ */
+class PipHelper @Inject constructor() {
+
+ /**
+ * Attempts to enter PiP mode if the device supports it.
+ *
+ * @param activity The activity that should enter PiP mode
+ * @param playerView The view showing the video
+ * @param player The ExoPlayer instance
+ * @return wasPlaying True if the player was playing when PiP mode was entered
+ */
+ fun enterPipMode(activity: Activity, playerView: View?, player: Player?): Boolean {
+ val isVideoReady = player?.playbackState == Player.STATE_READY
+ val wasPlaying = player?.isPlaying == true
+
+ if (isVideoReady && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ // Set 16:9 aspect ratio if we can't get the actual dimensions
+ val aspectRatio = Rational(playerView?.width ?: 16, playerView?.height ?: 9)
+
+ val pipParamsBuilder = PictureInPictureParams.Builder()
+ .setAspectRatio(aspectRatio)
+
+ activity.enterPictureInPictureMode(pipParamsBuilder.build())
+ return wasPlaying
+ }
+
+ return false
+ }
+
+ /**
+ * Handles UI and playback changes when PiP mode changes.
+ * Shows/hides controls and manages playback state.
+ */
+ fun handlePipModeChanged(
+ isInPipMode: Boolean,
+ player: Player?,
+ showControls: (Boolean) -> Unit,
+ wasPlaying: Boolean
+ ) {
+ // Hide player controls in PiP mode
+ showControls(!isInPipMode)
+
+ // When exiting PiP mode, restore the previous playback state
+ if (!isInPipMode) {
+ if (wasPlaying) {
+ player?.play()
+ } else {
+ player?.pause()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/pubscale/basicvideoplayer/ui/player/VideoPlayerActivity.kt b/app/src/main/java/com/pubscale/basicvideoplayer/ui/player/VideoPlayerActivity.kt
new file mode 100644
index 0000000..3c05744
--- /dev/null
+++ b/app/src/main/java/com/pubscale/basicvideoplayer/ui/player/VideoPlayerActivity.kt
@@ -0,0 +1,200 @@
+package com.pubscale.basicvideoplayer.ui.player
+
+import android.content.res.Configuration
+import android.net.Uri
+import android.os.Bundle
+import android.view.View
+import android.widget.ProgressBar
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import androidx.media3.common.MediaItem
+import androidx.media3.common.PlaybackException
+import androidx.media3.common.Player
+import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.ui.PlayerView
+import com.pubscale.basicvideoplayer.R
+import com.pubscale.basicvideoplayer.ui.viewmodel.VideoPlayerViewModel
+import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
+
+/**
+ * Main activity that handles video playback and PiP functionality.
+ * Observes the ViewModel to get video URL and manages player lifecycle.
+ */
+@AndroidEntryPoint
+class VideoPlayerActivity : AppCompatActivity() {
+ private var player: ExoPlayer? = null
+ private var playerView: PlayerView? = null
+ private lateinit var progressBar: ProgressBar
+ private val viewModel: VideoPlayerViewModel by viewModels()
+ private var isVideoReady = false
+ private var wasPlayingBeforePip = false
+
+ @Inject
+ lateinit var pipHelper: PipHelper
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_video_player)
+
+ playerView = findViewById(R.id.player_view)
+ progressBar = findViewById(R.id.progress_bar)
+
+ setupExoPlayer()
+ setupObservers()
+ }
+
+ // Connect to ViewModel and observe changes
+ private fun setupObservers() {
+ viewModel.videoUrl.observe(this) { url ->
+ if (url.isNotEmpty()) {
+ loadRemoteVideo(url)
+ }
+ }
+
+ viewModel.isLoading.observe(this) { isLoading ->
+ progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE
+ }
+ }
+
+ // Initialize the ExoPlayer instance
+ private fun setupExoPlayer() {
+ player = ExoPlayer.Builder(this).build().apply {
+ addListener(PlayerEventListener())
+ }
+
+ playerView?.player = player
+ player?.playWhenReady = true
+ }
+
+ // Listen for player events to update UI and handle errors
+ private inner class PlayerEventListener : Player.Listener {
+ override fun onPlayerError(error: PlaybackException) {
+ loadLocalFallbackVideo()
+ }
+
+ override fun onPlaybackStateChanged(playbackState: Int) {
+ when (playbackState) {
+ Player.STATE_READY -> {
+ isVideoReady = true
+ progressBar.visibility = View.GONE
+ }
+ Player.STATE_BUFFERING -> {
+ progressBar.visibility = View.VISIBLE
+ }
+ }
+ }
+ }
+
+ // Load video from the URL provided by the ViewModel
+ private fun loadRemoteVideo(videoUrl: String) {
+ progressBar.visibility = View.VISIBLE
+ try {
+ val uri = Uri.parse(videoUrl)
+ val mediaItem = MediaItem.fromUri(uri)
+
+ player?.setMediaItem(mediaItem)
+ player?.prepare()
+
+ player?.addListener(object : Player.Listener {
+ override fun onPlayerError(error: PlaybackException) {
+ player?.removeListener(this)
+ loadLocalFallbackVideo()
+ }
+
+ override fun onPlaybackStateChanged(playbackState: Int) {
+ if (playbackState == Player.STATE_READY) {
+ progressBar.visibility = View.GONE
+ player?.play()
+ player?.removeListener(this)
+ }
+ }
+ })
+ } catch (e: Exception) {
+ loadLocalFallbackVideo()
+ }
+ }
+
+ // Fallback to local video if remote loading fails
+ private fun loadLocalFallbackVideo() {
+ val videoUri = Uri.parse("android.resource://" + packageName + "/" + R.raw.sample_video)
+ val mediaItem = MediaItem.fromUri(videoUri)
+ player?.setMediaItem(mediaItem)
+ player?.prepare()
+ player?.play()
+ }
+
+ // Enter PiP mode when user navigates away from the app
+ override fun onUserLeaveHint() {
+ super.onUserLeaveHint()
+ wasPlayingBeforePip = player?.isPlaying == true
+ pipHelper.enterPipMode(this, playerView, player)
+ }
+
+ // Handle PiP mode changes - show/hide controls and manage playback
+ override fun onPictureInPictureModeChanged(
+ isInPictureInPictureMode: Boolean,
+ newConfig: Configuration
+ ) {
+ super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
+
+ pipHelper.handlePipModeChanged(
+ isInPictureInPictureMode,
+ player,
+ { showControls -> playerView?.useController = showControls },
+ wasPlayingBeforePip
+ )
+ }
+
+ // Enter PiP mode when back button is pressed instead of exiting
+ @Deprecated("Deprecated in Java")
+ override fun onBackPressed() {
+ if (isVideoReady) {
+ wasPlayingBeforePip = player?.isPlaying == true
+ pipHelper.enterPipMode(this, playerView, player)
+ } else {
+ super.onBackPressed()
+ }
+ }
+
+ // Lifecycle management for ExoPlayer
+ override fun onStart() {
+ super.onStart()
+ if (player == null) {
+ setupExoPlayer()
+ }
+ }
+
+ override fun onRestart() {
+ super.onRestart()
+ player?.play()
+ }
+
+ override fun onPause() {
+ super.onPause()
+ if (!isInPictureInPictureMode) {
+ player?.pause()
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ if (!isInPictureInPictureMode) {
+ player?.play()
+ }
+ }
+
+ override fun onStop() {
+ super.onStop()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ releasePlayer()
+ }
+
+ private fun releasePlayer() {
+ player?.release()
+ player = null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/pubscale/basicvideoplayer/ui/viewmodel/VideoPlayerViewModel.kt b/app/src/main/java/com/pubscale/basicvideoplayer/ui/viewmodel/VideoPlayerViewModel.kt
new file mode 100644
index 0000000..9027a7a
--- /dev/null
+++ b/app/src/main/java/com/pubscale/basicvideoplayer/ui/viewmodel/VideoPlayerViewModel.kt
@@ -0,0 +1,49 @@
+package com.pubscale.basicvideoplayer.ui.viewmodel
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.pubscale.basicvideoplayer.data.repository.VideoRepository
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+/**
+ * ViewModel for the video player screen.
+ * Fetches video URL from repository and exposes it to the UI.
+ */
+@HiltViewModel
+class VideoPlayerViewModel @Inject constructor(
+ private val repository: VideoRepository
+) : ViewModel() {
+
+ // Holds the video URL to be played
+ private val _videoUrl = MutableLiveData()
+ val videoUrl: LiveData = _videoUrl
+
+ // Indicates whether loading is in progress
+ private val _isLoading = MutableLiveData()
+ val isLoading: LiveData = _isLoading
+
+ init {
+ fetchVideoUrl()
+ }
+
+ // Fetch video URL from the repository
+ fun fetchVideoUrl() {
+ viewModelScope.launch {
+ try {
+ _isLoading.value = true
+
+ val url = repository.getVideoUrl()
+ _videoUrl.value = url
+
+ } catch (e: Exception) {
+ // Repository should handle all errors, but just in case
+ } finally {
+ _isLoading.value = false
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 32f41fe..8376158 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -7,11 +7,23 @@
android:layout_height="match_parent"
tools:context=".MainActivity">
-
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_video_player.xml b/app/src/main/res/layout/activity_video_player.xml
new file mode 100644
index 0000000..51fcde8
--- /dev/null
+++ b/app/src/main/res/layout/activity_video_player.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 12edb2d..c5b9ce8 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,4 @@
- BasicVideoPlayer
+ Basic Video Player
+ Loading Video Player
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index 922f551..cac1431 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -2,4 +2,10 @@
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
+}
+
+buildscript {
+ dependencies {
+ classpath("com.google.dagger:hilt-android-gradle-plugin:2.50")
+ }
}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index b4e12e3..603349e 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -11,6 +11,7 @@ activity = "1.10.0"
constraintlayout = "2.2.0"
media3Ui = "1.5.1"
media3Exoplayer = "1.5.1"
+tracingPerfettoHandshake = "1.0.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -23,6 +24,7 @@ material = { group = "com.google.android.material", name = "material", version.r
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
androidx-media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3Exoplayer" }
+androidx-tracing-perfetto-handshake = { group = "androidx.tracing", name = "tracing-perfetto-handshake", version.ref = "tracingPerfettoHandshake" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }