diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/di/PlayerModule.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/di/PlayerModule.kt index 44eb316a4..45ffe3074 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/di/PlayerModule.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/di/PlayerModule.kt @@ -23,23 +23,26 @@ import java.net.URL */ object PlayerModule { - private fun provideIntegrationLayerItemSource(context: Context): MediaCompositionMediaItemSource = + private fun provideIntegrationLayerItemSource(context: Context, ilHost: URL = IlHost.DEFAULT): MediaCompositionMediaItemSource = MediaCompositionMediaItemSource( - mediaCompositionDataSource = DefaultMediaCompositionDataSource(vector = context.getVector()), + mediaCompositionDataSource = DefaultMediaCompositionDataSource(vector = context.getVector(), baseUrl = ilHost), ) /** * Provide mixed item source that load Url and Urn */ - fun provideMixedItemSource(context: Context): MixedMediaItemSource = MixedMediaItemSource( - provideIntegrationLayerItemSource(context) + fun provideMixedItemSource( + context: Context, + ilHost: URL = IlHost.DEFAULT + ): MixedMediaItemSource = MixedMediaItemSource( + provideIntegrationLayerItemSource(context, ilHost) ) /** * Provide default player that allow to play urls and urns content from the SRG */ - fun provideDefaultPlayer(context: Context): PillarboxPlayer { - return DefaultPillarbox(context = context, mediaItemSource = provideMixedItemSource(context)) + fun provideDefaultPlayer(context: Context, ilHost: URL = IlHost.DEFAULT): PillarboxPlayer { + return DefaultPillarbox(context = context, mediaItemSource = provideMixedItemSource(context, ilHost)) } /** diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/MainNavigation.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/MainNavigation.kt index 02b32671b..14726769b 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/MainNavigation.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/MainNavigation.kt @@ -7,9 +7,15 @@ package ch.srgssr.pillarbox.demo.ui import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MenuDefaults import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold @@ -18,10 +24,16 @@ import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp import androidx.core.content.res.ResourcesCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle @@ -42,7 +54,9 @@ import androidx.navigation.navigation import ch.srg.dataProvider.integrationlayer.request.image.ImageWidth import ch.srg.dataProvider.integrationlayer.request.image.decorated import ch.srgssr.pillarbox.analytics.SRGAnalytics +import ch.srgssr.pillarbox.core.business.integrationlayer.service.IlHost import ch.srgssr.pillarbox.demo.DemoPageView +import ch.srgssr.pillarbox.demo.R import ch.srgssr.pillarbox.demo.shared.data.DemoItem import ch.srgssr.pillarbox.demo.shared.di.PlayerModule import ch.srgssr.pillarbox.demo.shared.ui.HomeDestination @@ -54,10 +68,13 @@ import ch.srgssr.pillarbox.demo.ui.integrationLayer.SearchView import ch.srgssr.pillarbox.demo.ui.integrationLayer.listNavGraph import ch.srgssr.pillarbox.demo.ui.player.SimplePlayerActivity import ch.srgssr.pillarbox.demo.ui.showcases.showCasesNavGraph +import ch.srgssr.pillarbox.demo.ui.theme.PillarboxTheme +import ch.srgssr.pillarbox.demo.ui.theme.paddings +import java.net.URL private val bottomNavItems = listOf(HomeDestination.Examples, HomeDestination.ShowCases, HomeDestination.Lists, HomeDestination.Search) private val topLevelRoutes = - listOf(HomeDestination.Examples.route, NavigationRoutes.showcaseList, NavigationRoutes.contentLists, HomeDestination.Search) + listOf(HomeDestination.Examples.route, NavigationRoutes.showcaseList, NavigationRoutes.contentLists, HomeDestination.Search.route) /** * Main view with all the navigation @@ -69,6 +86,9 @@ fun MainNavigation() { val navController = rememberNavController() val navBackStackEntry by navController.currentBackStackEntryAsState() val currentDestination = navBackStackEntry?.destination + + var ilHost by remember { mutableStateOf(IlHost.DEFAULT) } + Scaffold( topBar = { TopAppBar( @@ -88,6 +108,14 @@ fun MainNavigation() { } } } + }, + actions = { + if (currentDestination?.route == NavigationRoutes.contentLists) { + ListsMenu( + currentServer = ilHost, + onServerSelected = { ilHost = it } + ) + } } ) }, @@ -96,9 +124,7 @@ fun MainNavigation() { } ) { innerPadding -> val context = LocalContext.current - val ilRepository = remember { - PlayerModule.createIlRepository(context) - } + NavHost(navController = navController, startDestination = HomeDestination.Examples.route, modifier = Modifier.padding(innerPadding)) { composable(HomeDestination.Examples.route, DemoPageView("home", listOf("app", "pillarbox", "examples"))) { ExamplesHome() @@ -109,7 +135,9 @@ fun MainNavigation() { } navigation(startDestination = NavigationRoutes.contentLists, route = HomeDestination.Lists.route) { - listNavGraph(navController, ilRepository) + val ilRepository = PlayerModule.createIlRepository(context, ilHost) + + listNavGraph(navController, ilRepository, ilHost) } composable(HomeDestination.Info.route, DemoPageView("home", listOf("app", "pillarbox", "information"))) { @@ -117,6 +145,7 @@ fun MainNavigation() { } composable(route = NavigationRoutes.searchHome, DemoPageView("home", listOf("app", "pillarbox", "search"))) { + val ilRepository = PlayerModule.createIlRepository(context) val viewModel: SearchViewModel = viewModel(factory = SearchViewModel.Factory(ilRepository)) SearchView(searchViewModel = viewModel) { val item = DemoItem( @@ -133,6 +162,65 @@ fun MainNavigation() { } } +@Composable +private fun ListsMenu( + currentServer: URL, + onServerSelected: (server: URL) -> Unit +) { + var isMenuVisible by remember { mutableStateOf(false) } + + IconButton(onClick = { isMenuVisible = true }) { + Icon( + imageVector = Icons.Default.Settings, + contentDescription = stringResource(R.string.server) + ) + } + + DropdownMenu( + expanded = isMenuVisible, + onDismissRequest = { isMenuVisible = false }, + offset = DpOffset( + x = MaterialTheme.paddings.small, + y = 0.dp, + ), + ) { + val currentServerUrl = currentServer.toString() + val servers = mapOf( + stringResource(R.string.production) to IlHost.PROD.toString(), + stringResource(R.string.stage) to IlHost.STAGE.toString(), + stringResource(R.string.test) to IlHost.TEST.toString() + ) + + Text( + text = stringResource(R.string.server), + modifier = Modifier + .padding(MenuDefaults.DropdownMenuItemContentPadding) + .align(Alignment.CenterHorizontally), + style = MaterialTheme.typography.labelMedium + ) + + servers.forEach { (name, url) -> + DropdownMenuItem( + text = { Text(text = name) }, + onClick = { + onServerSelected(URL(url)) + isMenuVisible = false + }, + trailingIcon = if (currentServerUrl == url) { + { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null + ) + } + } else { + null + } + ) + } + } +} + @Composable private fun DemoBottomNavigation(navController: NavController, currentDestination: NavDestination?) { NavigationBar { @@ -163,6 +251,28 @@ private fun DemoBottomNavigation(navController: NavController, currentDestinatio } } +@Composable +@Preview(showBackground = true) +private fun ListsMenuPreview() { + PillarboxTheme { + ListsMenu( + currentServer = IlHost.PROD, + onServerSelected = {} + ) + } +} + +@Composable +@Preview(showBackground = true) +private fun DemoBottomNavigationPreview() { + PillarboxTheme { + DemoBottomNavigation( + navController = rememberNavController(), + currentDestination = null + ) + } +} + private fun NavDestination.getLabelResId(): Int { val routes = hierarchy.map { it.route } val navItem: HomeDestination? = bottomNavItems.firstOrNull { it.route in routes } diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/integrationLayer/ContentListsView.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/integrationLayer/ContentListsView.kt index 9597e30aa..bd72c0376 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/integrationLayer/ContentListsView.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/integrationLayer/ContentListsView.kt @@ -36,13 +36,14 @@ import ch.srgssr.pillarbox.demo.ui.composable import ch.srgssr.pillarbox.demo.ui.player.SimplePlayerActivity import ch.srgssr.pillarbox.demo.ui.theme.PillarboxTheme import ch.srgssr.pillarbox.demo.ui.theme.paddings +import java.net.URL private val defaultListsLevels = listOf("app", "pillarbox", "lists") /** * Build Navigation for integration layer list view */ -fun NavGraphBuilder.listNavGraph(navController: NavController, ilRepository: ILRepository) { +fun NavGraphBuilder.listNavGraph(navController: NavController, ilRepository: ILRepository, ilHost: URL) { val contentClick = { contentList: ContentList, content: Content -> when (content) { is Content.Show -> { @@ -65,7 +66,7 @@ fun NavGraphBuilder.listNavGraph(navController: NavController, ilRepository: ILR is Content.Media -> { val item = DemoItem(title = content.title, uri = content.urn) - SimplePlayerActivity.startActivity(navController.context, item) + SimplePlayerActivity.startActivity(navController.context, item, ilHost) } is Content.Channel -> { diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/SimplePlayerActivity.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/SimplePlayerActivity.kt index f9faa4686..8d2d99f9f 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/SimplePlayerActivity.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/SimplePlayerActivity.kt @@ -15,7 +15,6 @@ import android.os.Bundle import android.os.IBinder import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.activity.viewModels import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme @@ -24,10 +23,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier import androidx.lifecycle.Lifecycle +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.media3.common.Player import ch.srgssr.pillarbox.analytics.SRGAnalytics +import ch.srgssr.pillarbox.core.business.integrationlayer.service.IlHost import ch.srgssr.pillarbox.demo.DemoPageView import ch.srgssr.pillarbox.demo.service.DemoPlaybackService import ch.srgssr.pillarbox.demo.shared.data.DemoItem @@ -37,6 +38,7 @@ import ch.srgssr.pillarbox.demo.ui.theme.PillarboxTheme import ch.srgssr.pillarbox.player.service.PlaybackService import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import java.net.URL /** * Simple player activity that can handle picture in picture. @@ -49,7 +51,7 @@ import kotlinx.coroutines.launch */ class SimplePlayerActivity : ComponentActivity(), ServiceConnection { - private val playerViewModel: SimplePlayerViewModel by viewModels() + private lateinit var playerViewModel: SimplePlayerViewModel private var layoutStyle: Int = LAYOUT_PLAYLIST private fun readIntent(intent: Intent) { @@ -66,6 +68,8 @@ class SimplePlayerActivity : ComponentActivity(), ServiceConnection { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val ilHost = (intent.extras?.getSerializable(ARG_IL_HOST) as URL?) ?: IlHost.DEFAULT + playerViewModel = ViewModelProvider(this, factory = SimplePlayerViewModel.Factory(application, ilHost))[SimplePlayerViewModel::class.java] readIntent(intent) lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.RESUMED) { @@ -185,25 +189,27 @@ class SimplePlayerActivity : ComponentActivity(), ServiceConnection { companion object { private const val ARG_PLAYLIST = "ARG_PLAYLIST" private const val ARG_LAYOUT = "ARG_LAYOUT" + private const val ARG_IL_HOST = "ARG_IL_HOST" private const val LAYOUT_SIMPLE = 1 private const val LAYOUT_PLAYLIST = 0 /** * Start activity [SimplePlayerActivity] with [playlist] */ - fun startActivity(context: Context, playlist: Playlist) { + fun startActivity(context: Context, playlist: Playlist, ilHost: URL = IlHost.DEFAULT) { val layoutStyle: Int = if (playlist.items.isEmpty() || playlist.items.size > 1) LAYOUT_PLAYLIST else LAYOUT_SIMPLE val intent = Intent(context, SimplePlayerActivity::class.java) intent.putExtra(ARG_PLAYLIST, playlist) intent.putExtra(ARG_LAYOUT, layoutStyle) + intent.putExtra(ARG_IL_HOST, ilHost) context.startActivity(intent) } /** - * Start activity [SimplePlayerActivity] with [demoItem] + * Start activity [SimplePlayerActivity] with DemoItem. */ - fun startActivity(context: Context, item: DemoItem) { - startActivity(context, Playlist("UniqueItem", listOf(item))) + fun startActivity(context: Context, item: DemoItem, ilHost: URL = IlHost.DEFAULT) { + startActivity(context, Playlist("UniqueItem", listOf(item)), ilHost) } } } diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/SimplePlayerViewModel.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/SimplePlayerViewModel.kt index 51fc3db82..4233c8e76 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/SimplePlayerViewModel.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/SimplePlayerViewModel.kt @@ -8,6 +8,8 @@ import android.app.Application import android.util.Log import android.util.Rational import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider import androidx.media3.common.C import androidx.media3.common.MediaMetadata import androidx.media3.common.PlaybackException @@ -20,15 +22,16 @@ import ch.srgssr.pillarbox.demo.shared.di.PlayerModule import ch.srgssr.pillarbox.player.extension.toRational import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import java.net.URL /** * Simple player view model than handle a PillarboxPlayer [player] */ -class SimplePlayerViewModel(application: Application) : AndroidViewModel(application), Player.Listener { +class SimplePlayerViewModel(application: Application, ilHost: URL) : AndroidViewModel(application), Player.Listener { /** * Player as PillarboxPlayer */ - val player = PlayerModule.provideDefaultPlayer(application) + val player = PlayerModule.provideDefaultPlayer(application, ilHost) private val _pauseOnBackground = MutableStateFlow(true) private val _displayNotification = MutableStateFlow(false) @@ -176,4 +179,13 @@ class SimplePlayerViewModel(application: Application) : AndroidViewModel(applica companion object { private const val TAG = "PillarboxDemo" } + + /** + * Factory to create [SimplePlayerViewModel]. + */ + class Factory(private val application: Application, private val ilHost: URL) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return SimplePlayerViewModel(application, ilHost) as T + } + } } diff --git a/pillarbox-demo/src/main/res/values/strings.xml b/pillarbox-demo/src/main/res/values/strings.xml index c8b1f516d..50a64b1b2 100644 --- a/pillarbox-demo/src/main/res/values/strings.xml +++ b/pillarbox-demo/src/main/res/values/strings.xml @@ -21,4 +21,8 @@ Clear License URL Play + Server + Production + Stage + Test