Skip to content

Commit

Permalink
Adding pip snippets
Browse files Browse the repository at this point in the history
  • Loading branch information
MagicalMeghan committed Jan 22, 2024
1 parent c381c78 commit a82827b
Show file tree
Hide file tree
Showing 2 changed files with 371 additions and 2 deletions.
5 changes: 3 additions & 2 deletions compose/snippets/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,13 @@ dependencies {
implementation "androidx.compose.ui:ui-tooling-preview"
implementation "androidx.compose.ui:ui-viewbinding"
implementation "androidx.fragment:fragment:1.5.4"
implementation 'androidx.activity:activity-compose:1.6.1'
implementation 'androidx.activity:activity-compose:1.9.0-alpha01'
implementation 'androidx.compose.material3:material3:1.0.1'
implementation 'androidx.compose.runtime:runtime-livedata'
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1'
implementation 'androidx.media3:media3-exoplayer:1.2.1'
implementation 'androidx.navigation:navigation-compose:2.5.3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation(composeBom)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,368 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package com.example.compose.snippets.pictureInPicture

import android.app.PendingIntent
import android.app.PictureInPictureParams
import android.app.RemoteAction
import android.content.BroadcastReceiver
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.IntentFilter
import android.graphics.drawable.Icon
import android.os.Build
import android.util.Rational
import androidx.activity.ComponentActivity
import androidx.annotation.DrawableRes
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toAndroidRectF
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import androidx.core.app.PictureInPictureModeChangedInfo
import androidx.core.content.ContextCompat
import androidx.core.graphics.toRect
import androidx.core.util.Consumer
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer

var shouldEnterPipMode by mutableStateOf(false)

// [START region_tag_13]
// Constants for broadcast receiver
const val ACTION_BROADCAST_CONTROL = "broadcast_control"

// Intent extra for broadcast controls from Picture-in-Picture mode.
const val EXTRA_CONTROL_TYPE = "control_type"
const val EXTRA_CONTROL_PLAY = 1
const val EXTRA_CONTROL_PAUSE = 2
const val REQUEST_PLAY = 5
const val REQUEST_PAUSE = 6
// [END region_tag_13]

@Composable
fun PipListenerPreAPI12(shouldEnterPipMode: Boolean) {
// [START region_tag_1]
// [START region_tag_10]
val currentShouldEnterPipMode by rememberUpdatedState(newValue = shouldEnterPipMode)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
Build.VERSION.SDK_INT < Build.VERSION_CODES.S
) {
val context = LocalContext.current
DisposableEffect(context) {
val onUserLeaveBehavior = {
if (currentShouldEnterPipMode) {
context.findActivity()
.enterPictureInPictureMode(PictureInPictureParams.Builder().build())
}
}
// [END region_tag_10]
context.findActivity().addOnUserLeaveHintListener(
onUserLeaveBehavior
)
onDispose {
context.findActivity().removeOnUserLeaveHintListener(
onUserLeaveBehavior
)
}
}
}
// [END region_tag_1]
}

@Composable
fun VideoPlayer(
modifier: Modifier = Modifier
) {
// [END region_tag_2]
val context = LocalContext.current
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val pipModifier = modifier.onGloballyPositioned { layoutCoordinates ->
val builder = PictureInPictureParams.Builder()

// Add autoEnterEnabled for versions S and up
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
builder.setAutoEnterEnabled(true)
}
context.findActivity().setPictureInPictureParams(builder.build())
}
}
// [END region_tag_2]
}

// [START region_tag_3]
internal fun Context.findActivity(): ComponentActivity {
var context = this
while (context is ContextWrapper) {
if (context is ComponentActivity) return context
context = context.baseContext
}
throw IllegalStateException("Picture in picture should be called in the context of an Activity")
}
// [END region_tag_3]

@Composable
fun VideoPlayerScreen(
) {
// [START region_tag_3]
val context = LocalContext.current
Button(onClick = {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.findActivity().enterPictureInPictureMode(
// the parameters have been set by previous calls
PictureInPictureParams.Builder().build()
)
}
}) {
Text(text = "Enter PiP mode!")
}
// [END region_tag_4]
}

// [START region_tag_5]
@Composable
fun isInPipMode(): Boolean {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val activity = LocalContext.current.findActivity()
var pipMode by remember { mutableStateOf(activity.isInPictureInPictureMode) }
DisposableEffect(activity) {
val observer = Consumer<PictureInPictureModeChangedInfo> { info ->
pipMode = info.isInPictureInPictureMode
}
activity.addOnPictureInPictureModeChangedListener(
observer
)
onDispose { activity.removeOnPictureInPictureModeChangedListener(observer) }
}


return pipMode
} else {
return false
}
}
// [END region_tag_5]

