Update v1.0.49 (#330)

* Added PDF top tab strip visibility toggle and fixed WebView hit test NPE

* Refactored desktop reader screens and state management into specialized components

* Added image gallery to reader sidebar and refactored desktop PDF UI components

* Implemented EPUB image gallery

* Refactored reader and library models to use shared common types, centralizing file type resolution and texture management while removing redundant mapping logic

* Standardized UI styling and refactored app navigation layout

* Implemented auto-hiding reader chrome and activity tracking in desktop

* Refactored reader panels into distinct left and right modal layers with platform-specific sizing and keyboard navigation support.

* Added PPTX support for desktop and refactored parsing into a shared module

* Implement paid AI features and account management for the desktop application.

* Implement AI Hub and enhanced Cloud TTS integration for Desktop

* Implement streaming support for AI definition and summarization features

* Implement support for password-protected PDFs and file actions in the desktop reader.

* Implement cloud synchronization for desktop using Firestore and Google Drive

* Implement PDF reflow and "Text View" for the desktop reader

* Refactor OPDS logic to use SharedOpdsController

* Optimize PDF tile rendering performance

* Implement two-page spread support for PDF pagination

* Implement two-page spread support for the PDF viewer

* Improved shared spread zoom in PDF viewer

* Improve PDF spread navigation with fling support and configurable page gaps

* Add brightness control to PDF and EPUB readers

* Refactor folder synchronization to use shared logic engine

* Implement safe string formatting and validation for localized resources

* Implement TTS chunk skip navigation

* Implement deep-linking and playback controls for TTS media sessions

* Implement start index for TTS playback

* Improve TTS navigation, prefetching, and notification duration reporting

* Implement TTS mini playback bar for background reading

* Implement multi-window reader support for the desktop application

* Improve desktop modal window management and visibility syncing

* Implement localized string support for Desktop and shared UI

* Implement language selection and persistence for Desktop

* Implement plural string support for Desktop and migrate hardcoded counts to plurals.xml

* Implement localized banner messages and UI strings using resource-backed SharedText

* Implement compact badge styling for small book covers

* Refactor PDF native interaction and improve HTML import memory safety

* fix language persistence

* Refactor reader overflow menus to use section-based logic

* Refactor PDF layout remapping and improve text box interaction

* Improve CFI resolution and TTS resume accuracy using dynamic chunk offsets

* Centralize PDF annotation export mapping and improve metadata handling

* Add support for threaded comments in PDF highlight annotations

* Flatten highlight comments into a single thread for PDF export and allow author editing

* Integrate page slider into reader chrome and persist toggle state

* Handle fragments and queries in EPUB chapter paths

* Implement dynamic, theme-aware coloring for the reader slider

* Implement customizable app-wide font preference

* Implement one-hand zoom gestures in the PDF viewer

* Implement File Information dialog for PDF and EPUB readers

* Bump version to 1.0.49 (53)

* Refactor PDF reader logic into modular components

* Add ProGuard rules to prevent R8 optimization issues in EPUB reader screens

* Add option to use PDF filenames as display names

* Fix preservation of PDF filename display preference in library projection
This commit is contained in:
Aryan 2026-05-20 22:14:01 +05:30 committed by GitHub
parent dc5196526f
commit 9510293ac3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
245 changed files with 37538 additions and 12460 deletions

View file

@ -0,0 +1,198 @@
package com.aryan.reader.tts
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.SkipNext
import androidx.compose.material.icons.filled.SkipPrevious
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.media3.common.util.UnstableApi
import com.aryan.reader.R
import com.aryan.reader.tts.TtsPlaybackManager.TtsState
private const val TTS_MINI_BAR_EDGE_PADDING_DP = 16
private const val TTS_MINI_BAR_MAIN_BOTTOM_PADDING_DP = 96
internal fun shouldShowReaderTtsMiniBar(
ttsState: TtsState,
isOnReaderRoute: Boolean
): Boolean {
if (isOnReaderRoute) return false
if (ttsState.playbackSource != "READER") return false
if (ttsState.sessionEndedByStop || ttsState.sessionFinished) return false
return ttsState.isLoading || !ttsState.currentText.isNullOrBlank()
}
internal fun readerTtsMiniBarBottomPaddingDp(isOnMainRoute: Boolean): Int {
return if (isOnMainRoute) {
TTS_MINI_BAR_MAIN_BOTTOM_PADDING_DP
} else {
TTS_MINI_BAR_EDGE_PADDING_DP
}
}
@androidx.annotation.OptIn(UnstableApi::class)
@Composable
fun ReaderTtsMiniBar(
ttsController: TtsController,
ttsState: TtsState,
onOpenReader: () -> Unit,
modifier: Modifier = Modifier
) {
val canOpenReader = !ttsState.bookId.isNullOrBlank()
val canSkipPreviousChunk = !ttsState.isLoading &&
ttsState.currentChunkIndex > 0 &&
ttsState.totalChunks > 0
val canSkipNextChunk = !ttsState.isLoading &&
ttsState.currentChunkIndex >= 0 &&
ttsState.currentChunkIndex < ttsState.totalChunks - 1
val chunkLabel = remember(ttsState.currentChunkIndex, ttsState.totalChunks) {
if (ttsState.currentChunkIndex >= 0 && ttsState.totalChunks > 0) {
"Chunk ${ttsState.currentChunkIndex + 1}/${ttsState.totalChunks}"
} else {
null
}
}
val title = ttsState.bookTitle
?.takeIf { it.isNotBlank() }
?: stringResource(R.string.action_read_aloud)
val subtitle = remember(title, ttsState.chapterTitle, chunkLabel) {
listOfNotNull(
ttsState.chapterTitle
?.takeIf { it.isNotBlank() && it != title },
chunkLabel
).joinToString(" - ")
}
Surface(
shape = RoundedCornerShape(24.dp),
color = MaterialTheme.colorScheme.surfaceContainerHigh,
contentColor = MaterialTheme.colorScheme.onSurface,
tonalElevation = 0.dp,
shadowElevation = 8.dp,
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant),
modifier = modifier
) {
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 64.dp)
.padding(horizontal = 8.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(16.dp))
.clickable(enabled = canOpenReader, onClick = onOpenReader)
.padding(horizontal = 10.dp, vertical = 6.dp),
verticalArrangement = Arrangement.Center
) {
Text(
text = title,
style = MaterialTheme.typography.labelLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
if (subtitle.isNotBlank()) {
Text(
text = subtitle,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
Spacer(Modifier.width(4.dp))
IconButton(
enabled = canSkipPreviousChunk,
onClick = ttsController::skipToPreviousChunk,
modifier = Modifier.size(40.dp)
) {
Icon(
imageVector = Icons.Default.SkipPrevious,
contentDescription = stringResource(R.string.content_desc_tts_previous_chunk),
modifier = Modifier.size(24.dp)
)
}
Box(modifier = Modifier.size(48.dp), contentAlignment = Alignment.Center) {
FilledIconButton(
onClick = {
if (ttsState.isPlaying) {
ttsController.pause()
} else {
ttsController.resume()
}
},
modifier = Modifier.size(44.dp),
colors = IconButtonDefaults.filledIconButtonColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
) {
Icon(
painter = painterResource(if (ttsState.isPlaying) R.drawable.pause else R.drawable.play),
contentDescription = stringResource(
if (ttsState.isPlaying) {
R.string.content_desc_pause_tts
} else {
R.string.content_desc_resume_tts
}
),
modifier = Modifier.size(22.dp)
)
}
if (ttsState.isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(48.dp),
color = MaterialTheme.colorScheme.primary,
strokeWidth = 2.dp
)
}
}
IconButton(
enabled = canSkipNextChunk,
onClick = ttsController::skipToNextChunk,
modifier = Modifier.size(40.dp)
) {
Icon(
imageVector = Icons.Default.SkipNext,
contentDescription = stringResource(R.string.content_desc_tts_next_chunk),
modifier = Modifier.size(24.dp)
)
}
}
}
}

