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" }