mirror of
https://github.com/emptyynes/EmptyPlayer.git
synced 2025-04-30 22:33:09 +03:00
tracks virtual queue implemented
This commit is contained in:
parent
eb574f3115
commit
1956170e33
6 changed files with 128 additions and 30 deletions
.idea
app/src/main
|
@ -1,4 +1,3 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:tools="http://schemas.android.com/tools"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||||
tools:ignore="ScopedStorage" />
|
tools:ignore="ScopedStorage" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
@ -22,6 +27,9 @@
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<service android:name=".PlayerService" />
|
<service
|
||||||
|
android:name=".PlayerService"
|
||||||
|
android:foregroundServiceType="mediaPlayback"
|
||||||
|
android:launchMode="singleTop" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
|
@ -6,6 +6,7 @@ import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
import android.util.Log
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
|
@ -19,7 +20,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
@ -34,6 +35,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import usr.empty.player.ui.theme.PlayerTheme
|
import usr.empty.player.ui.theme.PlayerTheme
|
||||||
|
@ -45,6 +47,7 @@ inline fun <T> nullifyException(block: () -> T) = try {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -56,7 +59,10 @@ class MainActivity : ComponentActivity() {
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
startService(Intent(this, PlayerService::class.java))
|
startForegroundService(Intent(this, PlayerService::class.java).apply {
|
||||||
|
putExtra("check", "empty")
|
||||||
|
putExtra("type", "start")
|
||||||
|
})
|
||||||
|
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
|
@ -74,6 +80,7 @@ fun uriToPath(uri: Uri): String {
|
||||||
return "/storage/" + result.replace("primary:", "/storage/emulated/0/")
|
return "/storage/" + result.replace("primary:", "/storage/emulated/0/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
@Composable
|
@Composable
|
||||||
fun MainLayout(modifier: Modifier = Modifier) {
|
fun MainLayout(modifier: Modifier = Modifier) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
@ -134,20 +141,24 @@ fun MainLayout(modifier: Modifier = Modifier) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
@Composable
|
@Composable
|
||||||
fun NotaList(notas: List<NotaDescriptor>, modifier: Modifier = Modifier) {
|
fun NotaList(notas: List<NotaDescriptor>, modifier: Modifier = Modifier, queueId: Int = notas.hashCode()) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
items(notas) { nota ->
|
itemsIndexed(notas) { index, nota ->
|
||||||
Column(modifier = Modifier
|
Column(modifier = Modifier
|
||||||
.fillParentMaxWidth()
|
.fillParentMaxWidth()
|
||||||
.clickable {
|
.clickable {
|
||||||
|
Log.d("meow", "queueId: $queueId")
|
||||||
context.startService(Intent(context, PlayerService::class.java).apply {
|
context.startService(Intent(context, PlayerService::class.java).apply {
|
||||||
putExtra("check", "empty")
|
putExtra("check", "empty")
|
||||||
putExtra("type", "nota")
|
putExtra("type", "queue")
|
||||||
putExtra("nota", Json.encodeToString(nota))
|
putExtra("queueId", queueId)
|
||||||
|
putExtra("queue", Json.encodeToString(notas))
|
||||||
|
putExtra("start", index)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
|
@ -163,6 +174,7 @@ fun NotaList(notas: List<NotaDescriptor>, modifier: Modifier = Modifier) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun Preview() {
|
fun Preview() {
|
||||||
|
|
9
app/src/main/java/usr/empty/player/NotaQueue.kt
Normal file
9
app/src/main/java/usr/empty/player/NotaQueue.kt
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package usr.empty.player
|
||||||
|
|
||||||
|
import java.util.LinkedList
|
||||||
|
|
||||||
|
class NotaQueue : LinkedList<Nota>() {
|
||||||
|
var currentId = 0
|
||||||
|
val current
|
||||||
|
get() = if (currentId < size) this[currentId] else null
|
||||||
|
}
|
|
@ -1,28 +1,71 @@
|
||||||
package usr.empty.player
|
package usr.empty.player
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
|
import androidx.media3.common.Player
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
import androidx.media3.session.MediaSession
|
import androidx.media3.session.MediaSession
|
||||||
import androidx.media3.session.MediaSessionService
|
import androidx.media3.session.MediaSessionService
|
||||||
|
import androidx.media3.session.MediaStyleNotificationHelper
|
||||||
|
import androidx.media3.ui.PlayerNotificationManager
|
||||||
import com.google.common.util.concurrent.Futures
|
import com.google.common.util.concurrent.Futures
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.util.concurrent.ConcurrentLinkedDeque
|
|
||||||
|
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
class PlayerService : MediaSessionService() {
|
class PlayerService : MediaSessionService() {
|
||||||
private lateinit var mediaSession: MediaSession
|
private lateinit var mediaSession: MediaSession
|
||||||
lateinit var player: ExoPlayer
|
lateinit var player: ExoPlayer
|
||||||
private var virtualQueue = ConcurrentLinkedDeque<Nota>()
|
private lateinit var playerNotificationManager: PlayerNotificationManager
|
||||||
|
private val notificationId = 1004
|
||||||
|
private var virtualQueue = NotaQueue()
|
||||||
|
private var queueId = -1
|
||||||
|
|
||||||
|
@SuppressLint("ForegroundServiceType")
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
with(ExoPlayer.Builder(this).build()) {
|
player = ExoPlayer.Builder(this).build()
|
||||||
player = this
|
player.addListener(object : Player.Listener {
|
||||||
mediaSession = MediaSession.Builder(this@PlayerService, this).setCallback(MediaSessionCallback()).build()
|
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||||
}
|
if (!isPlaying) {
|
||||||
|
if (player.playbackState == Player.STATE_ENDED) next()
|
||||||
|
}
|
||||||
|
super.onIsPlayingChanged(isPlaying)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
mediaSession = MediaSession.Builder(this, player).setCallback(MediaSessionCallback()).build()
|
||||||
|
playerNotificationManager = PlayerNotificationManager.Builder(this, notificationId, "emptyynes")
|
||||||
|
.setMediaDescriptionAdapter(object : PlayerNotificationManager.MediaDescriptionAdapter {
|
||||||
|
override fun getCurrentContentTitle(player: Player) = "title?"
|
||||||
|
|
||||||
|
override fun createCurrentContentIntent(player: Player) = null
|
||||||
|
|
||||||
|
override fun getCurrentContentText(player: Player) = "context?"
|
||||||
|
|
||||||
|
override fun getCurrentLargeIcon(player: Player, callback: PlayerNotificationManager.BitmapCallback) = null
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
.apply {
|
||||||
|
setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
|
setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
|
setPlayer(player)
|
||||||
|
setMediaSessionToken(mediaSession.platformToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
notificationManager.createNotificationChannel(NotificationChannel("emptyynes", "Channel", NotificationManager.IMPORTANCE_LOW))
|
||||||
|
|
||||||
|
val nBuilder = NotificationCompat.Builder(this, "emptyynes").setStyle(MediaStyleNotificationHelper.MediaStyle(mediaSession))
|
||||||
|
startForeground(notificationId, nBuilder.build(), FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
@ -31,24 +74,62 @@ class PlayerService : MediaSessionService() {
|
||||||
if (getStringExtra("check") != "empty") return@run
|
if (getStringExtra("check") != "empty") return@run
|
||||||
handleCommand(getStringExtra("type")!!, this)
|
handleCommand(getStringExtra("type")!!, this)
|
||||||
}
|
}
|
||||||
return super.onStartCommand(intent, flags, startId)
|
super.onStartCommand(intent, flags, startId)
|
||||||
|
return START_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleCommand(type: String, intent: Intent) {
|
private fun handleCommand(type: String, intent: Intent) {
|
||||||
when (type) {
|
when (type) {
|
||||||
|
"start" -> {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
"queue" -> {
|
||||||
|
Log.d("meow", "queue!")
|
||||||
|
intent.getIntExtra("queueId", -1).let { newQueueId ->
|
||||||
|
if (newQueueId == queueId) return@let
|
||||||
|
virtualQueue.clear()
|
||||||
|
Log.d("meow", "queue reset!")
|
||||||
|
virtualQueue.addAll(Json.decodeFromString<List<NotaDescriptor>>(intent.getStringExtra("queue")!!).map { Nota(it) })
|
||||||
|
queueId = newQueueId
|
||||||
|
}
|
||||||
|
virtualQueue.currentId = intent.getIntExtra("start", -1)
|
||||||
|
Log.d("meow", "currentId: ${virtualQueue.currentId}")
|
||||||
|
virtualQueue.current?.let {
|
||||||
|
it.prepare(player)
|
||||||
|
player.prepare()
|
||||||
|
player.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"next" -> {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
"nota" -> {
|
"nota" -> {
|
||||||
|
virtualQueue.clear()
|
||||||
virtualQueue.add(Nota(Json.decodeFromString<NotaDescriptor>(intent.getStringExtra("nota")!!)))
|
virtualQueue.add(Nota(Json.decodeFromString<NotaDescriptor>(intent.getStringExtra("nota")!!)))
|
||||||
// virtualQueue.first.prepare(player)
|
|
||||||
virtualQueue.last.prepare(player)
|
virtualQueue.last.prepare(player)
|
||||||
player.prepare()
|
player.prepare()
|
||||||
player.play()
|
player.play()
|
||||||
Log.d("nya", virtualQueue.last.mediaSource.toString())
|
Log.d("nya", virtualQueue.last.mediaSource.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
"pause" -> player.pause()
|
"pause" -> player.pause()
|
||||||
"play" -> player.play()
|
"play" -> player.play()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun next() {
|
||||||
|
virtualQueue.currentId++
|
||||||
|
virtualQueue.current?.let {
|
||||||
|
it.prepare(player)
|
||||||
|
player.prepare()
|
||||||
|
player.play()
|
||||||
|
}
|
||||||
|
Log.d("meow", "next!")
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
player.release()
|
player.release()
|
||||||
mediaSession.release()
|
mediaSession.release()
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
package usr.empty.player.ui.theme
|
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
|
|
||||||
val Purple80 = Color(0xFFD0BCFF)
|
|
||||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
|
||||||
val Pink80 = Color(0xFFEFB8C8)
|
|
||||||
|
|
||||||
val Purple40 = Color(0xFF6650a4)
|
|
||||||
val PurpleGrey40 = Color(0xFF625b71)
|
|
||||||
val Pink40 = Color(0xFF7D5260)
|
|
Loading…
Reference in a new issue