Skip to content

Commit f3f3840

Browse files
StaehliJMGaetan89
andauthored
Improve service (#1042)
Co-authored-by: Gaëtan Muller <[email protected]>
1 parent ed82565 commit f3f3840

File tree

9 files changed

+218
-50
lines changed

9 files changed

+218
-50
lines changed

pillarbox-demo/src/main/AndroidManifest.xml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
<?xml version="1.0" encoding="utf-8"?>
1+
<?xml version="1.0" encoding="utf-8"?><!--
2+
~ Copyright (c) SRG SSR. All rights reserved.
3+
~ License information is available from the LICENSE file.
4+
-->
25
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
36
xmlns:tools="http://schemas.android.com/tools">
47

@@ -49,6 +52,13 @@
4952
android:exported="true"
5053
android:launchMode="singleTask"
5154
android:supportsPictureInPicture="true" />
55+
<activity
56+
android:name=".ui.showcases.integrations.auto.MediaBrowserActivity"
57+
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|smallestScreenSize"
58+
android:exported="true"
59+
android:launchMode="singleTask"
60+
android:supportsPictureInPicture="true" />
61+
5262
<activity
5363
android:name=".ui.showcases.integrations.MediaControllerActivity"
5464
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|smallestScreenSize"
@@ -64,6 +74,7 @@
6474
android:name=".service.DemoMediaSessionService"
6575
android:exported="true"
6676
android:foregroundServiceType="mediaPlayback"
77+
android:stopWithTask="true"
6778
tools:ignore="ExportedService">
6879
<intent-filter>
6980
<action android:name="androidx.media3.session.MediaSessionService" />
@@ -75,6 +86,7 @@
7586
android:enabled="true"
7687
android:exported="true"
7788
android:foregroundServiceType="mediaPlayback"
89+
android:stopWithTask="true"
7890
tools:ignore="ExportedService">
7991
<intent-filter>
8092
<action android:name="androidx.media3.session.MediaLibraryService" />

pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/service/DemoMediaLibraryService.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import androidx.media3.session.LibraryResult
1414
import androidx.media3.session.MediaSession
1515
import ch.srgssr.pillarbox.demo.shared.data.DemoBrowser
1616
import ch.srgssr.pillarbox.demo.shared.di.PlayerModule
17-
import ch.srgssr.pillarbox.demo.ui.showcases.integrations.MediaControllerActivity
17+
import ch.srgssr.pillarbox.demo.ui.showcases.integrations.auto.MediaBrowserActivity
1818
import ch.srgssr.pillarbox.player.session.PillarboxMediaLibraryService
1919
import ch.srgssr.pillarbox.player.session.PillarboxMediaLibrarySession
2020
import ch.srgssr.pillarbox.player.session.PillarboxMediaSession
@@ -43,7 +43,7 @@ class DemoMediaLibraryService : PillarboxMediaLibraryService() {
4343
}
4444

4545
override fun sessionActivity(): PendingIntent {
46-
val intent = Intent(applicationContext, MediaControllerActivity::class.java)
46+
val intent = Intent(applicationContext, MediaBrowserActivity::class.java)
4747
val flags = PendingIntentUtils.appendImmutableFlagIfNeeded(PendingIntent.FLAG_UPDATE_CURRENT)
4848
return PendingIntent.getActivity(
4949
applicationContext,

pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesHome.kt

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import ch.srgssr.pillarbox.demo.ui.components.DemoListItemView
3333
import ch.srgssr.pillarbox.demo.ui.components.DemoListSectionView
3434
import ch.srgssr.pillarbox.demo.ui.player.SimplePlayerActivity
3535
import ch.srgssr.pillarbox.demo.ui.showcases.integrations.MediaControllerActivity
36+
import ch.srgssr.pillarbox.demo.ui.showcases.integrations.auto.MediaBrowserActivity
3637
import ch.srgssr.pillarbox.demo.ui.theme.paddings
3738

3839
/**
@@ -193,7 +194,7 @@ fun ShowcasesHome(navController: NavController) {
193194
HorizontalDivider()
194195

195196
DemoListItemView(
196-
title = stringResource(R.string.auto),
197+
title = stringResource(R.string.media_controller),
197198
modifier = itemModifier(2),
198199
onClick = {
199200
val intent = Intent(context, MediaControllerActivity::class.java)
@@ -204,8 +205,19 @@ fun ShowcasesHome(navController: NavController) {
204205
HorizontalDivider()
205206

206207
DemoListItemView(
207-
title = stringResource(R.string.google_cast),
208+
title = stringResource(R.string.auto),
208209
modifier = itemModifier(3),
210+
onClick = {
211+
val intent = Intent(context, MediaBrowserActivity::class.java)
212+
context.startActivity(intent)
213+
}
214+
)
215+
216+
HorizontalDivider()
217+
218+
DemoListItemView(
219+
title = stringResource(R.string.google_cast),
220+
modifier = itemModifier(4),
209221
onClick = {
210222
navController.navigate(NavigationRoutes.CastShowcase)
211223
}

pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/integrations/MediaControllerViewModel.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ package ch.srgssr.pillarbox.demo.ui.showcases.integrations
77
import android.app.Application
88
import androidx.lifecycle.AndroidViewModel
99
import androidx.lifecycle.viewModelScope
10-
import ch.srgssr.pillarbox.demo.service.DemoMediaLibraryService
10+
import ch.srgssr.pillarbox.demo.service.DemoMediaSessionService
1111
import ch.srgssr.pillarbox.player.extension.RATIONAL_ONE
1212
import ch.srgssr.pillarbox.player.extension.toRational
13-
import ch.srgssr.pillarbox.player.session.PillarboxMediaBrowser
13+
import ch.srgssr.pillarbox.player.session.PillarboxMediaController
1414
import ch.srgssr.pillarbox.player.videoSizeAsFlow
1515
import kotlinx.coroutines.ExperimentalCoroutinesApi
1616
import kotlinx.coroutines.channels.awaitClose
@@ -33,7 +33,7 @@ class MediaControllerViewModel(application: Application) : AndroidViewModel(appl
3333
* Player
3434
*/
3535
val player = callbackFlow {
36-
val mediaBrowser = PillarboxMediaBrowser.Builder(application, DemoMediaLibraryService::class.java).build()
36+
val mediaBrowser = PillarboxMediaController.Builder(application, DemoMediaSessionService::class.java).build()
3737
trySend(mediaBrowser)
3838
awaitClose {
3939
mediaBrowser.release()
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
* Copyright (c) SRG SSR. All rights reserved.
3+
* License information is available from the LICENSE file.
4+
*/
5+
package ch.srgssr.pillarbox.demo.ui.showcases.integrations.auto
6+
7+
import android.app.PictureInPictureParams
8+
import android.content.pm.PackageManager
9+
import android.content.res.Configuration
10+
import android.os.Build
11+
import android.os.Bundle
12+
import androidx.activity.ComponentActivity
13+
import androidx.activity.compose.setContent
14+
import androidx.activity.viewModels
15+
import androidx.annotation.RequiresApi
16+
import androidx.compose.foundation.layout.fillMaxSize
17+
import androidx.compose.material3.MaterialTheme
18+
import androidx.compose.material3.Surface
19+
import androidx.compose.runtime.Composable
20+
import androidx.compose.runtime.collectAsState
21+
import androidx.compose.runtime.getValue
22+
import androidx.compose.ui.Modifier
23+
import androidx.lifecycle.Lifecycle
24+
import androidx.lifecycle.flowWithLifecycle
25+
import androidx.lifecycle.lifecycleScope
26+
import androidx.media3.common.Player
27+
import ch.srgssr.pillarbox.analytics.SRGAnalytics
28+
import ch.srgssr.pillarbox.demo.DemoPageView
29+
import ch.srgssr.pillarbox.demo.trackPagView
30+
import ch.srgssr.pillarbox.demo.ui.player.DemoPlayerView
31+
import ch.srgssr.pillarbox.demo.ui.theme.PillarboxTheme
32+
import kotlinx.coroutines.flow.collectLatest
33+
import kotlinx.coroutines.launch
34+
35+
/**
36+
* Media controller activity that handles a MediaBrowserService to play content with Android Auto.
37+
*
38+
* Using official guides for background playback at https://developer.android.com/guide/topics/media/media3/getting-started/playing-in-background
39+
*
40+
* @constructor Create empty Media controller activity
41+
*/
42+
class MediaBrowserActivity : ComponentActivity() {
43+
private val browserViewModel: MediaBrowserViewModel by viewModels()
44+
45+
override fun onCreate(savedInstanceState: Bundle?) {
46+
super.onCreate(savedInstanceState)
47+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
48+
lifecycleScope.launch {
49+
browserViewModel.pictureInPictureRatio.flowWithLifecycle(lifecycle, Lifecycle.State.CREATED).collectLatest {
50+
val params = PictureInPictureParams.Builder()
51+
.setAspectRatio(it)
52+
.build()
53+
setPictureInPictureParams(params)
54+
}
55+
}
56+
}
57+
58+
setContent {
59+
PillarboxTheme {
60+
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
61+
val mediaBrowser by browserViewModel.player.collectAsState()
62+
mediaBrowser?.let { player ->
63+
MainView(player = player)
64+
}
65+
}
66+
}
67+
}
68+
}
69+
70+
private fun isPictureInPicturePossible(): Boolean {
71+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
72+
return packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
73+
}
74+
return false
75+
}
76+
77+
@Composable
78+
private fun MainView(player: Player) {
79+
val pictureInPictureClick: (() -> Unit)? = if (isPictureInPicturePossible()) this::startPictureInPicture else null
80+
val pictureInPicture by browserViewModel.pictureInPictureEnabled.collectAsState()
81+
DemoPlayerView(
82+
player = player,
83+
pictureInPicture = pictureInPicture,
84+
pictureInPictureClick = pictureInPictureClick,
85+
displayPlaylist = true
86+
)
87+
}
88+
89+
private fun startPictureInPicture() {
90+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
91+
val params = PictureInPictureParams.Builder()
92+
.setAspectRatio(browserViewModel.pictureInPictureRatio.value)
93+
.build()
94+
enterPictureInPictureMode(params)
95+
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
96+
@Suppress("DEPRECATION")
97+
enterPictureInPictureMode()
98+
}
99+
}
100+
101+
@RequiresApi(Build.VERSION_CODES.O)
102+
override fun onPictureInPictureModeChanged(
103+
isInPictureInPictureMode: Boolean,
104+
newConfig: Configuration
105+
) {
106+
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
107+
browserViewModel.pictureInPictureEnabled.value = isInPictureInPictureMode
108+
}
109+
110+
override fun onConfigurationChanged(newConfig: Configuration) {
111+
super.onConfigurationChanged(newConfig)
112+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) return
113+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
114+
browserViewModel.pictureInPictureEnabled.value = isInPictureInPictureMode
115+
}
116+
}
117+
118+
override fun onResume() {
119+
super.onResume()
120+
SRGAnalytics.trackPagView(DemoPageView("media browser player", levels = listOf("app", "pillarbox")))
121+
}
122+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright (c) SRG SSR. All rights reserved.
3+
* License information is available from the LICENSE file.
4+
*/
5+
package ch.srgssr.pillarbox.demo.ui.showcases.integrations.auto
6+
7+
import android.app.Application
8+
import androidx.lifecycle.AndroidViewModel
9+
import androidx.lifecycle.viewModelScope
10+
import ch.srgssr.pillarbox.demo.service.DemoMediaLibraryService
11+
import ch.srgssr.pillarbox.player.extension.RATIONAL_ONE
12+
import ch.srgssr.pillarbox.player.extension.toRational
13+
import ch.srgssr.pillarbox.player.session.PillarboxMediaBrowser
14+
import ch.srgssr.pillarbox.player.videoSizeAsFlow
15+
import kotlinx.coroutines.ExperimentalCoroutinesApi
16+
import kotlinx.coroutines.channels.awaitClose
17+
import kotlinx.coroutines.flow.MutableStateFlow
18+
import kotlinx.coroutines.flow.SharingStarted
19+
import kotlinx.coroutines.flow.callbackFlow
20+
import kotlinx.coroutines.flow.filterNotNull
21+
import kotlinx.coroutines.flow.flatMapLatest
22+
import kotlinx.coroutines.flow.map
23+
import kotlinx.coroutines.flow.stateIn
24+
25+
/**
26+
* Media browser view model
27+
*
28+
* @param application
29+
*/
30+
class MediaBrowserViewModel(application: Application) : AndroidViewModel(application) {
31+
32+
/**
33+
* Player
34+
*/
35+
val player = callbackFlow {
36+
val mediaBrowser = PillarboxMediaBrowser.Builder(application, DemoMediaLibraryService::class.java).build()
37+
trySend(mediaBrowser)
38+
awaitClose {
39+
mediaBrowser.release()
40+
}
41+
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
42+
43+
/**
44+
* Picture in picture enabled
45+
*/
46+
val pictureInPictureEnabled = MutableStateFlow(false)
47+
48+
/**
49+
* Picture in picture aspect ratio
50+
*/
51+
@OptIn(ExperimentalCoroutinesApi::class)
52+
var pictureInPictureRatio = player.filterNotNull().flatMapLatest { mediaBrowser ->
53+
mediaBrowser.videoSizeAsFlow().map { it.toRational() }
54+
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), RATIONAL_ONE)
55+
}

pillarbox-demo/src/main/res/values/strings.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
<string name="layouts">Layouts</string>
1414
<string name="story">Story</string>
1515
<string name="simple_player">Simple</string>
16-
<string name="auto">MediaController (Android Auto)</string>
16+
<string name="auto">MediaBrowser (Android Auto)</string>
17+
<string name="media_controller">MediaSession service</string>
1718
<string name="adaptive">Resizable player</string>
1819
<string name="player_swap">Multi player</string>
1920
<string name="misc">Misc</string>

pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibraryService.kt

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
package ch.srgssr.pillarbox.player.session
66

77
import android.app.PendingIntent
8-
import android.content.Intent
98
import androidx.media3.session.MediaLibraryService
109
import androidx.media3.session.MediaSession
1110
import androidx.media3.session.MediaSession.ControllerInfo
@@ -36,6 +35,7 @@ import ch.srgssr.pillarbox.player.utils.PendingIntentUtils
3635
* android:name=".service.DemoMediaLibraryService"
3736
* android:enabled="true"
3837
* android:exported="true"
38+
* android:stopWithTask="true"
3939
* android:foregroundServiceType="mediaPlayback">
4040
* <intent-filter>
4141
* <action android:name="androidx.media3.session.MediaLibraryService" />
@@ -57,11 +57,6 @@ import ch.srgssr.pillarbox.player.utils.PendingIntentUtils
5757
abstract class PillarboxMediaLibraryService : MediaLibraryService() {
5858
private var mediaSession: PillarboxMediaLibrarySession? = null
5959

60-
/**
61-
* Release on task removed
62-
*/
63-
var releaseOnTaskRemoved = true
64-
6560
/**
6661
* Set player to use with this Service.
6762
* @param player [PillarboxPlayer] to link to this service.
@@ -99,9 +94,8 @@ abstract class PillarboxMediaLibraryService : MediaLibraryService() {
9994

10095
/**
10196
* Release the player and the MediaSession.
102-
* The [mediaSession] is set to null after this call
103-
*
104-
* called automatically in [onDestroy] and [onTaskRemoved] is [releaseOnTaskRemoved] = true
97+
* The [mediaSession] is set to null after this call.
98+
* Called automatically in [onDestroy]
10599
*/
106100
open fun release() {
107101
mediaSession?.run {
@@ -112,18 +106,7 @@ abstract class PillarboxMediaLibraryService : MediaLibraryService() {
112106
}
113107

114108
override fun onDestroy() {
115-
release()
116109
super.onDestroy()
117-
}
118-
119-
/**
120-
* We choose to stop playback when user remove application from the tasks
121-
*/
122-
override fun onTaskRemoved(rootIntent: Intent?) {
123-
super.onTaskRemoved(rootIntent)
124-
if (releaseOnTaskRemoved) {
125-
release()
126-
stopSelf()
127-
}
110+
release()
128111
}
129112
}

0 commit comments

Comments
 (0)