@Composable
fun VideoPlayerScreen(
modifier: Modifier = Modifier,
) {
// [START region_tag_6]
val inPipMode = isInPipMode()

Column(modifier = modifier) {
// This text will only show up when the app is in PiP mode
if (!inPipMode) {
Text(
text = "Picture in Picture",
)
}
VideoPlayer()
}
// [END region_tag_6]
}

fun initializePlayer(context: Context) {
val player = ExoPlayer.Builder(context.applicationContext)
.build().apply {}

// [START region_tag_7]
player.addListener(object : Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) {
shouldEnterPipMode = isPlaying
}
})
// [END region_tag_7]
}

// [START region_tag_8]
fun releasePlayer() {
shouldEnterPipMode = false
}
// [END region_tag_8]

@Composable
fun VideoPlayer(
shouldEnterPipMode: Boolean,
modifier: Modifier = Modifier,
) {
// [START region_tag_9]
val context = LocalContext.current

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val pipModifier = modifier.onGloballyPositioned { layoutCoordinates ->
val builder = PictureInPictureParams.Builder()

// Add autoEnterEnabled for versions S and up
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
builder.setAutoEnterEnabled(shouldEnterPipMode)
}
context.findActivity().setPictureInPictureParams(builder.build())
}
}
// [END region_tag_9]
}


@Composable
fun VideoPlayer1(
shouldEnterPipMode: Boolean,
modifier: Modifier = Modifier,
) {
// [START region_tag_11]
val context = LocalContext.current

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val pipModifier = modifier.onGloballyPositioned { layoutCoordinates ->
val builder = PictureInPictureParams.Builder()
if (shouldEnterPipMode) {
val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect()
builder.setSourceRectHint(sourceRect)
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
builder.setAutoEnterEnabled(shouldEnterPipMode)
}
context.findActivity().setPictureInPictureParams(builder.build())
}
}
// [END region_tag_11]
}

@Composable
fun VideoPlayer2(
shouldEnterPipMode: Boolean,
modifier: Modifier = Modifier,
) {
// [START region_tag_12]
val context = LocalContext.current

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val pipModifier = modifier.onGloballyPositioned { layoutCoordinates ->
val builder = PictureInPictureParams.Builder()

if (shouldEnterPipMode) {
val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect()
builder.setSourceRectHint(sourceRect)
builder.setAspectRatio(
Rational(sourceRect.width(), sourceRect.height())
)
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
builder.setAutoEnterEnabled(shouldEnterPipMode)
}
context.findActivity().setPictureInPictureParams(builder.build())
}
}
// [END region_tag_12]
}

// [START region_tag_14]
@RequiresApi(Build.VERSION_CODES.O)
private fun buildRemoteAction(
@DrawableRes iconResId: Int,
@StringRes titleResId: Int,
requestCode: Int,
controlType: Int,
context: Context
): RemoteAction {
return RemoteAction(
Icon.createWithResource(context, iconResId),
context.getString(titleResId),
context.getString(titleResId),
PendingIntent.getBroadcast(
context,
requestCode,
Intent(ACTION_BROADCAST_CONTROL)
.putExtra(EXTRA_CONTROL_TYPE, controlType),
PendingIntent.FLAG_IMMUTABLE
)
)
}
// [END region_tag_14]

// [START region_tag_15]
@RequiresApi(Build.VERSION_CODES.O)
@Composable
fun BroadcastReceiver(player: Player?) {
if (isInPipMode() && player != null) {
val context = LocalContext.current

DisposableEffect(key1 = player) {
val broadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if ((intent == null) || (intent.action != ACTION_BROADCAST_CONTROL)) {
return
}

when (intent.getIntExtra(EXTRA_CONTROL_TYPE, 0)) {
EXTRA_CONTROL_PAUSE -> player.pause()
EXTRA_CONTROL_PLAY -> player.play()
}
}
}
ContextCompat.registerReceiver(
context,
broadcastReceiver,
IntentFilter(ACTION_BROADCAST_CONTROL),
ContextCompat.RECEIVER_NOT_EXPORTED
)
onDispose {
context.unregisterReceiver(broadcastReceiver)
}
}
}
}
// [END region_tag_15]

@RequiresApi(Build.VERSION_CODES.O)
fun listOfRemoteActions(isPlaying: Boolean, context: Context): List<RemoteAction> {
return listOf()
}

// [START region_tag_16]
@Composable
fun VideoPlayer4(
shouldEnterPipMode: Boolean,
modifier: Modifier = Modifier,

) {
val context = LocalContext.current

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val pipModifier = modifier.onGloballyPositioned { layoutCoordinates ->
val builder = PictureInPictureParams.Builder()
builder.setActions(
listOfRemoteActions(shouldEnterPipMode, context)
)
context.findActivity().setPictureInPictureParams(builder.build())
}
}
}
// [END region_tag_16]

0 comments on commit a82827b

Please sign in to comment.