mirror of
https://github.com/emptyynes/EmptyPlayer.git
synced 2025-04-28 21:36:30 +03:00
temporal abyss
This commit is contained in:
parent
6fcb1a7d39
commit
e5fb995c84
16 changed files with 399 additions and 94 deletions
|
@ -67,6 +67,8 @@ dependencies {
|
||||||
implementation(libs.androidx.media3.session)
|
implementation(libs.androidx.media3.session)
|
||||||
implementation(libs.androidx.room.runtime)
|
implementation(libs.androidx.room.runtime)
|
||||||
implementation(libs.com.google.devtools.ksp.gradle.plugin)
|
implementation(libs.com.google.devtools.ksp.gradle.plugin)
|
||||||
|
implementation(libs.kotlinx.coroutines.android)
|
||||||
|
implementation(libs.androidx.room.ktx)
|
||||||
|
|
||||||
ksp(libs.androidx.room.compiler)
|
ksp(libs.androidx.room.compiler)
|
||||||
|
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
package usr.empty.player
|
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instrumented test, which will execute on an Android device.
|
|
||||||
*
|
|
||||||
* See [testing documentation](http://d.android.com/tools/testing).
|
|
||||||
*/
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
class ExampleInstrumentedTest {
|
|
||||||
@Test
|
|
||||||
fun useAppContext() {
|
|
||||||
// Context of the app under test.
|
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
|
||||||
assertEquals("usr.empty.player", appContext.packageName)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -16,6 +16,7 @@
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
|
android:name=".EmptyApplication"
|
||||||
android:theme="@style/Theme.Player">
|
android:theme="@style/Theme.Player">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
|
@ -23,7 +24,6 @@
|
||||||
android:theme="@style/Theme.Player">
|
android:theme="@style/Theme.Player">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
53
app/src/main/java/usr/empty/player/EmptyApplication.kt
Normal file
53
app/src/main/java/usr/empty/player/EmptyApplication.kt
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
package usr.empty.player
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.room.Room
|
||||||
|
import usr.empty.player.database.AppDatabase
|
||||||
|
import usr.empty.player.items.AlbumDescriptor
|
||||||
|
import usr.empty.player.items.ArtistDescriptor
|
||||||
|
import usr.empty.player.items.NotaDescriptor
|
||||||
|
|
||||||
|
|
||||||
|
class EmptyApplication : Application() {
|
||||||
|
companion object {
|
||||||
|
private var _appInstance: EmptyApplication? = null
|
||||||
|
|
||||||
|
val appInstance: EmptyApplication
|
||||||
|
get() = _appInstance!!
|
||||||
|
}
|
||||||
|
|
||||||
|
lateinit var database: AppDatabase
|
||||||
|
|
||||||
|
val allTracks = ArrayList<NotaDescriptor>()
|
||||||
|
|
||||||
|
val allAlbums = ArrayList<AlbumDescriptor>()
|
||||||
|
|
||||||
|
val allArtists = HashMap<String, ArtistDescriptor>()
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
database = Room.databaseBuilder(
|
||||||
|
applicationContext, AppDatabase::class.java, "local"
|
||||||
|
).allowMainThreadQueries().build()
|
||||||
|
database.artistDao().getAllArtists().forEach { allArtists[it.name ?: "unknown"] = ArtistDescriptor.fromArtist(it) }
|
||||||
|
database.albumDao().getAllAlbums().forEach { album ->
|
||||||
|
AlbumDescriptor.fromAlbum(album, database).let {
|
||||||
|
allAlbums.add(it)
|
||||||
|
allArtists[it.artistName]?.albumList?.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
database.trackDao().getAll().forEach { track ->
|
||||||
|
NotaDescriptor.fromTrack(track, database).let {
|
||||||
|
allTracks.add(it)
|
||||||
|
allArtists[it.artist]?.trackList?.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_appInstance = this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addNewTrack(nota: NotaDescriptor) {
|
||||||
|
database.addNewTrack(nota)
|
||||||
|
allArtists[nota.artist]?.trackList?.add(nota)
|
||||||
|
allTracks.add(nota)
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,36 +12,52 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Slider
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.toMutableStateList
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
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 androidx.media3.common.util.UnstableApi
|
||||||
import androidx.room.Room
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import usr.empty.player.database.AppDatabase
|
import usr.empty.player.items.NotaDescriptor
|
||||||
import usr.empty.player.database.Track
|
|
||||||
import usr.empty.player.ui.theme.PlayerTheme
|
import usr.empty.player.ui.theme.PlayerTheme
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
|
||||||
inline fun <T> nullifyException(block: () -> T) = try {
|
inline fun <T> nullifyException(block: () -> T) = try {
|
||||||
|
@ -55,12 +71,6 @@ class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
// Log.d("meow", "${filesDir.toPath().resolve("all_tracks.data")}")
|
|
||||||
val db = Room.databaseBuilder(
|
|
||||||
applicationContext, AppDatabase::class.java, "local"
|
|
||||||
).allowMainThreadQueries().build()
|
|
||||||
Log.d("meow", "${db.trackDao().getAll()}")
|
|
||||||
|
|
||||||
if (!Environment.isExternalStorageManager()) {
|
if (!Environment.isExternalStorageManager()) {
|
||||||
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
|
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
|
||||||
val uri = Uri.fromParts("package", packageName, null)
|
val uri = Uri.fromParts("package", packageName, null)
|
||||||
|
@ -93,7 +103,12 @@ fun uriToPath(uri: Uri): String {
|
||||||
@Composable
|
@Composable
|
||||||
fun MainLayout(modifier: Modifier = Modifier) {
|
fun MainLayout(modifier: Modifier = Modifier) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val notas = remember { mutableStateListOf<NotaDescriptor>() }
|
val queueId = remember { mutableIntStateOf(-1) }
|
||||||
|
val notas = remember { EmptyApplication.appInstance.allTracks.toMutableStateList() }
|
||||||
|
val isPlaying =
|
||||||
|
remember { mutableStateOf(if (PlayerService.isServiceRunning) PlayerService.serviceInstance.player.isPlaying else false) }
|
||||||
|
val currentNota =
|
||||||
|
remember { mutableStateOf(if (PlayerService.isServiceRunning) PlayerService.serviceInstance.currentTrack?.descriptor else null) }
|
||||||
|
|
||||||
val pickAudioLauncher = rememberLauncherForActivityResult(
|
val pickAudioLauncher = rememberLauncherForActivityResult(
|
||||||
ActivityResultContracts.GetContent()
|
ActivityResultContracts.GetContent()
|
||||||
|
@ -101,26 +116,21 @@ fun MainLayout(modifier: Modifier = Modifier) {
|
||||||
audioUri?.run {
|
audioUri?.run {
|
||||||
uriToPath(this).let {
|
uriToPath(this).let {
|
||||||
MediaMetadataRetriever().apply {
|
MediaMetadataRetriever().apply {
|
||||||
setDataSource(it) // notas.add(NotaDescriptor(name = nullifyException {
|
setDataSource(it)
|
||||||
// extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE)
|
val nd = NotaDescriptor(name = nullifyException {
|
||||||
// } ?: it.split('/').last().split('.').first(), artist = nullifyException {
|
|
||||||
// extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST)
|
|
||||||
// } ?: "Unknown Artist", sourceType = NotaDescriptor.Source.LOCAL, source = it))
|
|
||||||
val db = Room.databaseBuilder(
|
|
||||||
context.applicationContext, AppDatabase::class.java, "local"
|
|
||||||
).allowMainThreadQueries().build()
|
|
||||||
db.trackDao().insertTrack(Track(title = nullifyException {
|
|
||||||
extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE)
|
extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE)
|
||||||
} ?: it.split('/').last().split('.').first(),
|
} ?: it.split('/').last().split('.').first(), artist = nullifyException {
|
||||||
artistId = 0,
|
extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST)
|
||||||
albumId = null,
|
} ?: "Unknown Artist", sourceType = NotaDescriptor.Source.LOCAL, source = it)
|
||||||
sourceType = NotaDescriptor.Source.LOCAL,
|
notas.add(nd)
|
||||||
source = it))
|
queueId.intValue--
|
||||||
|
EmptyApplication.appInstance.addNewTrack(nd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Column(modifier.fillMaxSize()) {
|
Column(modifier.fillMaxSize()) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.Absolute.SpaceEvenly,
|
horizontalArrangement = Arrangement.Absolute.SpaceEvenly,
|
||||||
|
@ -133,7 +143,6 @@ fun MainLayout(modifier: Modifier = Modifier) {
|
||||||
}) {
|
}) {
|
||||||
Text("add track")
|
Text("add track")
|
||||||
}
|
}
|
||||||
// VerticalDivider()
|
|
||||||
Button(shape = RectangleShape, colors = ButtonDefaults.buttonColors(
|
Button(shape = RectangleShape, colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = MaterialTheme.colorScheme.secondaryContainer, contentColor = MaterialTheme.colorScheme.secondary
|
containerColor = MaterialTheme.colorScheme.secondaryContainer, contentColor = MaterialTheme.colorScheme.secondary
|
||||||
), modifier = Modifier, onClick = {
|
), modifier = Modifier, onClick = {
|
||||||
|
@ -155,19 +164,32 @@ fun MainLayout(modifier: Modifier = Modifier) {
|
||||||
Text("| play >")
|
Text("| play >")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NotaList(notas)
|
NotaList(notas, modifier = Modifier.weight(1f), queueId = queueId.intValue, playState = isPlaying, currentNota)
|
||||||
|
if (isPlaying.value) Player(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(bottom = 20.dp)
|
||||||
|
.border(1.dp, color = MaterialTheme.colorScheme.primary, shape = RoundedCornerShape(25))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
@Composable
|
@Composable
|
||||||
fun NotaList(notas: List<NotaDescriptor>, modifier: Modifier = Modifier, queueId: Int = notas.hashCode()) {
|
fun NotaList(
|
||||||
|
notas: List<NotaDescriptor>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
queueId: Int = notas.hashCode(),
|
||||||
|
playState: MutableState<Boolean>,
|
||||||
|
currentNota: MutableState<NotaDescriptor?>
|
||||||
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = modifier,
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
itemsIndexed(notas) { index, nota ->
|
itemsIndexed(notas) { index, nota ->
|
||||||
Column(modifier = Modifier
|
val localModifier =
|
||||||
|
if (currentNota.value == nota) Modifier.background(MaterialTheme.colorScheme.secondaryContainer) else Modifier
|
||||||
|
Column(modifier = localModifier
|
||||||
.fillParentMaxWidth()
|
.fillParentMaxWidth()
|
||||||
.clickable {
|
.clickable {
|
||||||
Log.d("meow", "queueId: $queueId")
|
Log.d("meow", "queueId: $queueId")
|
||||||
|
@ -177,6 +199,7 @@ fun NotaList(notas: List<NotaDescriptor>, modifier: Modifier = Modifier, queueId
|
||||||
putExtra("queueId", queueId)
|
putExtra("queueId", queueId)
|
||||||
putExtra("queue", Json.encodeToString(notas))
|
putExtra("queue", Json.encodeToString(notas))
|
||||||
putExtra("start", index)
|
putExtra("start", index)
|
||||||
|
playState.value = true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
|
@ -192,14 +215,86 @@ fun NotaList(notas: List<NotaDescriptor>, modifier: Modifier = Modifier, queueId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun minMax(vMin: Float, x: Float, vMax: Float) = max(min(vMax, x), vMin)
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
@Preview
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun Preview() {
|
fun Player(modifier: Modifier = Modifier) {
|
||||||
NotaList(
|
val sliderPosition = remember { mutableFloatStateOf(0.0f) }
|
||||||
listOf(
|
val sliderMovingAllow = remember { mutableStateOf(true) }
|
||||||
NotaDescriptor(name = "hello", artist = "there", sourceType = NotaDescriptor.Source.LOCAL, source = ""),
|
val service = PlayerService.serviceInstance
|
||||||
NotaDescriptor(name = "preview", artist = "me", sourceType = NotaDescriptor.Source.LOCAL, source = ""),
|
LaunchedEffect(Unit) {
|
||||||
)
|
while (true) {
|
||||||
)
|
if (service.player.isPlaying and sliderMovingAllow.value) {
|
||||||
|
sliderPosition.floatValue = minMax(0.0f, service.player.currentPosition.toFloat() / service.player.duration.toFloat(), 1.0f)
|
||||||
|
delay(service.player.duration / 2000)
|
||||||
|
Log.d("meow", "${service.player.duration / 2000}")
|
||||||
|
} else delay(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(1.dp)
|
||||||
|
.then(modifier)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
) { // Image(
|
||||||
|
// imageVector = Icons.Filled.PlayArrow,
|
||||||
|
// contentDescription = "play",
|
||||||
|
// colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary),
|
||||||
|
// modifier = Modifier
|
||||||
|
// .size(50.dp)
|
||||||
|
// .align(Alignment.CenterVertically)
|
||||||
|
// )
|
||||||
|
Column(modifier = Modifier.align(Alignment.CenterVertically)) {
|
||||||
|
val nota = service.currentTrack?.descriptor ?: NotaDescriptor(
|
||||||
|
"unknown track", "unknown artist", NotaDescriptor.Source.LOCAL, ""
|
||||||
|
)
|
||||||
|
Text(nota.name, style = MaterialTheme.typography.titleLarge)
|
||||||
|
Text(nota.artist, style = MaterialTheme.typography.titleSmall)
|
||||||
|
Slider(value = sliderPosition.floatValue, thumb = {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.clip(RoundedCornerShape(2.dp))
|
||||||
|
.width(3.dp)
|
||||||
|
.height(18.dp)
|
||||||
|
.background(MaterialTheme.colorScheme.tertiary)
|
||||||
|
)
|
||||||
|
}, track = {
|
||||||
|
Row(Modifier.clip(RoundedCornerShape(1.dp))) {
|
||||||
|
if (sliderPosition.floatValue != 0.0f) Box(
|
||||||
|
Modifier
|
||||||
|
.weight(sliderPosition.floatValue)
|
||||||
|
.height(2.dp)
|
||||||
|
.background(
|
||||||
|
Brush.linearGradient(
|
||||||
|
listOf(
|
||||||
|
MaterialTheme.colorScheme.secondary, MaterialTheme.colorScheme.tertiary
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (sliderPosition.floatValue != 1.0f) Box(
|
||||||
|
Modifier
|
||||||
|
.weight(1 - sliderPosition.floatValue)
|
||||||
|
.height(2.dp)
|
||||||
|
.background(
|
||||||
|
Brush.linearGradient(
|
||||||
|
listOf(
|
||||||
|
MaterialTheme.colorScheme.tertiaryContainer, MaterialTheme.colorScheme.secondaryContainer
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, onValueChange = {
|
||||||
|
sliderPosition.floatValue = it
|
||||||
|
sliderMovingAllow.value = false
|
||||||
|
}, onValueChangeFinished = {
|
||||||
|
service.player.seekTo((service.player.duration.toFloat() * sliderPosition.floatValue).toLong())
|
||||||
|
sliderMovingAllow.value = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -2,6 +2,7 @@ package usr.empty.player
|
||||||
|
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
|
import usr.empty.player.items.NotaDescriptor
|
||||||
|
|
||||||
|
|
||||||
class Nota(val descriptor: NotaDescriptor) {
|
class Nota(val descriptor: NotaDescriptor) {
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
package usr.empty.player
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class NotaDescriptor(
|
|
||||||
val name: String,
|
|
||||||
val artist: String,
|
|
||||||
val sourceType: Source,
|
|
||||||
val source: String,
|
|
||||||
) {
|
|
||||||
enum class Source {
|
|
||||||
LOCAL
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -18,21 +18,44 @@ import androidx.media3.session.MediaStyleNotificationHelper
|
||||||
import androidx.media3.ui.PlayerNotificationManager
|
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.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import usr.empty.player.items.NotaDescriptor
|
||||||
|
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
class PlayerService : MediaSessionService() {
|
class PlayerService : MediaSessionService() {
|
||||||
|
companion object {
|
||||||
|
private var _serviceInstance: PlayerService? = null
|
||||||
|
|
||||||
|
val serviceInstance: PlayerService
|
||||||
|
get() = _serviceInstance!!
|
||||||
|
|
||||||
|
var isServiceRunning = false
|
||||||
|
}
|
||||||
|
|
||||||
private lateinit var mediaSession: MediaSession
|
private lateinit var mediaSession: MediaSession
|
||||||
lateinit var player: ExoPlayer
|
lateinit var player: ExoPlayer
|
||||||
private lateinit var playerNotificationManager: PlayerNotificationManager
|
private lateinit var playerNotificationManager: PlayerNotificationManager
|
||||||
private val notificationId = 1004
|
private val notificationId = 1004
|
||||||
private var virtualQueue = NotaQueue()
|
private var virtualQueue = NotaQueue()
|
||||||
private var queueId = -1
|
val currentTrack
|
||||||
|
get() = virtualQueue.current
|
||||||
|
private var queueId = 0
|
||||||
|
var control = Channel<Boolean>()
|
||||||
|
var data = Channel<Pair<Boolean, Float>>()
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@SuppressLint("ForegroundServiceType")
|
@SuppressLint("ForegroundServiceType")
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
_serviceInstance = this
|
||||||
|
isServiceRunning = true
|
||||||
player = ExoPlayer.Builder(this).build()
|
player = ExoPlayer.Builder(this).build()
|
||||||
player.addListener(object : Player.Listener {
|
player.addListener(object : Player.Listener {
|
||||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||||
|
@ -49,13 +72,13 @@ class PlayerService : MediaSessionService() {
|
||||||
|
|
||||||
override fun createCurrentContentIntent(player: Player) = null
|
override fun createCurrentContentIntent(player: Player) = null
|
||||||
|
|
||||||
override fun getCurrentContentText(player: Player) = "context?"
|
override fun getCurrentContentText(player: Player) = "content?"
|
||||||
|
|
||||||
override fun getCurrentLargeIcon(player: Player, callback: PlayerNotificationManager.BitmapCallback) = null
|
override fun getCurrentLargeIcon(player: Player, callback: PlayerNotificationManager.BitmapCallback) = null
|
||||||
})
|
})
|
||||||
.build()
|
.build()
|
||||||
.apply {
|
.apply {
|
||||||
setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
setPriority(NotificationCompat.PRIORITY_MAX)
|
||||||
setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
setPlayer(player)
|
setPlayer(player)
|
||||||
setMediaSessionToken(mediaSession.platformToken)
|
setMediaSessionToken(mediaSession.platformToken)
|
||||||
|
@ -66,6 +89,16 @@ class PlayerService : MediaSessionService() {
|
||||||
|
|
||||||
val nBuilder = NotificationCompat.Builder(this, "emptyynes").setStyle(MediaStyleNotificationHelper.MediaStyle(mediaSession))
|
val nBuilder = NotificationCompat.Builder(this, "emptyynes").setStyle(MediaStyleNotificationHelper.MediaStyle(mediaSession))
|
||||||
startForeground(notificationId, nBuilder.build(), FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK)
|
startForeground(notificationId, nBuilder.build(), FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK)
|
||||||
|
|
||||||
|
CoroutineScope(SupervisorJob()).launch { // data.
|
||||||
|
while (true) {
|
||||||
|
while (control.isEmpty) {
|
||||||
|
data.send(player.isPlaying to (player.currentPosition.toFloat() / player.duration.toFloat()))
|
||||||
|
delay(if (player.isPlaying) player.duration / 2000 else 33)
|
||||||
|
}
|
||||||
|
if (!control.receive()) break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
|
30
app/src/main/java/usr/empty/player/database/Album.kt
Normal file
30
app/src/main/java/usr/empty/player/database/Album.kt
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package usr.empty.player.database
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy.Companion.IGNORE
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import androidx.room.Query
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
data class Album(
|
||||||
|
@ColumnInfo val name: String, @ColumnInfo val artistId: UUID
|
||||||
|
) {
|
||||||
|
@PrimaryKey
|
||||||
|
var uuid: UUID = UUID.nameUUIDFromBytes("$name$artistId".toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface AlbumDao {
|
||||||
|
@Insert(onConflict = IGNORE)
|
||||||
|
fun insertAlbum(album: Album)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM album")
|
||||||
|
fun getAllAlbums(): List<Album>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM album WHERE :uuid = uuid")
|
||||||
|
fun getAlbumByUUID(uuid: UUID): Album?
|
||||||
|
}
|
|
@ -2,10 +2,20 @@ package usr.empty.player.database
|
||||||
|
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
|
import usr.empty.player.items.NotaDescriptor
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [Track::class], version = 1
|
entities = [Track::class, Artist::class, Album::class], version = 1
|
||||||
)
|
)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
abstract fun trackDao(): TrackDao
|
abstract fun trackDao(): TrackDao
|
||||||
|
abstract fun artistDao(): ArtistDao
|
||||||
|
abstract fun albumDao(): AlbumDao
|
||||||
|
|
||||||
|
fun addNewTrack(nota: NotaDescriptor) {
|
||||||
|
Artist(Artist.ArtistType.UNKNOWN, nota.artist).let { artist ->
|
||||||
|
artistDao().insertArtist(artist)
|
||||||
|
trackDao().insertTrack(Track(nota.name, artist.uuid, null, nota.sourceType, nota.source))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
35
app/src/main/java/usr/empty/player/database/Artist.kt
Normal file
35
app/src/main/java/usr/empty/player/database/Artist.kt
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
package usr.empty.player.database
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy.Companion.IGNORE
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import androidx.room.Query
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
data class Artist(
|
||||||
|
@ColumnInfo val type: ArtistType, @ColumnInfo val name: String?
|
||||||
|
) {
|
||||||
|
enum class ArtistType {
|
||||||
|
UNKNOWN, SINGLE, DUET, FEAT, BAND, CIRCLE
|
||||||
|
}
|
||||||
|
|
||||||
|
@PrimaryKey
|
||||||
|
var uuid: UUID = if (name == null) UUID.randomUUID() else UUID.nameUUIDFromBytes(name.toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface ArtistDao {
|
||||||
|
@Insert(onConflict = IGNORE)
|
||||||
|
fun insertArtist(artist: Artist)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM artist")
|
||||||
|
fun getAllArtists(): List<Artist>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM artist WHERE :uuid = uuid")
|
||||||
|
fun getArtistByUUID(uuid: UUID): Artist?
|
||||||
|
}
|
|
@ -4,38 +4,39 @@ import androidx.room.ColumnInfo
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy.Companion.REPLACE
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import usr.empty.player.NotaDescriptor
|
import usr.empty.player.items.NotaDescriptor
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
// a.k.a. Nota
|
// a.k.a. Nota
|
||||||
@Entity
|
@Entity
|
||||||
data class Track(
|
data class Track(
|
||||||
@ColumnInfo val title: String,
|
@ColumnInfo val title: String,
|
||||||
@ColumnInfo val artistId: Int,
|
@ColumnInfo val artistId: UUID,
|
||||||
@ColumnInfo val albumId: Int?,
|
@ColumnInfo val albumId: UUID?,
|
||||||
@ColumnInfo val sourceType: NotaDescriptor.Source,
|
@ColumnInfo val sourceType: NotaDescriptor.Source,
|
||||||
@ColumnInfo val source: String,
|
@ColumnInfo val source: String,
|
||||||
) {
|
) {
|
||||||
@PrimaryKey(autoGenerate = true)
|
@PrimaryKey
|
||||||
var uid: Int = 0
|
|
||||||
|
|
||||||
@ColumnInfo
|
|
||||||
var uuid: UUID = UUID.nameUUIDFromBytes("${title}${artistId}".toByteArray())
|
var uuid: UUID = UUID.nameUUIDFromBytes("${title}${artistId}".toByteArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface TrackDao {
|
interface TrackDao {
|
||||||
@Insert
|
@Insert(onConflict = REPLACE)
|
||||||
fun insertTrack(track: Track)
|
fun insertTrack(track: Track)
|
||||||
|
|
||||||
@Query("SELECT * FROM track")
|
@Query("SELECT * FROM track")
|
||||||
fun getAll(): List<Track>
|
fun getAll(): List<Track>
|
||||||
|
|
||||||
@Query("SELECT * FROM track WHERE :artistId = artistId")
|
@Query("SELECT * FROM track WHERE :artistId = artistId")
|
||||||
fun getByArtistId(artistId: Int): List<Track>
|
fun getByArtistId(artistId: UUID): List<Track>
|
||||||
|
|
||||||
@Query("SELECT * FROM track WHERE :albumId = albumId")
|
@Query("SELECT * FROM track WHERE :albumId = albumId")
|
||||||
fun getByAlbumId(albumId: Int): List<Track>
|
fun getByAlbumId(albumId: UUID): List<Track>
|
||||||
|
|
||||||
|
@Query("DELETE FROM track WHERE uuid = :uuid")
|
||||||
|
fun deleteByUUID(uuid: UUID)
|
||||||
}
|
}
|
19
app/src/main/java/usr/empty/player/items/AlbumDescriptor.kt
Normal file
19
app/src/main/java/usr/empty/player/items/AlbumDescriptor.kt
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
package usr.empty.player.items
|
||||||
|
|
||||||
|
import usr.empty.player.database.Album
|
||||||
|
import usr.empty.player.database.AppDatabase
|
||||||
|
|
||||||
|
class AlbumDescriptor(
|
||||||
|
val name: String, val artistName: String?, @Suppress("unused") val trackList: List<NotaDescriptor>
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun fromAlbum(album: Album, database: AppDatabase): AlbumDescriptor {
|
||||||
|
return AlbumDescriptor(
|
||||||
|
album.name,
|
||||||
|
database.artistDao().getArtistByUUID(album.artistId)?.name,
|
||||||
|
database.trackDao().getByAlbumId(album.uuid).map {
|
||||||
|
NotaDescriptor.fromTrack(it, database)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
app/src/main/java/usr/empty/player/items/ArtistDescriptor.kt
Normal file
17
app/src/main/java/usr/empty/player/items/ArtistDescriptor.kt
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
package usr.empty.player.items
|
||||||
|
|
||||||
|
import usr.empty.player.database.Artist
|
||||||
|
|
||||||
|
|
||||||
|
class ArtistDescriptor(
|
||||||
|
val name: String
|
||||||
|
) {
|
||||||
|
val albumList = ArrayList<AlbumDescriptor>()
|
||||||
|
val trackList = ArrayList<NotaDescriptor>()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromArtist(artist: Artist): ArtistDescriptor {
|
||||||
|
return ArtistDescriptor(artist.name ?: "unknown")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
app/src/main/java/usr/empty/player/items/NotaDescriptor.kt
Normal file
45
app/src/main/java/usr/empty/player/items/NotaDescriptor.kt
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package usr.empty.player.items
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import usr.empty.player.database.AppDatabase
|
||||||
|
import usr.empty.player.database.Track
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class NotaDescriptor(
|
||||||
|
val name: String,
|
||||||
|
val artist: String,
|
||||||
|
val sourceType: Source,
|
||||||
|
val source: String,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun fromTrack(track: Track, database: AppDatabase): NotaDescriptor {
|
||||||
|
return NotaDescriptor(
|
||||||
|
track.title, database.artistDao().getArtistByUUID(track.artistId)?.name ?: "unknown", track.sourceType, track.source
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Source {
|
||||||
|
LOCAL
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other == null) return false
|
||||||
|
if (other !is NotaDescriptor) return false
|
||||||
|
if (this.hashCode() != other.hashCode()) return false
|
||||||
|
if (this.sourceType != other.sourceType) return false
|
||||||
|
if (this.source != other.source) return false
|
||||||
|
if (this.artist != other.artist) return false
|
||||||
|
if (this.name != other.name) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = name.hashCode()
|
||||||
|
result = 31 * result + artist.hashCode()
|
||||||
|
result = 31 * result + sourceType.hashCode()
|
||||||
|
result = 31 * result + source.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,12 +2,14 @@
|
||||||
agp = "8.6.1"
|
agp = "8.6.1"
|
||||||
kotlin = "2.0.0"
|
kotlin = "2.0.0"
|
||||||
coreKtx = "1.13.1"
|
coreKtx = "1.13.1"
|
||||||
|
kotlinxCoroutinesAndroid = "1.7.3"
|
||||||
kotlinxSerializationJson = "1.7.3"
|
kotlinxSerializationJson = "1.7.3"
|
||||||
lifecycleRuntimeKtx = "2.8.6"
|
lifecycleRuntimeKtx = "2.8.6"
|
||||||
activityCompose = "1.9.2"
|
activityCompose = "1.9.2"
|
||||||
composeBom = "2024.09.03"
|
composeBom = "2024.09.03"
|
||||||
media3Exoplayer = "1.4.1"
|
media3Exoplayer = "1.4.1"
|
||||||
roomRuntime = "2.6.1"
|
roomRuntime = "2.6.1"
|
||||||
|
composeAudiowaveformVersion = "1.1.2"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
|
@ -19,6 +21,7 @@ androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecyc
|
||||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||||
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||||
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomRuntime" }
|
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomRuntime" }
|
||||||
|
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomRuntime" }
|
||||||
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" }
|
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" }
|
||||||
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
|
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||||
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
||||||
|
@ -26,6 +29,7 @@ androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
||||||
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||||
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version = "1.7.3" }
|
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version = "1.7.3" }
|
||||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||||
|
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }
|
||||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
|
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
|
||||||
com-google-devtools-ksp-gradle-plugin = { group = "com.google.devtools.ksp", name = "com.google.devtools.ksp.gradle.plugin", version = "2.0.20-1.0.25" }
|
com-google-devtools-ksp-gradle-plugin = { group = "com.google.devtools.ksp", name = "com.google.devtools.ksp.gradle.plugin", version = "2.0.20-1.0.25" }
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue