v1.0.47 (#279)
* Add performance and stylus debugging logs * Refactor and decouple UI models from `MainViewModel` * Refactor library state management and projection logic * Implement desktop shell using Compose Multiplatform * Implement desktop shell using Compose Multiplatform * Implement desktop shell using Compose Multiplatform * Introduce ReaderEngine and enhance EPUB reader features in windows app * Move core paginated reader logic to a Kotlin Multiplatform `shared` module and introduce experimental desktop support. * Implement PDF rendering and text extraction for desktop using Pdfium * Add `NonReaderScreens.kt` and UI dependencies * Refactor and centralize library state management and models to improve cross-platform consistency * Implement JSON persistence for desktop library and enhance library management features including shelf CRUD, tagging, and metadata editing * Implement PDF annotation system and enhanced zoom controls for the desktop viewer * Implement WebView-based EPUB rendering for desktop using CEF and embedded resources * Optimize UI state projection, navigation state handling, and main screen pager performance * Implement Bring Your Own Key (BYOK) support for AI features in OSS version * Support Gemini-based Cloud TTS with BYOK support for OSS builds * Refactor table cell image sizing in `PaginatedReader` and improve `MobiParser` native library loading and error handling. * crash fixes * Enhance navigation stability with lifecycle-aware safety checks and update `navigation-compose` to 2.9.6 * Implement dynamic bottom padding for the page info bar to account for device rounded corners * Implement bidirectional jump history navigation and replace the jump-back pill with a dedicated `PdfJumpHistoryBar` * Optimize PDF tiling performance and refine pan-and-fling gesture handling * Implement customizable toolbars with drag-and-drop reordering and placement for PDF and EPUB readers * Updated UI for customize toolbar * Refine drag-and-drop reordering and section assignment for PDF and EPUB reader controls * restructure PDF viewer UI component hierarchy to fix verifier crash * Implement separate text dimming factors for light and dark themes * Synchronize Pdfium access and improve resource lifecycle safety across Kotlin and native layers * Enhance image alignment in paginated and EPUB readers through anchor detection and style-based positioning * Centralize file type resolution logic and implement HTML sanitization during import * Introduce vertical margin customization and configurable progress bar positioning * texture support in epub reader * Enhance TTS session management, progress tracking, and diagnostic logging * Optimize library state projection and folder synchronization performance by refactoring collection lookups and refining metadata extraction logic. * Refine TTS page mapping for PDF and overhaul TTS control UI * Implement natural session completion logic in `TtsPlaybackManager` for cloud tts * Replace Snackbar with `CustomTopBanner` for notifications in `PdfViewerScreen` * Refine TTS playback continuity across PDF pages and improve state management for session transitions * Implement global texture transparency and enhance textured theme support across PDF and EPUB readers. * Update reader themes and improve texture rendering in page animations, EPUB UI, and immersive mode * Add Support Project screen * Optimize library performance via projection caching, batch database updates, and scoped folder synchronization. * Enhance folder synchronization with fallback query mechanisms and refactor annotation sidecar importing logic * Bump version to 1.0.47 (51)
This commit is contained in:
parent
f42de6b462
commit
d7a9cae9e1
126 changed files with 15287 additions and 3154 deletions
|
|
@ -30,6 +30,9 @@ import androidx.media3.common.util.UnstableApi
|
|||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.MediaSessionService
|
||||
import com.aryan.reader.GEMINI_CLOUD_TTS_MODEL
|
||||
import com.aryan.reader.isByokCloudTtsAvailable
|
||||
import com.aryan.reader.loadAiByokSettings
|
||||
import com.aryan.reader.tts.TtsPlaybackManager.TtsMode
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
|
@ -236,16 +239,31 @@ class TtsService : MediaSessionService() {
|
|||
private lateinit var cacheManager: TtsCacheManager
|
||||
|
||||
override fun onUpdateNotification(session: MediaSession, startInForegroundRequired: Boolean) {
|
||||
val hasNotificationPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
|
||||
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
|
||||
val playerState = if (::player.isInitialized) {
|
||||
"playbackState=${player.playbackState}, isPlaying=${player.isPlaying}, playWhenReady=${player.playWhenReady}, mediaItems=${player.mediaItemCount}, currentIndex=${player.currentMediaItemIndex}"
|
||||
} else {
|
||||
"player=uninitialized"
|
||||
}
|
||||
Timber.tag(TTS_NOTIFICATION_DIAG_TAG).i(
|
||||
"onUpdateNotification called. startInForegroundRequired=$startInForegroundRequired, hasPostNotifications=$hasNotificationPermission, $playerState"
|
||||
)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
||||
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||
|
||||
if (startInForegroundRequired) {
|
||||
Timber.tag(TTS_NOTIFICATION_DIAG_TAG).w("Notification permission missing while foreground is required. Calling stopSelf().")
|
||||
stopSelf()
|
||||
} else {
|
||||
Timber.tag(TTS_NOTIFICATION_DIAG_TAG).w("Notification permission missing. Skipping notification update.")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
Timber.tag(TTS_NOTIFICATION_DIAG_TAG).i("Delegating notification update to MediaSessionService.")
|
||||
super.onUpdateNotification(session, startInForegroundRequired)
|
||||
}
|
||||
|
||||
|
|
@ -279,7 +297,12 @@ class TtsService : MediaSessionService() {
|
|||
data class Error(val message: String) : GeminiWsEvent()
|
||||
}
|
||||
|
||||
suspend fun ensureConnected(serverUrl: String, speaker: String, authToken: String?) = connectionMutex.withLock {
|
||||
suspend fun ensureConnected(
|
||||
serverUrl: String,
|
||||
speaker: String,
|
||||
authToken: String?,
|
||||
directGeminiApiKey: String? = null
|
||||
) = connectionMutex.withLock {
|
||||
if (webSocket != null) {
|
||||
if (connectedSpeaker == speaker) {
|
||||
val isSetup = try { setupDeferred.await() } catch(_: Exception) { false }
|
||||
|
|
@ -290,11 +313,15 @@ class TtsService : MediaSessionService() {
|
|||
webSocket = null
|
||||
}
|
||||
|
||||
val sanitizedUrl = serverUrl.removeSuffix("/")
|
||||
val wsUrlStr = sanitizedUrl.replace("https://", "wss://").replace("http://", "ws://")
|
||||
val url = "$wsUrlStr/live?speaker=$speaker&token=${authToken ?: ""}"
|
||||
val url = if (!directGeminiApiKey.isNullOrBlank()) {
|
||||
"wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent?key=$directGeminiApiKey"
|
||||
} else {
|
||||
val sanitizedUrl = serverUrl.removeSuffix("/")
|
||||
val wsUrlStr = sanitizedUrl.replace("https://", "wss://").replace("http://", "ws://")
|
||||
"$wsUrlStr/live?speaker=$speaker&token=${authToken ?: ""}"
|
||||
}
|
||||
|
||||
Timber.tag("TTS_CLOUD_DIAG").d("Connecting to WS: $url")
|
||||
Timber.tag("TTS_CLOUD_DIAG").d("Connecting to WS: ${if (!directGeminiApiKey.isNullOrBlank()) "Gemini BYOK" else url}")
|
||||
val request = Request.Builder().url(url).build()
|
||||
val connectedDeferred = CompletableDeferred<Boolean>()
|
||||
|
||||
|
|
@ -316,7 +343,7 @@ class TtsService : MediaSessionService() {
|
|||
|
||||
val setupMsg = JSONObject().apply {
|
||||
put("setup", JSONObject().apply {
|
||||
put("model", "models/gemini-3.1-flash-live-preview")
|
||||
put("model", "models/$GEMINI_CLOUD_TTS_MODEL")
|
||||
put("systemInstruction", JSONObject().apply {
|
||||
put("parts", org.json.JSONArray().apply {
|
||||
put(JSONObject().apply {
|
||||
|
|
@ -552,8 +579,17 @@ class TtsService : MediaSessionService() {
|
|||
TtsAudioData(audioFile = cachedFile, serverText = text, wordTimings = emptyList(), error = null, streamUri = null)
|
||||
} else {
|
||||
try {
|
||||
liveClient.ensureConnected(googleCloudWorkerTtsUrl, speaker, authToken)
|
||||
liveClient.generateChunk(text, cachedFile)
|
||||
val directGeminiApiKey = if (isByokCloudTtsAvailable(this@TtsService)) {
|
||||
loadAiByokSettings(this@TtsService).geminiKey
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (directGeminiApiKey.isNullOrBlank() && googleCloudWorkerTtsUrl.isBlank()) {
|
||||
TtsAudioData(audioFile = null, serverText = null, wordTimings = null, error = "Cloud TTS is not configured.")
|
||||
} else {
|
||||
liveClient.ensureConnected(googleCloudWorkerTtsUrl, speaker, authToken, directGeminiApiKey)
|
||||
liveClient.generateChunk(text, cachedFile)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.tag("TTS_CLOUD_DIAG").e(e, "Cloud TTS generation failed")
|
||||
TtsAudioData(audioFile = null, serverText = null, wordTimings = null, error = e.message ?: "Failed to connect to TTS service")
|
||||
|
|
@ -567,6 +603,11 @@ class TtsService : MediaSessionService() {
|
|||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Timber.d("TtsService created.")
|
||||
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(
|
||||
"TtsService onCreate. sdk=${Build.VERSION.SDK_INT}, hasPostNotifications=$hasNotificationPermission"
|
||||
)
|
||||
|
||||
cacheManager = TtsCacheManager(this)
|
||||
|
||||
|
|
@ -622,6 +663,7 @@ class TtsService : MediaSessionService() {
|
|||
.setHandleAudioBecomingNoisy(true)
|
||||
.setMediaSourceFactory(androidx.media3.exoplayer.source.DefaultMediaSourceFactory(this).setDataSourceFactory(dataSourceFactory))
|
||||
.build()
|
||||
Timber.tag(TTS_NOTIFICATION_DIAG_TAG).i("ExoPlayer created for TTS service.")
|
||||
|
||||
playbackManager = TtsPlaybackManager(
|
||||
player = player,
|
||||
|
|
@ -634,21 +676,30 @@ class TtsService : MediaSessionService() {
|
|||
.build()
|
||||
|
||||
mediaSession?.let { playbackManager.setMediaSession(it) }
|
||||
Timber.tag(TTS_NOTIFICATION_DIAG_TAG).i("MediaSession created and attached to playback manager. sessionAvailable=${mediaSession != null}")
|
||||
}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
Timber.tag(TTS_NOTIFICATION_DIAG_TAG).i(
|
||||
"onTaskRemoved. playWhenReady=${if (::player.isInitialized) player.playWhenReady else null}, isPlaying=${if (::player.isInitialized) player.isPlaying else null}"
|
||||
)
|
||||
if (!player.playWhenReady) {
|
||||
Timber.tag(TTS_NOTIFICATION_DIAG_TAG).w("Task removed while player is not playWhenReady. Calling stopSelf().")
|
||||
stopSelf()
|
||||
}
|
||||
Timber.d("onTaskRemoved called, stopping service.")
|
||||
}
|
||||
|
||||
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
|
||||
Timber.tag(TTS_NOTIFICATION_DIAG_TAG).i(
|
||||
"onGetSession. package=${controllerInfo.packageName}, sessionAvailable=${mediaSession != null}"
|
||||
)
|
||||
return mediaSession
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Timber.d("TtsService is being destroyed.")
|
||||
Timber.tag(TTS_NOTIFICATION_DIAG_TAG).w("TtsService onDestroy.")
|
||||
baseTtsSynthesizer.shutdown()
|
||||
playbackManager.release()
|
||||
mediaSession?.run {
|
||||
|
|
@ -658,4 +709,4 @@ class TtsService : MediaSessionService() {
|
|||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue