media3 playback related code, not working though
This commit is contained in:
		@ -23,6 +23,15 @@
 | 
				
			|||||||
                <category android:name="android.intent.category.LAUNCHER" />
 | 
					                <category android:name="android.intent.category.LAUNCHER" />
 | 
				
			||||||
            </intent-filter>
 | 
					            </intent-filter>
 | 
				
			||||||
        </activity>
 | 
					        </activity>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <service
 | 
				
			||||||
 | 
					            android:name=".service.PlaybackService"
 | 
				
			||||||
 | 
					            android:exported="true">
 | 
				
			||||||
 | 
					            <intent-filter>
 | 
				
			||||||
 | 
					                <action android:name="androidx.media3.session.MediaSessionService"/>
 | 
				
			||||||
 | 
					            </intent-filter>
 | 
				
			||||||
 | 
					        </service>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    </application>
 | 
					    </application>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</manifest>
 | 
					</manifest>
 | 
				
			||||||
@ -1,5 +1,6 @@
 | 
				
			|||||||
package net.blumia.pcmdroid
 | 
					package net.blumia.pcmdroid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.content.ComponentName
 | 
				
			||||||
import android.os.Bundle
 | 
					import android.os.Bundle
 | 
				
			||||||
import androidx.activity.ComponentActivity
 | 
					import androidx.activity.ComponentActivity
 | 
				
			||||||
import androidx.activity.compose.setContent
 | 
					import androidx.activity.compose.setContent
 | 
				
			||||||
@ -9,12 +10,25 @@ import androidx.compose.material.Surface
 | 
				
			|||||||
import androidx.compose.material.Text
 | 
					import androidx.compose.material.Text
 | 
				
			||||||
import androidx.compose.runtime.Composable
 | 
					import androidx.compose.runtime.Composable
 | 
				
			||||||
import androidx.compose.ui.tooling.preview.Preview
 | 
					import androidx.compose.ui.tooling.preview.Preview
 | 
				
			||||||
 | 
					import androidx.media3.common.MediaItem
 | 
				
			||||||
 | 
					import androidx.media3.common.util.UnstableApi
 | 
				
			||||||
 | 
					import androidx.media3.session.LibraryResult
 | 
				
			||||||
 | 
					import androidx.media3.session.MediaBrowser
 | 
				
			||||||
 | 
					import androidx.media3.session.SessionToken
 | 
				
			||||||
 | 
					import com.google.common.util.concurrent.ListenableFuture
 | 
				
			||||||
 | 
					import com.google.common.util.concurrent.MoreExecutors
 | 
				
			||||||
 | 
					import net.blumia.pcmdroid.service.PlaybackService
 | 
				
			||||||
import net.blumia.pcmdroid.ui.NavGraph
 | 
					import net.blumia.pcmdroid.ui.NavGraph
 | 
				
			||||||
import net.blumia.pcmdroid.ui.screen.main.MainPlayer
 | 
					import net.blumia.pcmdroid.ui.screen.main.MainPlayer
 | 
				
			||||||
