Skip to content

Commit

Permalink
Refactored PlayerController, added playback position flow.
Browse files Browse the repository at this point in the history
Tweaked system paddings for each screen.
Updated dependencies, project metadata.
  • Loading branch information
aleksey-saenko committed Nov 2, 2023
1 parent 83144d7 commit dd61561
Show file tree
Hide file tree
Showing 19 changed files with 177 additions and 74 deletions.
11 changes: 11 additions & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Developers

[aleksey-saenko](https://github.com/aleksey-saenko)

# Translators

| Language | Translators |
| :-- | :-- |
| English | [aleksey-saenko](https://github.com/aleksey-saenko) |
| Russian | [aleksey-saenko](https://github.com/aleksey-saenko) |
| Slovak | [tomz00](https://github.com/tomz00) |
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,11 @@ Stack: Kotlin, Coroutines, Jetpack Compose, Hilt, WorkManager, Room, OkHttp, Mos

This application uses the AudD® service as a Music Recognition API. You need a special API token provided by AudD® to use the application. If you don't have one, you can sign up for a free API token.
You can add the key on the onboarding or preferences screen, or just set it in `local.properties`.
There is also the option to use the app without a token, but please note that this will restrict the number of daily recognitions that can be performed

There is also the option to use the app without a token, but please note that this will restrict the number of daily recognitions that can be performed.

## License

Copyright (C) 2023 [Aleksey Saenko]
Copyright (C) 2023 [Aleksey Saenko].

The license is [GNU GPLv3](LICENSE.md).
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,21 @@ class AdapterPlayerController @Inject constructor(
get() = playerControllerDo.statusFlow
.map { status -> statusMapper.map(status) }

override val playbackPositionFlow: Flow<Int>
get() = playerControllerDo.playbackPositionFlow

override fun start(file: File) {
playerControllerDo.start(file)
}

override fun pause() {
playerControllerDo.pause()
}

override fun resume() {
playerControllerDo.resume()
}

override fun stop() {
playerControllerDo.stop()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ class PlayerStatusMapper @Inject constructor() : Mapper<PlayerStatusDo, PlayerSt
override fun map(input: PlayerStatusDo): PlayerStatus {
return when (input) {
PlayerStatusDo.Idle -> PlayerStatus.Idle
is PlayerStatusDo.Paused -> PlayerStatus.Paused(input.record)
is PlayerStatusDo.Started -> PlayerStatus.Started(input.record)
is PlayerStatusDo.Paused -> PlayerStatus.Paused(input.record, input.duration)
is PlayerStatusDo.Started -> PlayerStatus.Started(input.record, input.duration)
is PlayerStatusDo.Error -> PlayerStatus.Error(input.record, input.message)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
Expand Down Expand Up @@ -125,7 +127,7 @@ private fun NavGraphBuilder.barNavHost(
) {
composable(BAR_HOST_ROUTE) {
Column(
modifier = Modifier.fillMaxSize(),
modifier = Modifier.fillMaxSize().navigationBarsPadding(),
verticalArrangement = Arrangement.Bottom,
horizontalAlignment = Alignment.CenterHorizontally
) {
Expand All @@ -134,14 +136,21 @@ private fun NavGraphBuilder.barNavHost(
horizontalArrangement = Arrangement.Start,
modifier = Modifier.weight(1f, false)
) {
if (shouldShowNavRail) AppNavigationRail(navController = innerNavController)
if (shouldShowNavRail) {
AppNavigationRail(
navController = innerNavController,
modifier = Modifier.statusBarsPadding()
)
}
BarNavHost(
outerNavController = outerNavController,
innerNavController = innerNavController,
modifier = Modifier.weight(1f, false)
)
}
if (!shouldShowNavRail) AppNavigationBar(navController = innerNavController)
if (!shouldShowNavRail) {
AppNavigationBar(navController = innerNavController)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,18 @@ import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.*
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.mrsep.musicrecognizer.core.common.di.ApplicationScope
import com.mrsep.musicrecognizer.core.ui.theme.MusicRecognizerTheme
import com.mrsep.musicrecognizer.data.track.util.DatabaseFiller
import com.mrsep.musicrecognizer.feature.recognition.presentation.service.toggleNotificationService
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import javax.inject.Inject

@AndroidEntryPoint
class MainActivity : ComponentActivity() {

@Inject
lateinit var databaseFiller: DatabaseFiller

@Inject
@ApplicationScope
lateinit var appScope: CoroutineScope

private val viewModel: MainActivityViewModel by viewModels()

@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
Expand All @@ -53,17 +42,8 @@ class MainActivity : ComponentActivity() {
setContent {
val windowSizeClass = calculateWindowSizeClass(this)
val uiState by viewModel.uiStateStream.collectAsStateWithLifecycle()
MusicRecognizerTheme(
dynamicColor = isDynamicColorsEnabled(uiState)
) {
Surface(
color = Color.Unspecified,
contentColor = contentColorFor(MaterialTheme.colorScheme.background),
modifier = Modifier
.fillMaxSize()
.background(color = MaterialTheme.colorScheme.background)
.navigationBarsPadding(),
) {
MusicRecognizerTheme(dynamicColor = isDynamicColorsEnabled(uiState)) {
Surface(modifier = Modifier.fillMaxSize()) {
AppNavigation(
shouldShowNavRail = shouldShowNavRail(windowSizeClass),
isExpandedScreen = isExpandedScreen(windowSizeClass),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,43 +1,65 @@
package com.mrsep.musicrecognizer.data.player

import android.media.MediaPlayer
import com.mrsep.musicrecognizer.core.common.di.DefaultDispatcher
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import java.io.File
import java.lang.IllegalStateException
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds

@Suppress("unused")
private const val TAG = "MediaPlayerController"

//TODO: need to handle exceptions
class MediaPlayerController @Inject constructor() : PlayerControllerDo {
class MediaPlayerController @Inject constructor(
@DefaultDispatcher private val pollingDispatcher: CoroutineDispatcher
) : PlayerControllerDo {

private var player: MediaPlayer? = null

private val playerCoroutineScope = CoroutineScope(pollingDispatcher + SupervisorJob())
private var positionPollingJob: Job? = null

private val _currentPosition = Channel<Int>(Channel.CONFLATED)
override val playbackPositionFlow = _currentPosition.receiveAsFlow()

private val _statusFlow = MutableStateFlow<PlayerStatusDo>(PlayerStatusDo.Idle)
override val statusFlow = _statusFlow.asStateFlow()

override fun start(file: File) {
try {
stop()
stopWithStatus(PlayerStatusDo.Idle)
player = MediaPlayer().apply {
isLooping = false
setDataSource(file.absolutePath)
setOnCompletionListener {
_statusFlow.update { PlayerStatusDo.Idle }
stopWithStatus(PlayerStatusDo.Idle)
}
setOnErrorListener { _, what, extra ->
val errorStatus = PlayerStatusDo.Error(
record = file,
message = "what=$what, extra=$extra"
)
stopWithStatus(errorStatus)
true
}
setOnPreparedListener { mediaPlayer ->
mediaPlayer.start()
_statusFlow.update {
PlayerStatusDo.Error(
PlayerStatusDo.Started(
record = file,
message = "what=$what, extra=$extra"
duration = mediaPlayer.duration.milliseconds
)
}
true
}
setOnPreparedListener {
player?.start()
_statusFlow.update { PlayerStatusDo.Started(file) }
launchPositionPolling()
}
prepareAsync()
}
Expand All @@ -57,10 +79,16 @@ class MediaPlayerController @Inject constructor() : PlayerControllerDo {
if (currentStatus is PlayerStatusDo.Started) {
try {
player?.pause()
positionPollingJob?.cancel()
_statusFlow.update {
PlayerStatusDo.Paused(
record = currentStatus.record,
duration = (player?.duration ?: -1).milliseconds
)
}
} catch (e: IllegalStateException) {
e.printStackTrace()
}
_statusFlow.update { PlayerStatusDo.Paused(currentStatus.record) }
}
}

Expand All @@ -69,24 +97,53 @@ class MediaPlayerController @Inject constructor() : PlayerControllerDo {
if (currentStatus is PlayerStatusDo.Paused) {
try {
player?.start()
launchPositionPolling()
_statusFlow.update {
PlayerStatusDo.Started(
record = currentStatus.record,
duration = (player?.duration ?: -1).milliseconds
)
}
} catch (e: IllegalStateException) {
e.printStackTrace()
}
_statusFlow.update { PlayerStatusDo.Started(currentStatus.record) }
}
}

override fun stop() {
player?.apply {
try {
stop()
} catch (e: IllegalStateException) {
e.printStackTrace()
}
release()
stopWithStatus(PlayerStatusDo.Idle)
}

private fun stopWithStatus(playerStatus: PlayerStatusDo) {
stopPollingAndResetPosition()
try {
player?.stop()
} catch (e: IllegalStateException) {
e.printStackTrace()
}
player?.release()
player = null
_statusFlow.update { PlayerStatusDo.Idle }
_statusFlow.update { playerStatus }
}

private fun launchPositionPolling() {
positionPollingJob = playerCoroutineScope.launch {
while (player?.isPlaying == true) {
player?.currentPosition?.run { _currentPosition.send(this) }
delay(POSITION_POLLING_RATE_MS)
}
}
}

private fun stopPollingAndResetPosition() {
playerCoroutineScope.launch {
positionPollingJob?.cancelAndJoin()
_currentPosition.send(0)
}
}

companion object {
private const val POSITION_POLLING_RATE_MS = 100L
}

}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package com.mrsep.musicrecognizer.data.player

import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.Flow
import java.io.File

interface PlayerControllerDo {
val statusFlow: StateFlow<PlayerStatusDo>
val statusFlow: Flow<PlayerStatusDo>
val playbackPositionFlow: Flow<Int>

fun start(file: File)
fun stop()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
package com.mrsep.musicrecognizer.data.player

import java.io.File
import kotlin.time.Duration

sealed class PlayerStatusDo {

data object Idle : PlayerStatusDo()
data class Started(val record: File) : PlayerStatusDo()
data class Paused(val record: File) : PlayerStatusDo()
data class Error(val record: File, val message: String) : PlayerStatusDo()

data class Started(
val record: File,
val duration: Duration
) : PlayerStatusDo()

data class Paused(
val record: File,
val duration: Duration
) : PlayerStatusDo()

data class Error(
val record: File,
val message: String
) : PlayerStatusDo()

}
3 changes: 2 additions & 1 deletion fastlane/metadata/android/en-US/changelogs/3.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
* Added Slovak translation (thanks to tomz00)
* Added option to adapt track/lyrics screen to artwork colors (can be enabled via preferences)
* Added option to adapt track/lyrics screen to artwork colors (can be enabled via preferences)
* Fixed a bug with establishing websocket connection, which caused the app to crash on some devices
3 changes: 2 additions & 1 deletion fastlane/metadata/android/ru/changelogs/3.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
* Добавлен словацкий перевод (спасибо tomz00)
* Добавлена опция адаптации цветовой темы экрана трека под обложку (может быть включено через настройки)
* Добавлена опция адаптации цветовой темы экрана трека под обложку (может быть включено через настройки)
* Исправлена ошибка при установлении веб-сокет соединения, приводившая к сбою приложения на некоторых устройствах
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,16 @@ internal fun DeveloperScreen(
.fillMaxSize()
.background(color = MaterialTheme.colorScheme.background)
) {
DeveloperScreenTopBar(
topAppBarScrollBehavior = topBarBehaviour,
onBackPressed = onBackPressed
)
AnimatedVisibility(isProcessing) {
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.primary
)
}
DeveloperScreenTopBar(
topAppBarScrollBehavior = topBarBehaviour,
onBackPressed = onBackPressed
)
Column(
modifier = Modifier
.nestedScroll(topBarBehaviour.nestedScrollConnection)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ internal fun OnboardingScreen(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.statusBarsPadding()
.systemBarsPadding()
) { pageIndex ->
when (pageIndex) {
OnboardingPage.WELCOME.ordinal -> {
Expand Down
Loading

0 comments on commit dd61561

Please sign in to comment.