Skip to content
Open
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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
# PIP_Mode_Jetpack_Compose
# PIP_Mode_Jetpack_Compose

https://github.com/user-attachments/assets/05df9e22-2b5b-43f1-b4a5-c95b9c50adcb

1 change: 1 addition & 0 deletions app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
66 changes: 66 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
id("com.google.gms.google-services")
}

android {
namespace = "com.geeksforgeeks.demo"
compileSdk = 35

defaultConfig {
applicationId = "com.geeksforgeeks.demo"
minSdk = 24
targetSdk = 35
versionCode = 1
versionName = "1.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
compose = true
}
}

dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.activity)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)

implementation("com.google.firebase:firebase-firestore:25.1.4")
implementation ("androidx.navigation:navigation-compose:2.7.7")
implementation("io.coil-kt:coil-compose:2.4.0")
}
21 changes: 21 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.geeksforgeeks.demo

import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4

import org.junit.Test
import org.junit.runner.RunWith

import org.junit.Assert.*

/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.geeksforgeeks.demo", appContext.packageName)
}
}
29 changes: 29 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Demo"
tools:targetApi="31">
<receiver android:name=".PipReceiver"/>
<activity
android:name=".MainActivity"
android:exported="true"
android:supportsPictureInPicture="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:theme="@style/Theme.Demo">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
43 changes: 43 additions & 0 deletions app/src/main/java/com/geeksforgeeks/demo/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.geeksforgeeks.demo

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.MaterialTheme

/**
* MainActivity is the entry point of the app and hosts the Jetpack Compose UI.
* It also integrates Picture-in-Picture (PiP) functionality using PipManager.
*/
class MainActivity : ComponentActivity() {

// Manages Picture-in-Picture logic such as entering PiP mode and updating bounds
private lateinit var pipManager: PipManager

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// Initialize PipManager with the current Activity context
pipManager = PipManager(this)

// Set the Compose UI content
setContent {
MaterialTheme {
// Display the video player and provide a callback to report its screen bounds
VideoPlayerScreen(
videoUrl = PipManager.VIDEO_URL, // Static video URL
onBoundsChanged = { pipManager.updateBounds(it) } // Report video bounds to PipManager
)
}
}
}

/**
* Called when the user is about to leave the activity (e.g., pressing the Home button).
* This is the trigger point for entering Picture-in-Picture mode.
*/
override fun onUserLeaveHint() {
super.onUserLeaveHint()
pipManager.enterPipModeIfSupported() // Attempt to enter PiP mode if supported
}
}
91 changes: 91 additions & 0 deletions app/src/main/java/com/geeksforgeeks/demo/PipManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package com.geeksforgeeks.demo

import android.app.PendingIntent
import android.app.PictureInPictureParams
import android.app.RemoteAction
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Rect
import android.graphics.drawable.Icon
import android.net.Uri
import android.os.Build
import android.util.Rational
import androidx.annotation.RequiresApi

/**
* PipManager handles all Picture-in-Picture (PiP) related functionality,
* such as entering PiP mode and setting up custom actions.
*/
class PipManager(private val context: Context) {

// Holds the bounds of the video on the screen, used to hint PiP position
private var videoBounds: Rect = Rect()

companion object {
// Static video URL used by the player
val VIDEO_URL: Uri =
Uri.parse("https://www.sample-videos.com/video321/mp4/720/big_buck_bunny_720p_30mb.mp4")
}

// Check if PiP is supported on the current device
private val isPipSupported: Boolean
get() = context.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)

/**
* Updates the bounds of the video view. These bounds are used as a source hint
* for positioning the PiP window when it appears.
*/
fun updateBounds(bounds: Rect) {
videoBounds = bounds
}

/**
* Enters Picture-in-Picture mode if supported by the device and OS version.
* Configures the PiP parameters such as aspect ratio, source rect hint,
* and adds a custom action (play/pause button).
*/
fun enterPipModeIfSupported() {
// Exit early if PiP is not supported or the API level is below 26
if (!isPipSupported || Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return

// Cast context to Activity (required for PiP mode)
val activity = context as? android.app.Activity ?: return

// Build PiP parameters
val pipParams = PictureInPictureParams.Builder()
.setSourceRectHint(videoBounds) // Hint for initial PiP window placement
.setAspectRatio(Rational(16, 9)) // Set fixed aspect ratio
.setActions(listOf(buildRemoteAction())) // Add custom PiP action
.build()

// Request system to enter PiP mode
activity.enterPictureInPictureMode(pipParams)
}

/**
* Creates a custom RemoteAction (e.g. Play/Pause button) that appears in the PiP window.
* The action sends a broadcast, which is received by PipReceiver to toggle playback.
*/
@RequiresApi(Build.VERSION_CODES.O)
private fun buildRemoteAction(): RemoteAction {
// Create an Intent to broadcast when the PiP action is clicked
val intent = Intent(context, PipReceiver::class.java)

// Wrap the intent in a PendingIntent (required for RemoteAction)
val pendingIntent = PendingIntent.getBroadcast(
context,
0,
intent,
PendingIntent.FLAG_IMMUTABLE
)

// Create and return the RemoteAction with an icon, title, description, and intent
return RemoteAction(
Icon.createWithResource(context, R.drawable.play_pause), // Icon shown in PiP
"Play/Pause", // Title
"Toggle playback", // Description
pendingIntent // Action intent
)
}
}
20 changes: 20 additions & 0 deletions app/src/main/java/com/geeksforgeeks/demo/PipReceiver.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.geeksforgeeks.demo

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent

/**
* PipReceiver listens for broadcast actions triggered from Picture-in-Picture (PiP) mode.
* Specifically, it responds to the custom play/pause action added to the PiP controls.
*/
class PipReceiver : BroadcastReceiver() {

/**
* Called when the broadcast associated with the PiP action is received.
* This triggers playback to toggle (play or pause) via the VideoController.
*/
override fun onReceive(context: Context, intent: Intent) {
VideoController.togglePlayPause() // Toggle video playback state
}
}
36 changes: 36 additions & 0 deletions app/src/main/java/com/geeksforgeeks/demo/VideoController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.geeksforgeeks.demo

import android.widget.VideoView
import java.lang.ref.WeakReference

/**
* VideoController is a singleton object that provides centralized control over video playback.
* It allows external components (like the PiP Receiver) to toggle playback without direct access to the VideoView.
*/
object VideoController {

// A weak reference to the VideoView to avoid memory leaks
private var videoViewRef: WeakReference<VideoView>? = null

/**
* Registers the VideoView instance for external control.
* Stores it as a weak reference to prevent memory leaks.
*/
fun setVideoView(videoView: VideoView) {
videoViewRef = WeakReference(videoView)
}

/**
* Toggles the video playback state.
* If the video is playing, it pauses it; otherwise, it starts playback.
*/
fun togglePlayPause() {
videoViewRef?.get()?.let { videoView ->
if (videoView.isPlaying) {
videoView.pause()
} else {
videoView.start()
}
}
}
}
41 changes: 41 additions & 0 deletions app/src/main/java/com/geeksforgeeks/demo/VideoPlayerScreen.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.geeksforgeeks.demo

import android.net.Uri
import android.widget.VideoView
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toAndroidRect
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.viewinterop.AndroidView

/**
* Composable function that displays a VideoView inside a Compose UI using AndroidView interop.
*
* @param videoUrl The URL of the video to be played.
* @param onBoundsChanged Callback to report the video’s on-screen position for PiP purposes.
*/
@Composable
fun VideoPlayerScreen(
videoUrl: Uri,
onBoundsChanged: (android.graphics.Rect) -> Unit
) {
AndroidView(
factory = { context ->
// Creates a VideoView and configures it to play the given video
VideoView(context).apply {
setVideoURI(videoUrl) // Set the video source
start() // Start playback automatically
VideoController.setVideoView(this) // Register with VideoController for external control (e.g., play/pause from PiP)
}
},
modifier = Modifier
.fillMaxWidth() // Makes the video view fill the width of the parent
.onGloballyPositioned {
// Captures the position and size of the VideoView on the screen
// Converts Compose Rect to Android Rect for compatibility with PiP APIs
onBoundsChanged(it.boundsInWindow().toAndroidRect())
}
)
}
Loading