mirror of
https://github.com/emptyynes/EmptyPlayer.git
synced 2025-01-22 00:12:27 +03:00
tracks virtual queue implemented
This commit is contained in:
parent
eb574f3115
commit
1956170e33
6 changed files with 128 additions and 30 deletions
|
@ -1,4 +1,3 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<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"?>
|
||||
<manifest xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
<manifest 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"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
|
@ -22,6 +27,9 @@
|
|||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<service android:name=".PlayerService" />
|
||||
<service
|
||||
android:name=".PlayerService"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
android:launchMode="singleTop" />
|
||||
</application>
|
||||
</manifest>
|
|
@ -6,6 +6,7 @@ import android.net.Uri
|
|||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
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.padding
|
||||
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.ButtonDefaults
|
||||
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.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import usr.empty.player.ui.theme.PlayerTheme
|
||||
|
@ -45,6 +47,7 @@ inline fun <T> nullifyException(block: () -> T) = try {
|
|||
null
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -56,7 +59,10 @@ class MainActivity : ComponentActivity() {
|
|||
startActivity(intent)
|
||||
}
|
||||
|
||||
startService(Intent(this, PlayerService::class.java))
|
||||
startForegroundService(Intent(this, PlayerService::class.java).apply {
|
||||
putExtra("check", "empty")
|
||||
putExtra("type", "start")
|
||||
})
|
||||
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
|
@ -74,6 +80,7 @@ fun uriToPath(uri: Uri): String {
|
|||
return "/storage/" + result.replace("primary:", "/storage/emulated/0/")
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
@Composable
|
||||
fun MainLayout(modifier: Modifier = Modifier) {
|
||||
val context = LocalContext.current
|
||||
|
@ -134,20 +141,24 @@ fun MainLayout(modifier: Modifier = Modifier) {
|
|||
}
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
@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
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
) {
|
||||
items(notas) { nota ->
|
||||
itemsIndexed(notas) { index, nota ->
|
||||
Column(modifier = Modifier
|
||||
.fillParentMaxWidth()
|
||||
.clickable {
|
||||
Log.d("meow", "queueId: $queueId")
|
||||
context.startService(Intent(context, PlayerService::class.java).apply {
|
||||
putExtra("check", "empty")
|
||||
putExtra("type", "nota")
|
||||
putExtra("nota", Json.encodeToString(nota))
|
||||
putExtra("type", "queue")
|
||||
putExtra("queueId", queueId)
|
||||
putExtra("queue", Json.encodeToString(notas))
|
||||
putExtra("start", index)
|
||||
})
|
||||
}
|
||||
.padding(4.dp)
|
||||
|
@ -163,6 +174,7 @@ fun NotaList(notas: List<NotaDescriptor>, modifier: Modifier = Modifier) {
|
|||
}
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
@Preview
|
||||
@Composable
|
||||
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
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.session.MediaSession
|
||||
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.ListenableFuture
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.util.concurrent.ConcurrentLinkedDeque
|
||||
|
||||
|
||||
@UnstableApi
|
||||
class PlayerService : MediaSessionService() {
|
||||
private lateinit var mediaSession: MediaSession
|
||||
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() {
|
||||
super.onCreate()
|
||||
with(ExoPlayer.Builder(this).build()) {
|
||||
player = this
|
||||
mediaSession = MediaSession.Builder(this@PlayerService, this).setCallback(MediaSessionCallback()).build()
|
||||
}
|
||||
player = ExoPlayer.Builder(this).build()
|
||||
player.addListener(object : Player.Listener {
|
||||
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 {
|
||||
|
@ -31,24 +74,62 @@ class PlayerService : MediaSessionService() {
|
|||
if (getStringExtra("check") != "empty") return@run
|
||||
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) {
|
||||
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" -> {
|
||||
virtualQueue.clear()
|
||||
virtualQueue.add(Nota(Json.decodeFromString<NotaDescriptor>(intent.getStringExtra("nota")!!)))
|
||||
// virtualQueue.first.prepare(player)
|
||||
virtualQueue.last.prepare(player)
|
||||
player.prepare()
|
||||
player.play()
|
||||
Log.d("nya", virtualQueue.last.mediaSource.toString())
|
||||
}
|
||||
|
||||
"pause" -> player.pause()
|
||||
"play" -> player.play()
|
||||
}
|
||||
}
|
||||
|
||||
fun next() {
|
||||
virtualQueue.currentId++
|
||||
virtualQueue.current?.let {
|
||||
it.prepare(player)
|
||||
player.prepare()
|
||||
player.play()
|
||||
}
|
||||
Log.d("meow", "next!")
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
player.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