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