fix: coroutine-dispatcher.

This commit is contained in:
oxy-macmini
2025-04-20 17:29:08 +08:00
parent 0807da85a2
commit aacb95cfff
29 changed files with 161 additions and 406 deletions

View File

@ -9,8 +9,6 @@ import androidx.lifecycle.viewModelScope
import androidx.work.WorkManager
import com.m3u.smartphone.ui.common.connect.RemoteControlSheetValue
import com.m3u.core.architecture.Publisher
import com.m3u.core.architecture.dispatcher.Dispatcher
import com.m3u.core.architecture.dispatcher.M3uDispatchers.IO
import com.m3u.core.architecture.preferences.Preferences
import com.m3u.data.api.TvApiDelegate
import com.m3u.data.tv.model.RemoteDirection
@ -21,7 +19,6 @@ import com.m3u.data.repository.programme.ProgrammeRepository
import com.m3u.data.service.Messager
import com.m3u.data.worker.SubscriptionWorker
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
@ -29,7 +26,6 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
@ -47,7 +43,6 @@ class AppViewModel @Inject constructor(
private val workManager: WorkManager,
private val preferences: Preferences,
private val publisher: Publisher,
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
) : ViewModel() {
init {
refreshProgrammes()
@ -72,7 +67,6 @@ class AppViewModel @Inject constructor(
flowOf(ConnectionToTvValue.Idle())
}
}
.flowOn(ioDispatcher)
.stateIn(
scope = viewModelScope,
initialValue = ConnectionToTvValue.Idle(),

View File

@ -2,8 +2,6 @@ package com.m3u.smartphone.ui.business.channel
import android.content.pm.ActivityInfo
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDpAsState
@ -12,13 +10,10 @@ import androidx.compose.animation.core.animateIntAsState
import androidx.compose.animation.core.spring
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.border
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsDraggedAsState
import androidx.compose.foundation.interaction.collectIsHoveredAsState
@ -30,10 +25,10 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.automirrored.rounded.VolumeOff
@ -55,7 +50,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
@ -72,13 +66,16 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withLink
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.media3.common.Player
import com.m3u.business.channel.PlayerState
import com.m3u.core.architecture.preferences.hiltPreferences
@ -89,7 +86,6 @@ import com.m3u.core.foundation.ui.thenIf
import com.m3u.core.util.basic.isNotEmpty
import com.m3u.data.database.model.AdjacentChannels
import com.m3u.i18n.R.string
import com.m3u.smartphone.ui.business.channel.components.CwPositionRewinder
import com.m3u.smartphone.ui.business.channel.components.MaskDimension
import com.m3u.smartphone.ui.business.channel.components.MaskTextButton
import com.m3u.smartphone.ui.business.channel.components.PlayerMask
@ -123,9 +119,10 @@ fun ChannelMask(
favourite: Boolean,
isSeriesPlaylist: Boolean,
isPanelExpanded: Boolean,
useVertical: Boolean,
hasTrack: Boolean,
cwPosition: Long,
onRewind: () -> Unit,
onResetPlayback: () -> Unit,
onSpeedUpdated: (Float) -> Unit,
onSpeedStart: () -> Unit,
onSpeedEnd: () -> Unit,
@ -143,7 +140,6 @@ fun ChannelMask(
val preferences = hiltPreferences()
val helper = LocalHelper.current
val spacing = LocalSpacing.current
val configuration = LocalConfiguration.current
val coroutineScope = rememberCoroutineScope()
val onBackPressedDispatcher = checkNotNull(
@ -222,8 +218,6 @@ fun ChannelMask(
var volumeBeforeMuted: Float by remember { mutableFloatStateOf(0.4f) }
val isPanelGestureSupported = configuration.screenWidthDp < configuration.screenHeightDp
var bufferedPosition: Long? by remember { mutableStateOf(null) }
LaunchedEffect(bufferedPosition) {
bufferedPosition?.let {
@ -252,8 +246,10 @@ fun ChannelMask(
Color.Black.copy(alpha = if (isPanelExpanded) 0f else 0.54f)
)
val playStateDisplayText = ChannelMaskUtils.playStateDisplayText(playerState.playState)
val exceptionDisplayText = ChannelMaskUtils.playbackExceptionDisplayText(playerState.playerError)
val exceptionDisplayText =
ChannelMaskUtils.playbackExceptionDisplayText(playerState.playerError)
val cwPositionObj = cwPosition.takeIf { it != -1L }?.let { CwPosition(it) }
PlayerMask(
state = maskState,
color = color,
@ -305,7 +301,7 @@ fun ChannelMask(
)
}
if (!isPanelGestureSupported) {
if (!useVertical) {
MaskButton(
state = maskState,
icon = if (isPanelExpanded) Icons.Rounded.Archive
@ -386,10 +382,15 @@ fun ChannelMask(
}
}
},
control = cwPositionObj?.let {
composableOf<RowScope> {
CwPositionSliderImpl(it.milliseconds, onResetPlayback = onResetPlayback)
}
},
footer = composableOf<RowScope>(
any {
suggest { !isPanelExpanded }
suggest { !isPanelGestureSupported }
suggest { !useVertical }
suggest { playStateDisplayText.isNotEmpty() }
suggest { exceptionDisplayText.isNotEmpty() }
suggestAll {
@ -405,7 +406,7 @@ fun ChannelMask(
.weight(1f)
) {
val alpha by animateFloatAsState(
if (!isPanelExpanded || !isPanelGestureSupported) 1f else 0f
if (!isPanelExpanded || !useVertical) 1f else 0f
)
Column(Modifier.alpha(alpha)) {
Text(
@ -476,56 +477,17 @@ fun ChannelMask(
)
}
},
slider = {
val sliderRole: MaskSlideRole = when {
cwPosition != -1L -> MaskSlideRole.CwPosition(cwPosition)
isProgressEnabled && isStaticAndSeekable -> MaskSlideRole.Slide
else -> MaskSlideRole.None
}
AnimatedContent(
targetState = sliderRole,
modifier = Modifier.fillMaxWidth()
) { role ->
when (role) {
is MaskSlideRole.CwPosition -> {
CwPositionSliderImpl(
position = role.milliseconds,
onResetPlayback = onRewind,
modifier = Modifier.animateEnterExit(
enter = fadeIn() + scaleIn(initialScale = 0.85f),
exit = fadeOut() + scaleOut(targetScale = 0.85f)
)
)
}
MaskSlideRole.Slide -> {
SliderImpl(
contentDuration = contentDuration,
contentPosition = contentPosition,
bufferedPosition = bufferedPosition,
onBufferedPositionChanged = {
bufferedPosition = it
maskState.wake()
}
)
}
MaskSlideRole.None -> {}
slider = composableOf(isProgressEnabled && isStaticAndSeekable) {
SliderImpl(
contentDuration = contentDuration,
contentPosition = contentPosition,
bufferedPosition = bufferedPosition,
isPanelExpanded = isPanelExpanded,
onBufferedPositionChanged = {
bufferedPosition = it
maskState.wake()
}
}
AnimatedVisibility(
visible = true,
enter = slideInVertically(),
exit = slideOutVertically(),
modifier = Modifier.padding(top = 4.dp)
) {
}
when {
isProgressEnabled && isStaticAndSeekable -> {
}
}
)
},
onDimensionChanged = onDimensionChanged,
modifier = Modifier.fillMaxSize()
@ -539,6 +501,7 @@ private fun SliderImpl(
onBufferedPositionChanged: (Long) -> Unit,
contentPosition: Long,
contentDuration: Long,
isPanelExpanded: Boolean,
modifier: Modifier = Modifier
) {
val spacing = LocalSpacing.current
@ -550,10 +513,9 @@ private fun SliderImpl(
val fontWeight by animateIntAsState(
targetValue = if (bufferedPosition != null) 800
else 400,
label = "position-text-font-weight"
)
Row(
horizontalArrangement = Arrangement.spacedBy(spacing.medium),
horizontalArrangement = Arrangement.spacedBy(spacing.small),
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
) {
@ -566,23 +528,19 @@ private fun SliderImpl(
color = LocalContentColor.current.copy(alpha = 0.75f),
maxLines = 1,
fontFamily = FontFamilies.JetbrainsMono,
fontSize = 12.sp,
fontWeight = FontWeight(fontWeight),
overflow = TextOverflow.Ellipsis,
modifier = Modifier.basicMarquee()
)
val sliderThumbWidthDp by animateDpAsState(
targetValue = 4.dp,
label = "slider-thumb-width-dp"
)
val sliderThumbWidthDp by animateDpAsState(4.dp)
val sliderInteractionSource = remember { MutableInteractionSource() }
Slider(
value = animContentPosition,
valueRange = 0f..contentDuration
.coerceAtLeast(0L)
.toFloat(),
onValueChange = {
onBufferedPositionChanged(it.roundToLong())
},
onValueChange = { onBufferedPositionChanged(it.roundToLong()) },
thumb = {
SliderDefaults.Thumb(
interactionSource = sliderInteractionSource,
@ -600,38 +558,44 @@ private fun CwPositionSliderImpl(
modifier: Modifier = Modifier,
onResetPlayback: () -> Unit
) {
val spacing = LocalSpacing.current
val time = remember(position) {
position.toDuration(DurationUnit.MILLISECONDS).toComponents { hours, minutes, seconds, _ ->
buildString {
if (hours > 0) {
append("$hours:")
position.toDuration(DurationUnit.MILLISECONDS).toComponents { h, m, s, _ ->
listOf(h, m, s)
.dropWhile { it.toInt() == 0 }
.joinToString(":") {
if (it.toInt() >= 10) it.toString()
else "0$it"
}
if (minutes < 10) {
append("0")
}
append("$minutes:")
if (seconds < 10) {
append("0")
}
append("$seconds")
}
}
}
Column(modifier) {
Spacer(modifier = Modifier.height(8.dp))
CwPositionRewinder(
text = {
Text(
text = stringResource(string.feat_channel_cw_position_title, time),
)
},
action = {
TextButton(onClick = onResetPlayback) {
Text(
text = stringResource(string.feat_channel_cw_position_button)
)
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically
) {
// Text(
// text = stringResource(string.feat_channel_cw_position_title, time),
// modifier = Modifier.weight(1f)
// )
Text(
text = buildAnnotatedString {
withLink(
LinkAnnotation.Clickable(
tag = stringResource(string.feat_channel_cw_position_button),
linkInteractionListener = { onResetPlayback() }
),
) {
append(stringResource(string.feat_channel_cw_position_button))
}
}
},
style = MaterialTheme.typography.labelMedium,
modifier = Modifier
.border(
width = 1.dp,
color = LocalContentColor.current.copy(0.65f),
shape = CircleShape
)
.padding(spacing.small)
)
}
}
@ -658,7 +622,6 @@ private fun MaskCenterButton(
}
val scale by animateFloatAsState(
targetValue = if (isScaled) 0.65f else 1f,
label = "MaskCenterButton-scale",
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessMediumLow
@ -708,10 +671,7 @@ private fun MaskNavigateButton(
val isScaled by remember {
derivedStateOf { isPressed || isHovered || isDragged }
}
val scale by animateFloatAsState(
targetValue = if (isScaled) 0.85f else 1f,
label = "MaskCenterButton-scale"
)
val scale by animateFloatAsState(if (isScaled) 0.85f else 1f)
MaskCircleButton(
state = state,
isSmallDimension = true,
@ -759,8 +719,5 @@ private enum class MaskNavigateRole {
Next, Previous
}
private sealed class MaskSlideRole {
data object None : MaskSlideRole()
data class CwPosition(val milliseconds: Long) : MaskSlideRole()
data object Slide : MaskSlideRole()
}
data class CwPosition(val milliseconds: Long)
data object Slide

View File

@ -6,7 +6,6 @@ import android.graphics.Rect
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
@ -18,7 +17,6 @@ import androidx.compose.material.icons.automirrored.rounded.VolumeUp
import androidx.compose.material.icons.rounded.DarkMode
import androidx.compose.material.icons.rounded.LightMode
import androidx.compose.material.icons.rounded.Speed
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@ -31,7 +29,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalWindowInfo
@ -179,6 +176,7 @@ fun ChannelRoute(
}
.launchIn(this)
snapshotFlow { pullPanelLayoutState.fraction }
.drop(1)
.onEach { maskState.sleep() }
.launchIn(this)
}
@ -283,7 +281,7 @@ fun ChannelRoute(
speed = it
},
cwPosition = viewModel.cwPosition,
onRewind = viewModel::onRewind,
onResetPlayback = viewModel::onResetPlayback,
onPreviousChannelClick = viewModel::getPreviousChannel,
onNextChannelClick = viewModel::getNextChannel,
onEnterPipMode = {
@ -344,7 +342,7 @@ private fun ChannelPlayer(
brightness: Float,
speed: Float,
cwPosition: Long,
onRewind: () -> Unit,
onResetPlayback: () -> Unit,
onFavorite: () -> Unit,
openDlnaDevices: () -> Unit,
openChooseFormat: () -> Unit,
@ -455,9 +453,10 @@ private fun ChannelPlayer(
maskState = maskState,
favourite = favourite,
isSeriesPlaylist = isSeriesPlaylist,
useVertical = useVertical,
hasTrack = hasTrack,
cwPosition = cwPosition,
onRewind = onRewind,
onResetPlayback = onResetPlayback,
isPanelExpanded = isPanelExpanded,
onFavorite = onFavorite,
openDlnaDevices = openDlnaDevices,

View File

@ -1,85 +0,0 @@
package com.m3u.smartphone.ui.business.channel.components
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.m3u.smartphone.ui.material.components.FontFamilies
@Composable
fun CwPositionRewinder(
modifier: Modifier = Modifier,
containerColor: Color = Color.Unspecified,
contentColor: Color = Color.Unspecified,
borderColor: Color = Color.Unspecified,
text: @Composable () -> Unit,
action: (@Composable RowScope.() -> Unit)? = null,
) {
val lContainerColor = containerColor.takeOrElse { MaterialTheme.colorScheme.surfaceVariant }
val lContentColor = contentColor.takeOrElse {
MaterialTheme.colorScheme.contentColorFor(lContainerColor)
.takeOrElse { MaterialTheme.colorScheme.onSurfaceVariant }
}
val lBorderColor = borderColor.takeOrElse { MaterialTheme.colorScheme.outline }
CompositionLocalProvider(
LocalContentColor provides lContentColor,
LocalTextStyle provides MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamilies.LexendExa
)
) {
Row(
Modifier
.fillMaxWidth()
.drawBehind {
drawRoundRect(
color = lContainerColor,
cornerRadius = CornerRadius(16.dp.toPx())
)
drawRoundRect(
color = lBorderColor,
cornerRadius = CornerRadius(16.dp.toPx()),
style = Stroke(
width = 2.dp.toPx(),
cap = Stroke.DefaultCap,
miter = Stroke.DefaultMiter,
pathEffect = null
)
)
}
.semantics(mergeDescendants = true) {
onClick { true }
}
.then(modifier),
verticalAlignment = Alignment.CenterVertically
) {
Spacer(Modifier.width(16.dp))
Row(Modifier.weight(1f)) { text() }
if (action != null) {
Spacer(Modifier.size(8.dp))
action.invoke(this)
Spacer(Modifier.width(8.dp))
} else {
Spacer(Modifier.width(16.dp))
}
}
}
}

View File

@ -1,5 +1,6 @@
package com.m3u.smartphone.ui.material.components.mask
import android.util.Log
import androidx.annotation.IntRange
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
@ -42,7 +43,7 @@ private class MaskStateCoroutineImpl(
private var currentTime: Long by mutableLongStateOf(systemClock)
private var lastTime: Long by mutableLongStateOf(0L)
private var keys by mutableStateOf<Set<Any>>(emptySet())
override val locked: Boolean by derivedStateOf { keys.isNotEmpty() }
override val locked: Boolean = keys.isNotEmpty()
override val visible: Boolean by derivedStateOf {
val before = (locked || (currentTime - lastTime <= minDuration))
@ -62,10 +63,12 @@ private class MaskStateCoroutineImpl(
private val systemClock: Long get() = System.currentTimeMillis() / 1000
override fun wake(duration: Duration) {
Log.e("TAG", "ChannelPlayer: weak", )
lastTime = currentTime + duration.inWholeMilliseconds / 1000
}
override fun sleep() {
Log.e("TAG", "ChannelPlayer: sleep", )
lastTime = 0
val iterator = unlockedJobs.iterator()
while (iterator.hasNext()) {

View File

@ -99,10 +99,10 @@ class ChannelViewModel @Inject constructor(
}
}
fun onRewind() {
fun onResetPlayback() {
val channelUrl = channel.value?.url ?: return
viewModelScope.launch {
playerManager.onRewind(channelUrl)
playerManager.onResetPlayback(channelUrl)
}
}

View File

@ -15,8 +15,6 @@ import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.m3u.core.Contracts
import com.m3u.core.architecture.dispatcher.Dispatcher
import com.m3u.core.architecture.dispatcher.M3uDispatchers.IO
import com.m3u.core.architecture.logger.Logger
import com.m3u.core.architecture.logger.Profiles
import com.m3u.core.architecture.logger.install
@ -33,7 +31,6 @@ import com.m3u.data.repository.media.MediaRepository
import com.m3u.data.repository.playlist.PlaylistRepository
import com.m3u.data.service.PlayerManager
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@ -41,7 +38,6 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
@ -55,26 +51,16 @@ class FavoriteViewModel @Inject constructor(
private val mediaRepository: MediaRepository,
private val playerManager: PlayerManager,
preferences: Preferences,
@Dispatcher(IO) ioDispatcher: CoroutineDispatcher,
delegate: Logger
) : ViewModel() {
private val logger = delegate.install(Profiles.VIEWMODEL_FAVOURITE)
private val zappingMode = snapshotFlow { preferences.zappingMode }
.flowOn(ioDispatcher)
.stateIn(
scope = viewModelScope,
initialValue = Preferences.DEFAULT_ZAPPING_MODE,
started = SharingStarted.WhileSubscribed(5_000)
)
val zapping: StateFlow<Channel?> = combine(
zappingMode,
snapshotFlow { preferences.zappingMode },
playerManager.channel
) { zappingMode, channel ->
channel.takeIf { zappingMode }
}
.flowOn(ioDispatcher)
.stateIn(
scope = viewModelScope,
initialValue = null,
@ -94,7 +80,6 @@ class FavoriteViewModel @Inject constructor(
val sort = sortIndex
.map { sorts[it] }
.flowOn(ioDispatcher)
.stateIn(
scope = viewModelScope,
initialValue = Sort.UNSPECIFIED,

View File

@ -6,8 +6,6 @@ import androidx.lifecycle.viewModelScope
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkQuery
import com.m3u.core.architecture.dispatcher.Dispatcher
import com.m3u.core.architecture.dispatcher.M3uDispatchers.Default
import com.m3u.core.architecture.logger.Logger
import com.m3u.core.architecture.logger.Profiles
import com.m3u.core.architecture.logger.install
@ -26,7 +24,7 @@ import com.m3u.data.repository.programme.ProgrammeRepository
import com.m3u.data.service.PlayerManager
import com.m3u.data.worker.SubscriptionWorker
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@ -50,7 +48,6 @@ class ForyouViewModel @Inject constructor(
programmeRepository: ProgrammeRepository,
private val playerManager: PlayerManager,
preferences: Preferences,
@Dispatcher(Default) defaultDispatcher: CoroutineDispatcher,
workManager: WorkManager,
delegate: Logger
) : ViewModel() {
@ -88,7 +85,6 @@ class ForyouViewModel @Inject constructor(
private val unseensDuration = snapshotFlow { preferences.unseensMilliseconds }
.map { it.toDuration(DurationUnit.MILLISECONDS) }
.flowOn(defaultDispatcher)
.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
@ -99,13 +95,12 @@ class ForyouViewModel @Inject constructor(
unseensDuration.flatMapLatest { channelRepository.observeAllUnseenFavorites(it) },
channelRepository.observePlayedRecently(),
) { channels, playedRecently ->
playerManager.cwPositionObserver
listOfNotNull<Recommend.Spec>(
playedRecently?.let { Recommend.CwSpec(it, playerManager.getCwPosition(it.url)) },
*(channels.map { channel -> Recommend.UnseenSpec(channel) }.take(8).toTypedArray())
)
}
.flowOn(defaultDispatcher)
.flowOn(Dispatchers.IO)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(1_000L),

View File

@ -6,8 +6,6 @@ import androidx.lifecycle.viewModelScope
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkQuery
import com.m3u.core.architecture.dispatcher.Dispatcher
import com.m3u.core.architecture.dispatcher.M3uDispatchers.IO
import com.m3u.core.architecture.logger.Logger
import com.m3u.core.architecture.logger.Profiles
import com.m3u.core.architecture.logger.install
@ -23,6 +21,7 @@ import com.m3u.data.repository.programme.ProgrammeRepository
import com.m3u.data.worker.SubscriptionWorker
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
@ -46,7 +45,6 @@ class PlaylistConfigurationViewModel @Inject constructor(
private val programmeRepository: ProgrammeRepository,
private val xtreamParser: XtreamParser,
private val workManager: WorkManager,
@Dispatcher(IO) ioDispatcher: CoroutineDispatcher,
savedStateHandle: SavedStateHandle,
delegate: Logger
) : ViewModel() {
@ -107,7 +105,7 @@ class PlaylistConfigurationViewModel @Inject constructor(
)
}
}
.flowOn(ioDispatcher)
.flowOn(Dispatchers.Default)
.stateIn(
scope = viewModelScope,
initialValue = null,

View File

@ -21,8 +21,6 @@ import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkQuery
import com.m3u.core.Contracts
import com.m3u.core.architecture.dispatcher.Dispatcher
import com.m3u.core.architecture.dispatcher.M3uDispatchers.IO
import com.m3u.core.architecture.logger.Logger
import com.m3u.core.architecture.logger.Profiles
import com.m3u.core.architecture.logger.install
@ -50,6 +48,7 @@ import com.m3u.business.playlist.PlaylistMessage.ChannelCoverSaved
import com.m3u.core.wrapper.Sort
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@ -84,7 +83,6 @@ class PlaylistViewModel @Inject constructor(
private val playerManager: PlayerManager,
preferences: Preferences,
workManager: WorkManager,
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
delegate: Logger
) : ViewModel() {
private val logger = delegate.install(Profiles.VIEWMODEL_PLAYLIST)
@ -129,7 +127,7 @@ class PlaylistViewModel @Inject constructor(
)
}
}
.flowOn(ioDispatcher)
.flowOn(Dispatchers.Default)
.stateIn(
scope = viewModelScope,
initialValue = false,

View File

@ -13,8 +13,6 @@ import androidx.work.WorkManager
import androidx.work.WorkQuery
import androidx.work.workDataOf
import com.m3u.core.architecture.Publisher
import com.m3u.core.architecture.dispatcher.Dispatcher
import com.m3u.core.architecture.dispatcher.M3uDispatchers.IO
import com.m3u.core.architecture.logger.Logger
import com.m3u.core.architecture.logger.Profiles
import com.m3u.core.architecture.logger.install
@ -31,12 +29,11 @@ import com.m3u.data.parser.xtream.XtreamInput
import com.m3u.data.repository.playlist.PlaylistRepository
import com.m3u.data.repository.channel.ChannelRepository
import com.m3u.data.service.Messager
import com.m3u.data.service.PlayerManager
import com.m3u.data.worker.BackupWorker
import com.m3u.data.worker.RestoreWorker
import com.m3u.data.worker.SubscriptionWorker
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
@ -60,7 +57,6 @@ class SettingViewModel @Inject constructor(
publisher: Publisher,
// FIXME: do not use dao in viewmodel
private val colorSchemeDao: ColorSchemeDao,
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
delegate: Logger
) : ViewModel() {
private val logger = delegate.install(Profiles.VIEWMODEL_SETTING)
@ -89,7 +85,7 @@ class SettingViewModel @Inject constructor(
.filter { it.hiddenCategories.isNotEmpty() }
.flatMap { playlist -> playlist.hiddenCategories.map { playlist to it } }
}
.flowOn(ioDispatcher)
.flowOn(Dispatchers.Default)
.stateIn(
scope = viewModelScope,
initialValue = emptyList(),
@ -106,7 +102,7 @@ class SettingViewModel @Inject constructor(
colorSchemeDao.observeAll().catch { emit(emptyList()) },
snapshotFlow { preferences.followSystemTheme }
) { all, followSystemTheme -> if (followSystemTheme) all.filter { !it.isDark } else all }
.flowOn(ioDispatcher)
.flowOn(Dispatchers.Default)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
@ -254,7 +250,7 @@ class SettingViewModel @Inject constructor(
}
BackingUpAndRestoringState.of(backingUp, restoring)
}
.flowOn(ioDispatcher)
.flowOn(Dispatchers.Default)
.stateIn(
scope = viewModelScope,
// determine ui button enabled or not

View File

@ -16,7 +16,7 @@ abstract class Suggester {
val completedResult: Boolean
get() {
complete()
return result!!
return requireNotNull(result) { "suggester hasn't any conditions." }
}

View File

@ -33,4 +33,4 @@ fun <S> composableOf(
} else {
null
}
}
}

View File

@ -1,25 +0,0 @@
package com.m3u.core.architecture.dispatcher
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
@Module
@InstallIn(SingletonComponent::class)
object CoroutineDispatcherModule {
@Provides
@Dispatcher(M3uDispatchers.Default)
fun provideDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
@Provides
@Dispatcher(M3uDispatchers.IO)
fun provideIODispatcher(): CoroutineDispatcher = Dispatchers.IO
@Provides
@Dispatcher(M3uDispatchers.Main)
fun provideMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
}

View File

@ -1,14 +0,0 @@
package com.m3u.core.architecture.dispatcher
import javax.inject.Qualifier
import kotlin.annotation.AnnotationRetention.RUNTIME
@Qualifier
@Retention(RUNTIME)
annotation class Dispatcher(val dispatcher: M3uDispatchers)
enum class M3uDispatchers {
Default,
IO,
Main
}

View File

@ -2,7 +2,7 @@ package com.m3u.data.parser
import com.m3u.core.architecture.logger.Logger
import com.m3u.core.architecture.logger.execute
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
@ -15,10 +15,9 @@ class ParserUtils(
val json: Json,
val okHttpClient: OkHttpClient,
val logger: Logger,
val ioDispatcher: CoroutineDispatcher
) {
@OptIn(ExperimentalSerializationApi::class)
suspend inline fun <reified T> newCall(url: String): T? = withContext(ioDispatcher) {
suspend inline fun <reified T> newCall(url: String): T? = withContext(Dispatchers.IO) {
logger.execute {
okHttpClient.newCall(
Request.Builder().url(url).build()
@ -33,7 +32,7 @@ class ParserUtils(
@OptIn(ExperimentalSerializationApi::class)
suspend inline fun <reified T> newCallOrThrow(url: String): T =
withContext(ioDispatcher) {
withContext(Dispatchers.IO) {
okHttpClient.newCall(
Request.Builder().url(url).build()
)

View File

@ -1,12 +1,10 @@
package com.m3u.data.parser.epg
import android.util.Xml
import com.m3u.core.architecture.dispatcher.Dispatcher
import com.m3u.core.architecture.dispatcher.M3uDispatchers.IO
import com.m3u.core.architecture.logger.Logger
import com.m3u.core.architecture.logger.Profiles
import com.m3u.core.architecture.logger.install
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.flowOn
@ -15,7 +13,6 @@ import java.io.InputStream
import javax.inject.Inject
internal class EpgParserImpl @Inject constructor(
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
delegate: Logger
) : EpgParser {
private val logger = delegate.install(Profiles.PARSER_EPG)
@ -38,7 +35,7 @@ internal class EpgParserImpl @Inject constructor(
}
}
}
.flowOn(ioDispatcher)
.flowOn(Dispatchers.Default)
private val ns: String? = null
private fun XmlPullParser.readProgramme(): EpgProgramme {

View File

@ -1,12 +1,10 @@
package com.m3u.data.parser.m3u
import com.m3u.core.architecture.dispatcher.Dispatcher
import com.m3u.core.architecture.dispatcher.M3uDispatchers.IO
import com.m3u.core.architecture.logger.Logger
import com.m3u.core.architecture.logger.Profiles
import com.m3u.core.architecture.logger.install
import com.m3u.core.architecture.logger.post
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
@ -14,7 +12,6 @@ import java.io.InputStream
import javax.inject.Inject
internal class M3UParserImpl @Inject constructor(
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
delegate: Logger
) : M3UParser {
private val logger = delegate.install(Profiles.PARSER_M3U)
@ -105,5 +102,5 @@ internal class M3UParserImpl @Inject constructor(
emit(entry)
}
}
.flowOn(ioDispatcher)
.flowOn(Dispatchers.Default)
}

View File

@ -1,26 +1,23 @@
package com.m3u.data.parser.xtream
import com.m3u.core.architecture.dispatcher.Dispatcher
import com.m3u.core.architecture.dispatcher.M3uDispatchers.IO
import com.m3u.core.architecture.logger.Logger
import com.m3u.core.architecture.logger.Profiles
import com.m3u.core.architecture.logger.install
import com.m3u.data.api.OkhttpClient
import com.m3u.data.database.model.DataSource
import com.m3u.data.parser.ParserUtils
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import java.time.Duration
import javax.inject.Inject
internal class XtreamParserImpl @Inject constructor(
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
@OkhttpClient(true) okHttpClient: OkHttpClient,
delegate: Logger
) : XtreamParser {
@ -33,19 +30,12 @@ internal class XtreamParserImpl @Inject constructor(
explicitNulls = false
isLenient = true
}
private val okHttpClient = okHttpClient
.newBuilder()
.callTimeout(Duration.ofMillis(Int.MAX_VALUE.toLong()))
.connectTimeout(Duration.ofMillis(Int.MAX_VALUE.toLong()))
.readTimeout(Duration.ofMillis(Int.MAX_VALUE.toLong()))
.build()
private val utils by lazy {
ParserUtils(
json = json,
okHttpClient = okHttpClient,
logger = logger,
ioDispatcher = ioDispatcher
logger = logger
)
}
@ -94,6 +84,7 @@ internal class XtreamParserImpl @Inject constructor(
.collect { serial -> send(serial) }
}
}
.flowOn(Dispatchers.Default)
override suspend fun getXtreamOutput(input: XtreamInput): XtreamOutput {
val (basicUrl, username, password, type) = input

View File

@ -13,8 +13,6 @@ import coil.request.ErrorResult
import coil.request.ImageRequest
import coil.request.SuccessResult
import com.m3u.core.architecture.logger.Profiles
import com.m3u.core.architecture.dispatcher.Dispatcher
import com.m3u.core.architecture.dispatcher.M3uDispatchers.IO
import com.m3u.core.architecture.logger.Logger
import com.m3u.core.architecture.logger.execute
import com.m3u.core.architecture.logger.install
@ -23,7 +21,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import io.ktor.util.cio.writeChannel
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.copyAndClose
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.InputStream
@ -35,7 +33,6 @@ private const val BITMAP_QUALITY = 100
internal class MediaRepositoryImpl @Inject constructor(
@ApplicationContext private val context: Context,
delegate: Logger,
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher
) : MediaRepository {
private val logger = delegate.install(Profiles.REPOS_MEDIA)
private val applicationName = "M3U"
@ -48,7 +45,7 @@ internal class MediaRepositoryImpl @Inject constructor(
applicationName
)
override suspend fun savePicture(url: String): File = withContext(ioDispatcher) {
override suspend fun savePicture(url: String): File = withContext(Dispatchers.IO) {
val drawable = checkNotNull(loadDrawable(url))
val bitmap = drawable.toBitmap()
val name = "Picture_${System.currentTimeMillis()}.png"

View File

@ -5,8 +5,6 @@ import android.content.Context
import android.net.Uri
import androidx.core.net.toUri
import androidx.work.WorkManager
import com.m3u.core.architecture.dispatcher.Dispatcher
import com.m3u.core.architecture.dispatcher.M3uDispatchers
import com.m3u.core.architecture.logger.Logger
import com.m3u.core.architecture.logger.Profiles
import com.m3u.core.architecture.logger.execute
@ -44,7 +42,7 @@ import com.m3u.data.repository.createCoroutineCache
import com.m3u.data.worker.SubscriptionWorker
import dagger.hilt.android.qualifiers.ApplicationContext
import io.ktor.http.Url
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.channelFlow
@ -83,7 +81,6 @@ internal class PlaylistRepositoryImpl @Inject constructor(
private val preferences: Preferences,
private val workManager: WorkManager,
@ApplicationContext private val context: Context,
@Dispatcher(M3uDispatchers.IO) private val ioDispatcher: CoroutineDispatcher,
) : PlaylistRepository {
private val logger = delegate.install(Profiles.REPOS_PLAYLIST)
@ -158,7 +155,7 @@ internal class PlaylistRepositoryImpl @Inject constructor(
}
.onEach(cache::push)
.onCompletion { cache.flush() }
.flowOn(ioDispatcher)
.flowOn(Dispatchers.IO)
.collect()
}
@ -169,7 +166,7 @@ internal class PlaylistRepositoryImpl @Inject constructor(
password: String,
type: String?,
callback: (count: Int) -> Unit
): Unit = withContext(ioDispatcher) {
): Unit = withContext(Dispatchers.IO) {
val input = XtreamInput(basicUrl, username, password, type)
val (
liveCategories,
@ -347,17 +344,16 @@ internal class PlaylistRepositoryImpl @Inject constructor(
.collect()
}
override suspend fun insertEpgAsPlaylist(title: String, epg: String): Unit =
withContext(ioDispatcher) {
// just save epg playlist to db
playlistDao.insertOrReplace(
Playlist(
title = title,
url = epg,
source = DataSource.EPG
)
override suspend fun insertEpgAsPlaylist(title: String, epg: String) {
// just save epg playlist to db
playlistDao.insertOrReplace(
Playlist(
title = title,
url = epg,
source = DataSource.EPG
)
}
)
}
override suspend fun refresh(url: String) = logger.sandBox {
val playlist = checkNotNull(get(url)) { "Cannot find playlist: $url" }
@ -388,7 +384,7 @@ internal class PlaylistRepositoryImpl @Inject constructor(
}
}
override suspend fun backupOrThrow(uri: Uri): Unit = withContext(ioDispatcher) {
override suspend fun backupOrThrow(uri: Uri): Unit = withContext(Dispatchers.IO) {
val json = Json {
prettyPrint = false
}
@ -420,7 +416,7 @@ internal class PlaylistRepositoryImpl @Inject constructor(
}
override suspend fun restoreOrThrow(uri: Uri) = logger.sandBox {
withContext(ioDispatcher) {
withContext(Dispatchers.IO) {
val json = Json {
ignoreUnknownKeys = true
}
@ -644,9 +640,9 @@ internal class PlaylistRepositoryImpl @Inject constructor(
private suspend fun String.actualUrl(): String {
if (!isSupportedAndroidUrl()) return this
val uri = Uri.parse(this)
val uri = this.toUri()
if (uri.scheme == ContentResolver.SCHEME_FILE) return uri.toString()
return withContext(ioDispatcher) {
return withContext(Dispatchers.IO) {
val contentResolver = context.contentResolver
val filename = uri.readFileName(contentResolver) ?: filenameWithTimezone
val destinationFile = File(context.filesDir, filename)
@ -672,7 +668,7 @@ internal class PlaylistRepositoryImpl @Inject constructor(
}
private fun openAndroidInput(url: String): InputStream? {
val uri = Uri.parse(url)
val uri = url.toUri()
return context.contentResolver.openInputStream(uri)
}
}

View File

@ -3,8 +3,6 @@ package com.m3u.data.repository.programme
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import com.m3u.core.architecture.dispatcher.Dispatcher
import com.m3u.core.architecture.dispatcher.M3uDispatchers.IO
import com.m3u.core.architecture.logger.Logger
import com.m3u.core.architecture.logger.Profiles
import com.m3u.core.architecture.logger.execute
@ -21,7 +19,7 @@ import com.m3u.data.database.model.epgUrlsOrXtreamXmlUrl
import com.m3u.data.parser.epg.EpgParser
import com.m3u.data.parser.epg.EpgProgramme
import com.m3u.data.parser.epg.toProgramme
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.Flow
@ -46,7 +44,6 @@ internal class ProgrammeRepositoryImpl @Inject constructor(
private val programmeDao: ProgrammeDao,
private val epgParser: EpgParser,
@OkhttpClient(true) private val okHttpClient: OkHttpClient,
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
delegate: Logger
) : ProgrammeRepository {
private val logger = delegate.install(Profiles.REPOS_PROGRAMME)
@ -191,7 +188,7 @@ internal class ProgrammeRepositoryImpl @Inject constructor(
.collect { send(it) }
}
}
.flowOn(ioDispatcher)
.flowOn(Dispatchers.IO)
/**
* Attempts to find the first valid EPG URL from a list of URLs.

View File

@ -3,8 +3,6 @@ package com.m3u.data.repository.tv
import android.net.nsd.NsdServiceInfo
import androidx.compose.runtime.snapshotFlow
import com.m3u.core.architecture.Publisher
import com.m3u.core.architecture.dispatcher.Dispatcher
import com.m3u.core.architecture.dispatcher.M3uDispatchers.IO
import com.m3u.core.architecture.logger.Logger
import com.m3u.core.architecture.logger.Profiles
import com.m3u.core.architecture.logger.install
@ -17,9 +15,9 @@ import com.m3u.data.tv.Utils
import com.m3u.data.tv.http.HttpServer
import com.m3u.data.tv.model.TvInfo
import com.m3u.data.tv.nsd.NsdDeviceManager
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.Flow
@ -46,11 +44,10 @@ class TvRepositoryImpl @Inject constructor(
logger: Logger,
preferences: Preferences,
publisher: Publisher,
@Dispatcher(IO) ioDispatcher: CoroutineDispatcher
) : TvRepository() {
private val logger = logger.install(Profiles.REPOS_LEANBACK)
private val tv = publisher.tv
private val coroutineScope = CoroutineScope(ioDispatcher)
private val coroutineScope = CoroutineScope(SupervisorJob())
init {
snapshotFlow { preferences.remoteControl }

View File

@ -48,7 +48,7 @@ interface PlayerManager {
suspend fun recordVideo(uri: Uri)
val cwPositionObserver: SharedFlow<Long>
suspend fun onRewind(channelUrl: String)
suspend fun onResetPlayback(channelUrl: String)
suspend fun getCwPosition(channelUrl: String): Long
suspend fun reloadThumbnail(channelUrl: String): Uri?
suspend fun syncThumbnail(channelUrl: String): Uri?

View File

@ -3,17 +3,16 @@ package com.m3u.data.service.internal
import android.net.Uri
import androidx.core.net.toUri
import com.jakewharton.disklrucache.DiskLruCache
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.security.MessageDigest
internal class ChannelPreferenceProvider(
directory: File,
appVersion: Int,
ioDispatcher: CoroutineDispatcher
appVersion: Int
) {
private val limitedParallelism = ioDispatcher.limitedParallelism(1, "channel-preference")
private val limitedParallelism = Dispatchers.IO.limitedParallelism(1, "channel-preference")
private val cache = DiskLruCache.open(directory, appVersion, 3, 4 * 1024 * 1024) // 4mb
suspend operator fun get(

View File

@ -1,24 +1,16 @@
package com.m3u.data.service.internal
import androidx.compose.runtime.Immutable
import com.m3u.core.architecture.dispatcher.Dispatcher
import com.m3u.core.architecture.dispatcher.M3uDispatchers.Main
import com.m3u.data.tv.model.RemoteDirection
import com.m3u.data.service.DPadReactionService
import kotlinx.coroutines.CoroutineDispatcher
import com.m3u.data.tv.model.RemoteDirection
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.withContext
import javax.inject.Inject
@Immutable
class DPadReactionServiceImpl @Inject constructor(
@Dispatcher(Main) private val mainDispatcher: CoroutineDispatcher
) : DPadReactionService {
class DPadReactionServiceImpl @Inject constructor() : DPadReactionService {
override val incoming = MutableSharedFlow<RemoteDirection>()
override suspend fun emit(remoteDirection: RemoteDirection) {
withContext(mainDispatcher) {
incoming.emit(remoteDirection)
}
incoming.emit(remoteDirection)
}
}

View File

@ -1,12 +1,10 @@
package com.m3u.data.service.internal
import com.m3u.core.architecture.dispatcher.Dispatcher
import com.m3u.core.architecture.dispatcher.M3uDispatchers.IO
import com.m3u.core.wrapper.Message
import com.m3u.data.service.Messager
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -14,14 +12,12 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
class MessagerImpl @Inject constructor(
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher
) : Messager {
class MessagerImpl @Inject constructor() : Messager {
private val _message: MutableStateFlow<Message> = MutableStateFlow(Message.Dynamic.EMPTY)
override val message: StateFlow<Message> get() = _message.asStateFlow()
private var job: Job? = null
private val coroutineScope = CoroutineScope(ioDispatcher)
private val coroutineScope = CoroutineScope(SupervisorJob())
override fun emit(message: Message) {
job?.cancel()

View File

@ -51,9 +51,6 @@ import androidx.media3.transformer.InAppMp4Muxer
import androidx.media3.transformer.TransformationRequest
import androidx.media3.transformer.Transformer
import com.m3u.core.architecture.Publisher
import com.m3u.core.architecture.dispatcher.Dispatcher
import com.m3u.core.architecture.dispatcher.M3uDispatchers.IO
import com.m3u.core.architecture.dispatcher.M3uDispatchers.Main
import com.m3u.core.architecture.logger.Logger
import com.m3u.core.architecture.logger.Profiles
import com.m3u.core.architecture.logger.install
@ -73,8 +70,8 @@ import com.m3u.data.service.MediaCommand
import com.m3u.data.service.PlayerManager
import dagger.hilt.android.qualifiers.ApplicationContext
import io.ktor.http.Url
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
@ -108,8 +105,6 @@ import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
class PlayerManagerImpl @Inject constructor(
@Dispatcher(Main) private val mainDispatcher: CoroutineDispatcher,
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
@ApplicationContext private val context: Context,
@OkhttpClient(false) private val okHttpClient: OkHttpClient,
private val preferences: Preferences,
@ -119,13 +114,12 @@ class PlayerManagerImpl @Inject constructor(
publisher: Publisher,
delegate: Logger
) : PlayerManager, Player.Listener, MediaSession.Callback {
private val mainCoroutineScope = CoroutineScope(mainDispatcher)
private val ioCoroutineScope = CoroutineScope(ioDispatcher)
private val mainCoroutineScope = CoroutineScope(Dispatchers.Main)
private val ioCoroutineScope = CoroutineScope(Dispatchers.IO)
private val channelPreferenceProvider = ChannelPreferenceProvider(
directory = context.cacheDir.resolve("channel-preferences"),
appVersion = publisher.versionCode,
ioDispatcher = ioDispatcher
appVersion = publisher.versionCode
)
private val continueWatchingCondition = ContinueWatchingCondition.getInstance<Player>()
@ -428,7 +422,7 @@ class PlayerManagerImpl @Inject constructor(
delay(1.seconds)
}
}
.flowOn(ioDispatcher)
.flowOn(Dispatchers.IO)
override suspend fun reloadThumbnail(channelUrl: String): Uri? {
val channelPreference = getChannelPreference(channelUrl)
@ -442,8 +436,9 @@ class PlayerManagerImpl @Inject constructor(
}
}
}
override suspend fun syncThumbnail(channelUrl: String): Uri? = withContext(ioDispatcher) {
val thumbnail = codecs.getThumbnail(context, channelUrl.toUri())?: return@withContext null
override suspend fun syncThumbnail(channelUrl: String): Uri? = withContext(Dispatchers.IO) {
val thumbnail = codecs.getThumbnail(context, channelUrl.toUri()) ?: return@withContext null
val filename = UUID.randomUUID().toString() + ".jpeg"
val file = File(thumbnailDir, filename)
while (!file.createNewFile()) {
@ -462,6 +457,7 @@ class PlayerManagerImpl @Inject constructor(
)
uri
}
private fun createPlayer(
mediaSourceFactory: MediaSource.Factory,
tunneling: Boolean
@ -621,7 +617,7 @@ class PlayerManagerImpl @Inject constructor(
}
override suspend fun recordVideo(uri: Uri) {
withContext(mainDispatcher) {
withContext(Dispatchers.Main) {
try {
val currentPlayer = player.value ?: return@withContext
val tracksGroup = currentPlayer.currentTracks.groups.first {
@ -700,7 +696,7 @@ class PlayerManagerImpl @Inject constructor(
)
.build()
withContext(mainDispatcher) {
withContext(Dispatchers.Main) {
transformer.start(
MediaItem.fromUri(channel.value?.url.orEmpty()),
uri.path.orEmpty()
@ -714,7 +710,7 @@ class PlayerManagerImpl @Inject constructor(
override val cwPositionObserver = MutableSharedFlow<Long>(replay = 1)
override suspend fun onRewind(channelUrl: String) {
override suspend fun onResetPlayback(channelUrl: String) {
cwPositionObserver.emit(-1L)
resetContinueWatching(channelUrl, ignorePositionCondition = true)
val currentPlayer = player.value ?: return
@ -794,7 +790,7 @@ class PlayerManagerImpl @Inject constructor(
cwPositionObserver.emit(-1L)
return
}
withContext(mainDispatcher) {
withContext(Dispatchers.Main) {
if (continueWatchingCondition.isRestoringSupported(player)) {
logger.post { "restoreContinueWatching, $cwPosition" }
cwPositionObserver.emit(cwPosition)
@ -803,12 +799,19 @@ class PlayerManagerImpl @Inject constructor(
}
}
private suspend fun resetContinueWatching(channelUrl: String, ignorePositionCondition: Boolean = false) {
private suspend fun resetContinueWatching(
channelUrl: String,
ignorePositionCondition: Boolean = false
) {
logger.post { "resetContinueWatching, channelUrl=$channelUrl, ignorePositionCondition=$ignorePositionCondition" }
val channelPreference = getChannelPreference(channelUrl)
val player = this@PlayerManagerImpl.player.value
withContext(mainDispatcher) {
if (player != null && continueWatchingCondition.isResettingSupported(player, ignorePositionCondition)) {
withContext(Dispatchers.Main) {
if (player != null && continueWatchingCondition.isResettingSupported(
player,
ignorePositionCondition
)
) {
addChannelPreference(
channelUrl,
channelPreference?.copy(cwPosition = -1L) ?: ChannelPreference(cwPosition = -1L)

View File

@ -2,27 +2,22 @@ package com.m3u.data.tv.nsd
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import com.m3u.core.architecture.dispatcher.Dispatcher
import com.m3u.core.architecture.dispatcher.M3uDispatchers.IO
import com.m3u.core.architecture.logger.Logger
import com.m3u.core.architecture.logger.Profiles
import com.m3u.core.architecture.logger.install
import com.m3u.data.tv.Utils
import com.m3u.data.tv.nsd.NsdDeviceManager.Companion.META_DATA_PIN
import com.m3u.data.tv.nsd.NsdDeviceManager.Companion.SERVICE_TYPE
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flowOn
import javax.inject.Inject
class NsdDeviceManagerImpl @Inject constructor(
private val nsdManager: NsdManager,
delegate: Logger,
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher
) : NsdDeviceManager {
private val logger = delegate.install(Profiles.SERVICE_NSD)
@ -91,7 +86,6 @@ class NsdDeviceManagerImpl @Inject constructor(
nsdManager.stopServiceDiscovery(listener)
}
}
.flowOn(ioDispatcher)
override fun broadcast(
name: String,
@ -139,5 +133,4 @@ class NsdDeviceManagerImpl @Inject constructor(
trySendBlocking(null)
}
}
.flowOn(ioDispatcher)
}