import net.blumia.pcmdroid.ui.theme.PrivateCloudMusicTheme
 | 
					import net.blumia.pcmdroid.ui.theme.PrivateCloudMusicTheme
 | 
				
			||||||
 | 
					import java.lang.Exception
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class MainActivity : ComponentActivity() {
 | 
					class MainActivity : ComponentActivity() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private lateinit var browserFuture: ListenableFuture<MediaBrowser>
 | 
				
			||||||
 | 
					    private val browser: MediaBrowser?
 | 
				
			||||||
 | 
					        get() = if (browserFuture.isDone) browserFuture.get() else null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private val model: MainViewModel by viewModels {
 | 
					    private val model: MainViewModel by viewModels {
 | 
				
			||||||
        MainViewModelFactory((application as MainApplication).repository)
 | 
					        MainViewModelFactory((application as MainApplication).repository)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -25,9 +39,49 @@ class MainActivity : ComponentActivity() {
 | 
				
			|||||||
            PrivateCloudMusicTheme {
 | 
					            PrivateCloudMusicTheme {
 | 
				
			||||||
                // A surface container using the 'background' color from the theme
 | 
					                // A surface container using the 'background' color from the theme
 | 
				
			||||||
                Surface(color = MaterialTheme.colors.background) {
 | 
					                Surface(color = MaterialTheme.colors.background) {
 | 
				
			||||||
                    NavGraph(model)
 | 
					                    NavGraph(model, browser)
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun onStart() {
 | 
				
			||||||
 | 
					        super.onStart()
 | 
				
			||||||
 | 
					        initializeBrowser()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun onStop() {
 | 
				
			||||||
 | 
					        releaseBrowser()
 | 
				
			||||||
 | 
					        super.onStop()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @androidx.annotation.OptIn(UnstableApi::class)
 | 
				
			||||||
 | 
					    private fun initializeBrowser() {
 | 
				
			||||||
 | 
					        browserFuture =
 | 
				
			||||||
 | 
					            MediaBrowser.Builder(
 | 
				
			||||||
 | 
					                this,
 | 
				
			||||||
 | 
					                SessionToken(this, ComponentName(this, PlaybackService::class.java))
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .buildAsync()
 | 
				
			||||||
 | 
					        browserFuture.addListener({ pushRoot() }, MoreExecutors.directExecutor())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private fun releaseBrowser() {
 | 
				
			||||||
 | 
					        MediaBrowser.releaseFuture(browserFuture)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private fun pushRoot() {
 | 
				
			||||||
 | 
					        // browser can be initialized many times
 | 
				
			||||||
 | 
					        // only push root at the first initialization
 | 
				
			||||||
 | 
					        val browser = this.browser ?: return
 | 
				
			||||||
 | 
					        val rootFuture = browser.getLibraryRoot(/* params= */ null)
 | 
				
			||||||
 | 
					        rootFuture.addListener(
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                val result: LibraryResult<MediaItem> = rootFuture.get()!!
 | 
				
			||||||
 | 
					                val root: MediaItem = result.value!!
 | 
				
			||||||
 | 
					//                pushPathStack(root)
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            MoreExecutors.directExecutor()
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -0,0 +1,61 @@
 | 
				
			|||||||
 | 
					package net.blumia.pcmdroid.service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.app.PendingIntent.FLAG_IMMUTABLE
 | 
				
			||||||
 | 
					import android.app.PendingIntent.FLAG_UPDATE_CURRENT
 | 
				
			||||||
 | 
					import android.content.Intent
 | 
				
			||||||
 | 
					import android.os.Build
 | 
				
			||||||
 | 
					import androidx.core.app.TaskStackBuilder
 | 
				
			||||||
 | 
					import androidx.media3.common.AudioAttributes
 | 
				
			||||||
 | 
					import androidx.media3.exoplayer.ExoPlayer
 | 
				
			||||||
 | 
					import androidx.media3.session.MediaLibraryService
 | 
				
			||||||
 | 
					import androidx.media3.session.MediaSession
 | 
				
			||||||
 | 
					import net.blumia.pcmdroid.MainActivity
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PlaybackService : MediaLibraryService() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private lateinit var player: ExoPlayer
 | 
				
			||||||
 | 
					    private lateinit var mediaLibrarySession: MediaLibrarySession
 | 
				
			||||||
 | 
					    private val librarySessionCallback = CustomMediaLibrarySessionCallback()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private inner class CustomMediaLibrarySessionCallback
 | 
				
			||||||
 | 
					        : MediaLibrarySession.MediaLibrarySessionCallback
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession {
 | 
				
			||||||
 | 
					        return mediaLibrarySession
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun onCreate() {
 | 
				
			||||||
 | 
					        super.onCreate()
 | 
				
			||||||
 | 
					        initializeSessionAndPlayer()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun onDestroy() {
 | 
				
			||||||
 | 
					        player.release()
 | 
				
			||||||
 | 
					        mediaLibrarySession.release()
 | 
				
			||||||
 | 
					        super.onDestroy()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private fun initializeSessionAndPlayer() {
 | 
				
			||||||
 | 
					        player =
 | 
				
			||||||
 | 
					            ExoPlayer.Builder(this)
 | 
				
			||||||
 | 
					                .setAudioAttributes(AudioAttributes.DEFAULT, true)
 | 
				
			||||||
 | 
					                .build()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val mainActivityIntent = Intent(this, MainActivity::class.java)
 | 
				
			||||||
 | 
					        val pendingIntent =
 | 
				
			||||||
 | 
					            TaskStackBuilder.create(this).run {
 | 
				
			||||||
 | 
					                addNextIntent(mainActivityIntent)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                val immutableFlag = if (Build.VERSION.SDK_INT >= 23) FLAG_IMMUTABLE else 0
 | 
				
			||||||
 | 
					                getPendingIntent(0, immutableFlag or FLAG_UPDATE_CURRENT)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        mediaLibrarySession =
 | 
				
			||||||
 | 
					            MediaLibrarySession.Builder(this, player, librarySessionCallback)
 | 
				
			||||||
 | 
					//                .setMediaItemFiller(CustomMediaItemFiller())
 | 
				
			||||||
 | 
					                .setSessionActivity(pendingIntent!!)
 | 
				
			||||||
 | 
					                .build()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -5,6 +5,8 @@ import androidx.compose.runtime.Composable
 | 
				
			|||||||
import androidx.compose.runtime.getValue
 | 
					import androidx.compose.runtime.getValue
 | 
				
			||||||
import androidx.compose.runtime.livedata.observeAsState
 | 
					import androidx.compose.runtime.livedata.observeAsState
 | 
				
			||||||
import androidx.lifecycle.MutableLiveData
 | 
					import androidx.lifecycle.MutableLiveData
 | 
				
			||||||
 | 
					import androidx.media3.common.MediaItem
 | 
				
			||||||
 | 
					import androidx.media3.session.MediaBrowser
 | 
				
			||||||
import androidx.navigation.NavHostController
 | 
					import androidx.navigation.NavHostController
 | 
				
			||||||
import androidx.navigation.NavType
 | 
					import androidx.navigation.NavType
 | 
				
			||||||
import androidx.navigation.compose.NavHost
 | 
					import androidx.navigation.compose.NavHost
 | 
				
			||||||
@ -30,6 +32,7 @@ object MainDestinations {
 | 
				
			|||||||
@Composable
 | 
					@Composable
 | 
				
			||||||
fun NavGraph(
 | 
					fun NavGraph(
 | 
				
			||||||
    viewModel: MainViewModel,
 | 
					    viewModel: MainViewModel,
 | 
				
			||||||
 | 
					    browser: MediaBrowser?,
 | 
				
			||||||
    navController: NavHostController = rememberNavController(),
 | 
					    navController: NavHostController = rememberNavController(),
 | 
				
			||||||
    startDestination: String = MainDestinations.MAIN_ROUTE,
 | 
					    startDestination: String = MainDestinations.MAIN_ROUTE,
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
@ -62,6 +65,16 @@ fun NavGraph(
 | 
				
			|||||||
                onFolderItemClicked = { folder ->
 | 
					                onFolderItemClicked = { folder ->
 | 
				
			||||||
                    viewModel.fetchFolder(folder)
 | 
					                    viewModel.fetchFolder(folder)
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
 | 
					                onSongItemClicked = { song, songs ->
 | 
				
			||||||
 | 
					                    Log.d("vvv", song.url + browser.toString())
 | 
				
			||||||
 | 
					                    val playlist = browser ?: return@MainPlayer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    val item = MediaItem.Builder().setUri(song.url).build()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    playlist.setMediaItem(item)
 | 
				
			||||||
 | 
					                    playlist.prepare()
 | 
				
			||||||
 | 
					                    playlist.play()
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
                onEditServerActionTriggered = { server ->
 | 
					                onEditServerActionTriggered = { server ->
 | 
				
			||||||
                    navController.navigate( "${MainDestinations.EDIT_SERVER_ROUTE}?id=${server.serverId}")
 | 
					                    navController.navigate( "${MainDestinations.EDIT_SERVER_ROUTE}?id=${server.serverId}")
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
 | 
				
			|||||||
@ -29,6 +29,62 @@ import net.blumia.pcmdroid.model.Folder
 | 
				
			|||||||
import net.blumia.pcmdroid.model.Server
 | 
					import net.blumia.pcmdroid.model.Server
 | 
				
			||||||
import net.blumia.pcmdroid.model.Song
 | 
					import net.blumia.pcmdroid.model.Song
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Preview(showBackground = true)
 | 
				
			||||||
 | 
					@OptIn(ExperimentalMaterialApi::class)
 | 
				
			||||||
 | 
					@Composable
 | 
				
			||||||
 | 
					fun FileList(
 | 
				
			||||||
 | 
					    modifier: Modifier = Modifier,
 | 
				
			||||||
 | 
					    currentFolder: Folder? = null,
 | 
				
			||||||
 | 
					    folders: List<Folder> = listOf(),
 | 
				
			||||||
 | 
					    songs: List<Song> = listOf(),
 | 
				
			||||||
 | 
					    onFolderItemClicked: (Folder) -> Unit = {},
 | 
				
			||||||
 | 
					    onSongItemClicked: (Song, List<Song>) -> Unit = { _, _ -> },
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    Column(
 | 
				
			||||||
 | 
					        modifier = modifier
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Breadcrumb(
 | 
				
			||||||
 | 
					            folder = currentFolder,
 | 
				
			||||||
 | 
					            onFolderClicked = onFolderItemClicked,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        folders.forEach { folder ->
 | 
				
			||||||
 | 
					            ListItem(
 | 
				
			||||||
 | 
					                modifier = Modifier.clickable {
 | 
				
			||||||
 | 
					                    onFolderItemClicked(folder)
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                icon = {
 | 
				
			||||||
 | 
					                    Icon(
 | 
				
			||||||
 | 
					                        imageVector = Icons.Filled.FolderOpen,
 | 
				
			||||||
 | 
					                        contentDescription = null,
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                text = {
 | 
				
			||||||
 | 
					                    Text(folder.displayName())
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        songs.forEach { song ->
 | 
				
			||||||
 | 
					            ListItem(
 | 
				
			||||||
 | 
					                modifier = Modifier.clickable {
 | 
				
			||||||
 | 
					                    onSongItemClicked(song, songs)
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                icon = {
 | 
				
			||||||
 | 
					                    Icon(
 | 
				
			||||||
 | 
					                        imageVector = Icons.Filled.MusicNote,
 | 
				
			||||||
 | 
					                        contentDescription = null
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                text = {
 | 
				
			||||||
 | 
					                    Text(song.displayName())
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Preview(showBackground = true)
 | 
					@Preview(showBackground = true)
 | 
				
			||||||
@OptIn(ExperimentalMaterialApi::class)
 | 
					@OptIn(ExperimentalMaterialApi::class)
 | 
				
			||||||
@Composable
 | 
					@Composable
 | 
				
			||||||
@ -82,6 +138,7 @@ fun MainPlayer(
 | 
				
			|||||||
    addServerActionTriggered: () -> Unit = {},
 | 
					    addServerActionTriggered: () -> Unit = {},
 | 
				
			||||||
    onServerItemIconClicked: (Server) -> Unit = {},
 | 
					    onServerItemIconClicked: (Server) -> Unit = {},
 | 
				
			||||||
    onFolderItemClicked: (Folder) -> Unit = {},
 | 
					    onFolderItemClicked: (Folder) -> Unit = {},
 | 
				
			||||||
 | 
					    onSongItemClicked: (Song, List<Song>) -> Unit = { _, _ -> },
 | 
				
			||||||
    onEditServerActionTriggered: (Server) -> Unit = {},
 | 
					    onEditServerActionTriggered: (Server) -> Unit = {},
 | 
				
			||||||
    onDeleteServerActionTriggered: (Server) -> Unit = {},
 | 
					    onDeleteServerActionTriggered: (Server) -> Unit = {},
 | 
				
			||||||
    settingsActionTriggered: () -> Unit = {},
 | 
					    settingsActionTriggered: () -> Unit = {},
 | 
				
			||||||
@ -160,49 +217,13 @@ fun MainPlayer(
 | 
				
			|||||||
            Column(
 | 
					            Column(
 | 
				
			||||||
                modifier = Modifier.fillMaxSize()
 | 
					                modifier = Modifier.fillMaxSize()
 | 
				
			||||||
            ) {
 | 
					            ) {
 | 
				
			||||||
                Column(
 | 
					                FileList(
 | 
				
			||||||
                    modifier = Modifier
 | 
					                    modifier = Modifier
 | 
				
			||||||
                        .weight(1f, fill = true)
 | 
					                        .weight(1f, fill = true)
 | 
				
			||||||
                        .verticalScroll(rememberScrollState())
 | 
					                        .verticalScroll(rememberScrollState()),
 | 
				
			||||||
                ) {
 | 
					                    currentFolder, folders, songs,
 | 
				
			||||||
 | 
					                    onFolderItemClicked, onSongItemClicked
 | 
				
			||||||
                    Breadcrumb(
 | 
					                )
 | 
				
			||||||
                        folder = currentFolder,
 | 
					 | 
				
			||||||
                        onFolderClicked = onFolderItemClicked,
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    folders.forEach { folder ->
 | 
					 | 
				
			||||||
                        ListItem(
 | 
					 | 
				
			||||||
                            modifier = Modifier.clickable {
 | 
					 | 
				
			||||||
                                onFolderItemClicked(folder)
 | 
					 | 
				
			||||||
                            },
 | 
					 | 
				
			||||||
                            icon = {
 | 
					 | 
				
			||||||
                                Icon(
 | 
					 | 
				
			||||||
                                    imageVector = Icons.Filled.FolderOpen,
 | 
					 | 
				
			||||||
                                    contentDescription = null,
 | 
					 | 
				
			||||||
                                )
 | 
					 | 
				
			||||||
                            },
 | 
					 | 
				
			||||||
                            text = {
 | 
					 | 
				
			||||||
                                Text(folder.displayName())
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    songs.forEach { song ->
 | 
					 | 
				
			||||||
                        ListItem(
 | 
					 | 
				
			||||||
                            modifier = Modifier.clickable {  },
 | 
					 | 
				
			||||||
                            icon = {
 | 
					 | 
				
			||||||
                                Icon(
 | 
					 | 
				
			||||||
                                    imageVector = Icons.Filled.MusicNote,
 | 
					 | 
				
			||||||
                                    contentDescription = null
 | 
					 | 
				
			||||||
                                )
 | 
					 | 
				
			||||||
                            },
 | 
					 | 
				
			||||||
                            text = {
 | 
					 | 
				
			||||||
                                Text(song.displayName())
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                NowPlaying()
 | 
					                NowPlaying()
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user