View file

@ -153,8 +153,11 @@ class TtsController(context: Context) : Player.Listener {
bookTitle: String,
chapterTitle: String?,
coverImageUri: String?,
bookId: String? = null,
chapterIndex: Int? = null,
totalChapters: Int? = null,
pageIndex: Int? = null,
startChunkIndex: Int = 0,
continueSession: Boolean = false,
ttsMode: TtsPlaybackManager.TtsMode,
playbackSource: String = "READER",
@ -184,8 +187,11 @@ class TtsController(context: Context) : Player.Listener {
putString(KEY_BOOK_TITLE, bookTitle)
putString(KEY_CHAPTER_TITLE, chapterTitle)
putString(KEY_COVER_IMAGE_URI, coverImageUri)
bookId?.let { putString(KEY_BOOK_ID, it) }
chapterIndex?.let { putInt(KEY_CHAPTER_INDEX, it) }
totalChapters?.let { putInt(KEY_TOTAL_CHAPTERS, it) }
pageIndex?.let { putInt(KEY_PAGE_INDEX, it) }
putInt(KEY_START_CHUNK_INDEX, startChunkIndex)
putBoolean(KEY_CONTINUE_SESSION, continueSession)
putString(KEY_TTS_MODE, ttsMode.name)
putString(KEY_PLAYBACK_SOURCE, playbackSource)
@ -254,6 +260,16 @@ class TtsController(context: Context) : Player.Listener {
mediaController?.sendCustomCommand(SLICE_CURRENT_AND_RELOAD_COMMAND, Bundle.EMPTY)
}
fun skipToPreviousChunk() {
Timber.d("UI sending SKIP_TO_PREVIOUS_TTS_CHUNK command.")
mediaController?.sendCustomCommand(SKIP_TO_PREVIOUS_TTS_CHUNK_COMMAND, Bundle.EMPTY)
}
fun skipToNextChunk() {
Timber.d("UI sending SKIP_TO_NEXT_TTS_CHUNK command.")
mediaController?.sendCustomCommand(SKIP_TO_NEXT_TTS_CHUNK_COMMAND, Bundle.EMPTY)
}
override fun onEvents(player: Player, events: Player.Events) {
Timber.tag(TTS_NOTIFICATION_DIAG_TAG).i(
"Controller onEvents. playbackState=${player.playbackState}, isPlaying=${player.isPlaying}, playWhenReady=${player.playWhenReady}, mediaItems=${player.mediaItemCount}, currentIndex=${player.currentMediaItemIndex}, events=$events"
@ -266,6 +282,7 @@ class TtsController(context: Context) : Player.Listener {
val customState = controller.customLayout.firstOrNull()?.extras ?: Bundle.EMPTY
val currentMediaItem = controller.currentMediaItem
val mediaItemExtras = currentMediaItem?.mediaMetadata?.extras
val currentMediaBookTitle = currentMediaItem?.mediaMetadata?.title?.toString()
val currentTextFromMediaItem = mediaItemExtras?.getString("ttsText")
?: currentMediaItem?.mediaMetadata?.subtitle?.toString()
val isPlaybackActive = controller.isPlaying || controller.playbackState == Player.STATE_READY || controller.playbackState == Player.STATE_BUFFERING
@ -278,12 +295,17 @@ class TtsController(context: Context) : Player.Listener {
val serviceChapterTitle = customState.getString("chapterTitle")
val serviceChapterIndex = customState.getInt("chapterIndex", -1).takeIf { it >= 0 }
val serviceTotalChapters = customState.getInt("totalChapters", -1).takeIf { it > 0 }
val serviceBookId = customState.getString("bookId") ?: mediaItemExtras?.getString("bookId")
val servicePageIndex = customState.getInt("pageIndex", -1)
.takeIf { it >= 0 }
?: mediaItemExtras?.getInt("pageIndex", -1)?.takeIf { it >= 0 }
val serviceCurrentChunkIndex = customState.getInt("currentChunkIndex", -1)
val serviceTotalChunks = customState.getInt("totalChunks", 0)
val serviceBookProgressPercent = customState.getInt("bookProgressPercent", -1).takeIf { it >= 0 }
val sourceCfi = mediaItemExtras?.getString("sourceCfi")
val startOffset = mediaItemExtras?.getInt("startOffset", -1) ?: -1
val sourceCfi = mediaItemExtras?.getString("sourceCfi") ?: customState.getString("sourceCfi")
val startOffset = mediaItemExtras?.getInt("startOffset", -1)
?: customState.getInt("startOffset", -1)
val currentWordSourceCfi = customState.getString("currentWordSourceCfi")
val currentWordStartOffset = customState.getInt("currentWordStartOffset", -1)
val serviceMode = customState.getString("ttsMode", _ttsState.value.ttsMode)
@ -298,8 +320,13 @@ class TtsController(context: Context) : Player.Listener {
if (isLoading) currentState.currentText else null
},
errorMessage = customState.getString("errorMessage"),
bookId = if (isPlaybackActive || isLoading) {
serviceBookId ?: currentState.bookId
} else {
serviceBookId
},
bookTitle = if (isPlaybackActive) {
currentMediaItem?.mediaMetadata?.artist?.toString() ?: serviceBookTitle
currentMediaBookTitle ?: serviceBookTitle
} else {
if (isLoading) currentState.bookTitle else serviceBookTitle
},
@ -318,6 +345,11 @@ class TtsController(context: Context) : Player.Listener {
} else {
serviceTotalChapters
},
pageIndex = if (isPlaybackActive || isLoading) {
servicePageIndex ?: currentState.pageIndex
} else {
servicePageIndex
},
currentChunkIndex = serviceCurrentChunkIndex,
totalChunks = serviceTotalChunks,
bookProgressPercent = serviceBookProgressPercent,

View file

@ -0,0 +1,8 @@
package com.aryan.reader.tts
const val ACTION_OPEN_TTS_SESSION = "com.aryan.reader.tts.OPEN_SESSION"
const val EXTRA_TTS_BOOK_ID = "com.aryan.reader.tts.extra.BOOK_ID"
const val EXTRA_TTS_CHAPTER_INDEX = "com.aryan.reader.tts.extra.CHAPTER_INDEX"
const val EXTRA_TTS_SOURCE_CFI = "com.aryan.reader.tts.extra.SOURCE_CFI"
const val EXTRA_TTS_START_OFFSET = "com.aryan.reader.tts.extra.START_OFFSET"
const val EXTRA_TTS_PAGE_INDEX = "com.aryan.reader.tts.extra.PAGE_INDEX"

File diff suppressed because it is too large Load diff

View file

@ -30,10 +30,16 @@ import android.content.pm.ServiceInfo
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
import androidx.media3.common.ForwardingPlayer
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.CommandButton
import androidx.media3.session.DefaultMediaNotificationProvider
import androidx.media3.session.MediaNotification
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import com.aryan.reader.R
@ -61,6 +67,7 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import com.google.common.collect.ImmutableList
data class WordTimingInfo(val word: String, val startTime: Double)
@ -242,6 +249,227 @@ private const val TTS_FOREGROUND_CHANNEL_ID = "tts_playback"
// Keep this aligned with Media3's default notification ID so playback updates replace the fallback.
private const val TTS_FOREGROUND_NOTIFICATION_ID = 1001
private const val TTS_FOREGROUND_IDLE_GRACE_MS = 15_000L
private const val ACTION_TTS_NOTIFICATION_PREVIOUS_CHUNK = "com.aryan.reader.tts.NOTIFICATION_PREVIOUS_CHUNK"
private const val ACTION_TTS_NOTIFICATION_NEXT_CHUNK = "com.aryan.reader.tts.NOTIFICATION_NEXT_CHUNK"
private const val TTS_NOTIFICATION_PREVIOUS_REQUEST_CODE = 4208
private const val TTS_NOTIFICATION_NEXT_REQUEST_CODE = 4209
@UnstableApi
private class TtsMediaNotificationProvider(
context: android.content.Context
) : DefaultMediaNotificationProvider(
context,
{ _ -> TTS_FOREGROUND_NOTIFICATION_ID },
TTS_FOREGROUND_CHANNEL_ID,
R.string.tts_notification_channel_name
) {
private val appContext = context.applicationContext
override fun addNotificationActions(
mediaSession: MediaSession,
mediaButtons: ImmutableList<CommandButton>,
builder: NotificationCompat.Builder,
actionFactory: MediaNotification.ActionFactory
): IntArray {
return super.addNotificationActions(
mediaSession,
mediaButtons,
builder,
TtsNotificationActionFactory(appContext, actionFactory)
)
}
}
@UnstableApi
private class TtsNotificationActionFactory(
private val context: android.content.Context,
private val delegate: MediaNotification.ActionFactory
) : MediaNotification.ActionFactory {
override fun createMediaAction(
mediaSession: MediaSession,
icon: IconCompat,
title: CharSequence,
command: Int
): NotificationCompat.Action {
return when (command) {
Player.COMMAND_SEEK_TO_PREVIOUS,
Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM -> createChunkSkipAction(
iconResId = R.drawable.skip_previous,
title = title,
action = ACTION_TTS_NOTIFICATION_PREVIOUS_CHUNK,
requestCode = TTS_NOTIFICATION_PREVIOUS_REQUEST_CODE
)
Player.COMMAND_SEEK_TO_NEXT,
Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM -> createChunkSkipAction(
iconResId = R.drawable.skip_next,
title = title,
action = ACTION_TTS_NOTIFICATION_NEXT_CHUNK,
requestCode = TTS_NOTIFICATION_NEXT_REQUEST_CODE
)
else -> delegate.createMediaAction(mediaSession, icon, title, command)
}
}
override fun createCustomAction(
mediaSession: MediaSession,
icon: IconCompat,
title: CharSequence,
customAction: String,
extras: android.os.Bundle
): NotificationCompat.Action {
return delegate.createCustomAction(mediaSession, icon, title, customAction, extras)
}
override fun createCustomActionFromCustomCommandButton(
mediaSession: MediaSession,
customCommandButton: CommandButton
): NotificationCompat.Action {
return delegate.createCustomActionFromCustomCommandButton(mediaSession, customCommandButton)
}
override fun createMediaActionPendingIntent(mediaSession: MediaSession, command: Long): PendingIntent {
return delegate.createMediaActionPendingIntent(mediaSession, command)
}
override fun createNotificationDismissalIntent(mediaSession: MediaSession): PendingIntent {
return delegate.createNotificationDismissalIntent(mediaSession)
}
private fun createChunkSkipAction(
iconResId: Int,
title: CharSequence,
action: String,
requestCode: Int
): NotificationCompat.Action {
val intent = Intent(context, TtsService::class.java).apply {
this.action = action
setPackage(context.packageName)
}
val pendingIntent = PendingIntent.getService(
context,
requestCode,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
return NotificationCompat.Action.Builder(
IconCompat.createWithResource(context, iconResId),
title,
pendingIntent
)
.setShowsUserInterface(false)
.build()
}
}
@UnstableApi
private class TtsSessionPlayer(
player: Player,
private val canSkipToPreviousChunk: () -> Boolean,
private val canSkipToNextChunk: () -> Boolean,
private val skipToPreviousChunk: () -> Unit,
private val skipToNextChunk: () -> Unit,
private val isCurrentChunkStreaming: () -> Boolean,
private val currentChunkDurationForNotification: (Long) -> Long
) : ForwardingPlayer(player) {
override fun getAvailableCommands(): Player.Commands {
val builder = super.getAvailableCommands().buildUpon()
if (canSkipToPreviousChunk()) {
builder
.add(Player.COMMAND_SEEK_TO_PREVIOUS)
.add(Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
} else {
builder
.remove(Player.COMMAND_SEEK_TO_PREVIOUS)
.remove(Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
}
if (canSkipToNextChunk()) {
builder
.add(Player.COMMAND_SEEK_TO_NEXT)
.add(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
} else {
builder
.remove(Player.COMMAND_SEEK_TO_NEXT)
.remove(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
}
return builder.build()
}
override fun isCommandAvailable(command: Int): Boolean {
return getAvailableCommands().contains(command)
}
override fun hasPreviousMediaItem(): Boolean {
return canSkipToPreviousChunk() || super.hasPreviousMediaItem()
}
override fun hasNextMediaItem(): Boolean {
return canSkipToNextChunk() || super.hasNextMediaItem()
}
override fun seekToPrevious() {
skipToPreviousChunk()
}
override fun seekToPreviousMediaItem() {
skipToPreviousChunk()
}
override fun seekToNext() {
skipToNextChunk()
}
override fun seekToNextMediaItem() {
skipToNextChunk()
}
override fun getDuration(): Long {
return notificationDurationMs().takeIf { it != C.TIME_UNSET } ?: super.getDuration()
}
override fun getContentDuration(): Long {
return getDuration()
}
override fun getBufferedPosition(): Long {
return adjustedStreamingBufferedPosition(super.getBufferedPosition())
}
override fun getContentBufferedPosition(): Long {
return getBufferedPosition()
}
override fun getBufferedPercentage(): Int {
val duration = notificationDurationMs()
if (!isCurrentChunkStreaming() || duration == C.TIME_UNSET || duration <= 0L) {
return super.getBufferedPercentage()
}
val bufferedPosition = getBufferedPosition().coerceIn(0L, duration)
return ((bufferedPosition * 100L) / duration).toInt().coerceIn(0, 100)
}
override fun isCurrentMediaItemDynamic(): Boolean {
return if (isCurrentChunkStreaming()) false else super.isCurrentMediaItemDynamic()
}
private fun notificationDurationMs(): Long {
val currentPositionMs = super.getCurrentPosition().coerceAtLeast(0L)
return currentChunkDurationForNotification(currentPositionMs)
}
private fun adjustedStreamingBufferedPosition(delegatePositionMs: Long): Long {
val duration = notificationDurationMs()
if (!isCurrentChunkStreaming() || duration == C.TIME_UNSET || duration <= 0L) {
return delegatePositionMs
}
val currentPositionMs = super.getCurrentPosition().coerceAtLeast(0L)
val bufferedPositionMs = if (delegatePositionMs == C.TIME_UNSET || delegatePositionMs < currentPositionMs) {
currentPositionMs
} else {
delegatePositionMs
}
return bufferedPositionMs.coerceIn(0L, duration)
}
}
@UnstableApi
class TtsService : MediaSessionService() {
@ -258,6 +486,29 @@ class TtsService : MediaSessionService() {
private var foregroundChapterTitle: String? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_TTS_NOTIFICATION_PREVIOUS_CHUNK -> {
Timber.tag(TTS_NOTIFICATION_DIAG_TAG).i("Notification previous chunk action received.")
Timber.tag(TTS_CHUNK_NAV_DIAG_TAG).i(
"serviceAction=notification-previous hasPlaybackManager=${::playbackManager.isInitialized} playerInitialized=${::player.isInitialized}"
)
if (::playbackManager.isInitialized) {
playbackManager.skipToPreviousChunkFromTransport()
}
return START_STICKY
}
ACTION_TTS_NOTIFICATION_NEXT_CHUNK -> {
Timber.tag(TTS_NOTIFICATION_DIAG_TAG).i("Notification next chunk action received.")
Timber.tag(TTS_CHUNK_NAV_DIAG_TAG).i(
"serviceAction=notification-next hasPlaybackManager=${::playbackManager.isInitialized} playerInitialized=${::player.isInitialized}"
)
if (::playbackManager.isInitialized) {
playbackManager.skipToNextChunkFromTransport()
}
return START_STICKY
}
}
val result = super.onStartCommand(intent, flags, startId)
Timber.tag(TTS_NOTIFICATION_DIAG_TAG).i(
"onStartCommand. action=${intent?.action}, startId=$startId, result=$result"
@ -747,6 +998,7 @@ class TtsService : MediaSessionService() {
override fun onCreate() {
super.onCreate()
Timber.d("TtsService created.")
setMediaNotificationProvider(TtsMediaNotificationProvider(this))
val hasNotificationPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
Timber.tag(TTS_NOTIFICATION_DIAG_TAG).i(
@ -818,7 +1070,17 @@ class TtsService : MediaSessionService() {
onPlaybackSessionStopped = ::onPlaybackSessionStopped
)
mediaSession = MediaSession.Builder(this, player)
val sessionPlayer = TtsSessionPlayer(
player = player,
canSkipToPreviousChunk = playbackManager::canSkipToPreviousChunk,
canSkipToNextChunk = playbackManager::canSkipToNextChunk,
skipToPreviousChunk = playbackManager::skipToPreviousChunkFromTransport,
skipToNextChunk = playbackManager::skipToNextChunkFromTransport,
isCurrentChunkStreaming = playbackManager::isCurrentChunkStreaming,
currentChunkDurationForNotification = playbackManager::currentChunkDurationForNotification
)
mediaSession = MediaSession.Builder(this, sessionPlayer)
.setCallback(playbackManager)
.build()