Skip to content

Commit a82827b

Browse files
committed
Adding pip snippets
1 parent c381c78 commit a82827b

File tree

2 files changed

+371
-2
lines changed

2 files changed

+371
-2
lines changed

compose/snippets/build.gradle

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,13 @@ dependencies {
5555
implementation "androidx.compose.ui:ui-tooling-preview"
5656
implementation "androidx.compose.ui:ui-viewbinding"
5757
implementation "androidx.fragment:fragment:1.5.4"
58-
implementation 'androidx.activity:activity-compose:1.6.1'
58+
implementation 'androidx.activity:activity-compose:1.9.0-alpha01'
5959
implementation 'androidx.compose.material3:material3:1.0.1'
6060
implementation 'androidx.compose.runtime:runtime-livedata'
61-
implementation 'androidx.core:core-ktx:1.9.0'
61+
implementation 'androidx.core:core-ktx:1.12.0'
6262
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
6363
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1'
64+
implementation 'androidx.media3:media3-exoplayer:1.2.1'
6465
implementation 'androidx.navigation:navigation-compose:2.5.3'
6566
testImplementation 'junit:junit:4.13.2'
6667
androidTestImplementation(composeBom)
Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
1+
/*
2+
* Copyright 2024 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5+
* except in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the
10+
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11+
* KIND, either express or implied. See the License for the specific language governing
12+
* permissions and limitations under the License.
13+
*/
14+
15+
package com.example.compose.snippets.pictureInPicture
16+
17+
import android.app.PendingIntent
18+
import android.app.PictureInPictureParams
19+
import android.app.RemoteAction
20+
import android.content.BroadcastReceiver
21+
import android.content.Context
22+
import android.content.ContextWrapper
23+
import android.content.Intent
24+
import android.content.IntentFilter
25+
import android.graphics.drawable.Icon
26+
import android.os.Build
27+
import android.util.Rational
28+
import androidx.activity.ComponentActivity
29+
import androidx.annotation.DrawableRes
30+
import androidx.annotation.RequiresApi
31+
import androidx.annotation.StringRes
32+
import androidx.compose.foundation.layout.Column
33+
import androidx.compose.material3.Button
34+
import androidx.compose.material3.Text
35+
import androidx.compose.runtime.Composable
36+
import androidx.compose.runtime.DisposableEffect
37+
import androidx.compose.runtime.getValue
38+
import androidx.compose.runtime.mutableStateOf
39+
import androidx.compose.runtime.remember
40+
import androidx.compose.runtime.rememberUpdatedState
41+
import androidx.compose.runtime.setValue
42+
import androidx.compose.ui.Modifier
43+
import androidx.compose.ui.graphics.toAndroidRectF
44+
import androidx.compose.ui.layout.boundsInWindow
45+
import androidx.compose.ui.layout.onGloballyPositioned
46+
import androidx.compose.ui.platform.LocalContext
47+
import androidx.core.app.PictureInPictureModeChangedInfo
48+
import androidx.core.content.ContextCompat
49+
import androidx.core.graphics.toRect
50+
import androidx.core.util.Consumer
51+
import androidx.media3.common.Player
52+
import androidx.media3.exoplayer.ExoPlayer
53+
54+
var shouldEnterPipMode by mutableStateOf(false)
55+
56+
// [START region_tag_13]
57+
// Constants for broadcast receiver
58+
const val ACTION_BROADCAST_CONTROL = "broadcast_control"
59+
60+
// Intent extra for broadcast controls from Picture-in-Picture mode.
61+
const val EXTRA_CONTROL_TYPE = "control_type"
62+
const val EXTRA_CONTROL_PLAY = 1
63+
const val EXTRA_CONTROL_PAUSE = 2
64+
const val REQUEST_PLAY = 5
65+
const val REQUEST_PAUSE = 6
66+
// [END region_tag_13]
67+
68+
@Composable
69+
fun PipListenerPreAPI12(shouldEnterPipMode: Boolean) {
70+
// [START region_tag_1]
71+
// [START region_tag_10]
72+
val currentShouldEnterPipMode by rememberUpdatedState(newValue = shouldEnterPipMode)
73+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
74+
Build.VERSION.SDK_INT < Build.VERSION_CODES.S
75+
) {
76+
val context = LocalContext.current
77+
DisposableEffect(context) {
78+
val onUserLeaveBehavior = {
79+
if (currentShouldEnterPipMode) {
80+
context.findActivity()
81+
.enterPictureInPictureMode(PictureInPictureParams.Builder().build())
82+
}
83+
}
84+
// [END region_tag_10]
85+
context.findActivity().addOnUserLeaveHintListener(
86+
onUserLeaveBehavior
87+
)
88+
onDispose {
89+
context.findActivity().removeOnUserLeaveHintListener(
90+
onUserLeaveBehavior
91+
)
92+
}
93+
}
94+
}
95+
// [END region_tag_1]
96+
}
97+
98+
@Composable
99+
fun VideoPlayer(
100+
modifier: Modifier = Modifier
101+
) {
102+
// [END region_tag_2]
103+
val context = LocalContext.current
104+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
105+
val pipModifier = modifier.onGloballyPositioned { layoutCoordinates ->
106+
val builder = PictureInPictureParams.Builder()
107+
108+
// Add autoEnterEnabled for versions S and up
109+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
110+
builder.setAutoEnterEnabled(true)
111+
}
112+
context.findActivity().setPictureInPictureParams(builder.build())
113+
}
114+
}
115+
// [END region_tag_2]
116+
}
117+
118+
// [START region_tag_3]
119+
internal fun Context.findActivity(): ComponentActivity {
120+
var context = this
121+
while (context is ContextWrapper) {
122+
if (context is ComponentActivity) return context
123+
context = context.baseContext
124+
}
125+
throw IllegalStateException("Picture in picture should be called in the context of an Activity")
126+
}
127+
// [END region_tag_3]
128+
129+
@Composable
130+
fun VideoPlayerScreen(
131+
) {
132+
// [START region_tag_3]
133+
val context = LocalContext.current
134+
Button(onClick = {
135+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
136+
context.findActivity().enterPictureInPictureMode(
137+
// the parameters have been set by previous calls
138+
PictureInPictureParams.Builder().build()
139+
)
140+
}
141+
}) {
142+
Text(text = "Enter PiP mode!")
143+
}
144+
// [END region_tag_4]
145+
}
146+
147+
// [START region_tag_5]
148+
@Composable
149+
fun isInPipMode(): Boolean {
150+
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
151+
val activity = LocalContext.current.findActivity()
152+
var pipMode by remember { mutableStateOf(activity.isInPictureInPictureMode) }
153+
DisposableEffect(activity) {
154+
val observer = Consumer<PictureInPictureModeChangedInfo> { info ->
155+
pipMode = info.isInPictureInPictureMode
156+
}
157+
activity.addOnPictureInPictureModeChangedListener(
158+
observer
159+
)
160+
onDispose { activity.removeOnPictureInPictureModeChangedListener(observer) }
161+
}
162+
163+
164+
return pipMode
165+
} else {
166+
return false
167+
}
168+
}
169+
// [END region_tag_5]
170+
171+
@Composable
172+
fun VideoPlayerScreen(
173+
modifier: Modifier = Modifier,
174+
) {
175+
// [START region_tag_6]
176+
val inPipMode = isInPipMode()
177+
178+
Column(modifier = modifier) {
179+
// This text will only show up when the app is in PiP mode
180+
if (!inPipMode) {
181+
Text(
182+
text = "Picture in Picture",
183+
)
184+
}
185+
VideoPlayer()
186+
}
187+
// [END region_tag_6]
188+
}
189+
190+
fun initializePlayer(context: Context) {
191+
val player = ExoPlayer.Builder(context.applicationContext)
192+
.build().apply {}
193+
194+
// [START region_tag_7]
195+
player.addListener(object : Player.Listener {
196+
override fun onIsPlayingChanged(isPlaying: Boolean) {
197+
shouldEnterPipMode = isPlaying
198+
}
199+
})
200+
// [END region_tag_7]
201+
}
202+
203+
// [START region_tag_8]
204+
fun releasePlayer() {
205+
shouldEnterPipMode = false
206+
}
207+
// [END region_tag_8]
208+
209+
@Composable
210+
fun VideoPlayer(
211+
shouldEnterPipMode: Boolean,
212+
modifier: Modifier = Modifier,
213+
) {
214+
// [START region_tag_9]
215+
val context = LocalContext.current
216+
217+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
218+
val pipModifier = modifier.onGloballyPositioned { layoutCoordinates ->
219+
val builder = PictureInPictureParams.Builder()
220+
221+
// Add autoEnterEnabled for versions S and up
222+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
223+
builder.setAutoEnterEnabled(shouldEnterPipMode)
224+
}
225+
context.findActivity().setPictureInPictureParams(builder.build())
226+
}
227+
}
228+
// [END region_tag_9]
229+
}
230+
231+
232+
@Composable
233+
fun VideoPlayer1(
234+
shouldEnterPipMode: Boolean,
235+
modifier: Modifier = Modifier,
236+
) {
237+
// [START region_tag_11]
238+
val context = LocalContext.current
239+
240+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
241+
val pipModifier = modifier.onGloballyPositioned { layoutCoordinates ->
242+
val builder = PictureInPictureParams.Builder()
243+
if (shouldEnterPipMode) {
244+
val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect()
245+
builder.setSourceRectHint(sourceRect)
246+
}
247+
248+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
249+
builder.setAutoEnterEnabled(shouldEnterPipMode)
250+
}
251+
context.findActivity().setPictureInPictureParams(builder.build())
252+
}
253+
}
254+
// [END region_tag_11]
255+
}
256+
257+
@Composable
258+
fun VideoPlayer2(
259+
shouldEnterPipMode: Boolean,
260+
modifier: Modifier = Modifier,
261+
) {
262+
// [START region_tag_12]
263+
val context = LocalContext.current
264+
265+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
266+
val pipModifier = modifier.onGloballyPositioned { layoutCoordinates ->
267+
val builder = PictureInPictureParams.Builder()
268+
269+
if (shouldEnterPipMode) {
270+
val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect()
271+
builder.setSourceRectHint(sourceRect)
272+
builder.setAspectRatio(
273+
Rational(sourceRect.width(), sourceRect.height())
274+
)
275+
}
276+
277+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
278+
builder.setAutoEnterEnabled(shouldEnterPipMode)
279+
}
280+
context.findActivity().setPictureInPictureParams(builder.build())
281+
}
282+
}
283+
// [END region_tag_12]
284+
}
285+
286+
// [START region_tag_14]
287+
@RequiresApi(Build.VERSION_CODES.O)
288+
private fun buildRemoteAction(
289+
@DrawableRes iconResId: Int,
290+
@StringRes titleResId: Int,
291+
requestCode: Int,
292+
controlType: Int,
293+
context: Context
294+
): RemoteAction {
295+
return RemoteAction(
296+
Icon.createWithResource(context, iconResId),
297+
context.getString(titleResId),
298+
context.getString(titleResId),
299+
PendingIntent.getBroadcast(
300+
context,
301+
requestCode,
302+
Intent(ACTION_BROADCAST_CONTROL)
303+
.putExtra(EXTRA_CONTROL_TYPE, controlType),
304+
PendingIntent.FLAG_IMMUTABLE
305+
)
306+
)
307+
}
308+
// [END region_tag_14]
309+
310+
// [START region_tag_15]
311+
@RequiresApi(Build.VERSION_CODES.O)
312+
@Composable
313+
fun BroadcastReceiver(player: Player?) {
314+
if (isInPipMode() && player != null) {
315+
val context = LocalContext.current
316+
317+
DisposableEffect(key1 = player) {
318+
val broadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() {
319+
override fun onReceive(context: Context?, intent: Intent?) {
320+
if ((intent == null) || (intent.action != ACTION_BROADCAST_CONTROL)) {
321+
return
322+
}
323+
324+
when (intent.getIntExtra(EXTRA_CONTROL_TYPE, 0)) {
325+
EXTRA_CONTROL_PAUSE -> player.pause()
326+
EXTRA_CONTROL_PLAY -> player.play()
327+
}
328+
}
329+
}
330+
ContextCompat.registerReceiver(
331+
context,
332+
broadcastReceiver,
333+
IntentFilter(ACTION_BROADCAST_CONTROL),
334+
ContextCompat.RECEIVER_NOT_EXPORTED
335+
)
336+
onDispose {
337+
context.unregisterReceiver(broadcastReceiver)
338+
}
339+
}
340+
}
341+
}
342+
// [END region_tag_15]
343+
344+
@RequiresApi(Build.VERSION_CODES.O)
345+
fun listOfRemoteActions(isPlaying: Boolean, context: Context): List<RemoteAction> {
346+
return listOf()
347+
}
348+
349+
// [START region_tag_16]
350+
@Composable
351+
fun VideoPlayer4(
352+
shouldEnterPipMode: Boolean,
353+
modifier: Modifier = Modifier,
354+
355+
) {
356+
val context = LocalContext.current
357+
358+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
359+
val pipModifier = modifier.onGloballyPositioned { layoutCoordinates ->
360+
val builder = PictureInPictureParams.Builder()
361+
builder.setActions(
362+
listOfRemoteActions(shouldEnterPipMode, context)
363+
)
364+
context.findActivity().setPictureInPictureParams(builder.build())
365+
}
366+
}
367+
}
368+
// [END region_tag_16]

0 commit comments

Comments
 (0)