From 8f0ed6215f8ece5b7efb8fc04a7d0aa5d12af4f8 Mon Sep 17 00:00:00 2001 From: Bruno Abe Date: Sun, 24 Aug 2025 12:08:49 -0300 Subject: [PATCH 1/5] Create main view model --- .../mobyle/abbay/presentation/MainActivity.kt | 48 +++++++++++++- .../abbay/presentation/MainViewModel.kt | 62 +++++++++++++++++++ .../presentation/booklist/BooksListScreen.kt | 17 ----- .../booklist/BooksListViewModel.kt | 2 +- 4 files changed, 110 insertions(+), 19 deletions(-) create mode 100644 app/src/main/java/com/mobyle/abbay/presentation/MainViewModel.kt diff --git a/app/src/main/java/com/mobyle/abbay/presentation/MainActivity.kt b/app/src/main/java/com/mobyle/abbay/presentation/MainActivity.kt index d00fb93..26fbcb5 100644 --- a/app/src/main/java/com/mobyle/abbay/presentation/MainActivity.kt +++ b/app/src/main/java/com/mobyle/abbay/presentation/MainActivity.kt @@ -4,15 +4,19 @@ import android.content.ComponentName import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.viewModels import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.media3.common.Player import androidx.media3.session.MediaController import androidx.media3.session.SessionToken import androidx.navigation.compose.rememberNavController @@ -22,9 +26,13 @@ import com.google.common.util.concurrent.MoreExecutors import com.mobyle.abbay.infra.navigation.AbbayNavHost import com.mobyle.abbay.presentation.common.service.PlayerService import com.mobyle.abbay.presentation.common.theme.MyApplicationTheme +import com.mobyle.abbay.presentation.utils.prepareBook +import com.mobyle.abbay.presentation.utils.prepareMultipleBooks +import com.model.MultipleBooks import com.usecase.IsPlayWhenAppIsClosedEnabled import com.usecase.UpdateAppLifeStatus import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.MutableStateFlow import javax.inject.Inject @ExperimentalMaterialApi @@ -37,6 +45,8 @@ class MainActivity : ComponentActivity() { @Inject lateinit var updateAppLifeStatus: UpdateAppLifeStatus + private val viewModel: MainViewModel by viewModels() + private lateinit var controller: ListenableFuture private var isPlayerReady = false @@ -62,12 +72,12 @@ class MainActivity : ComponentActivity() { val currentPlayer = remember { mutableStateOf(null) } + val selectedBook by viewModel.selectedBook.collectAsState() LaunchedEffect(Unit) { controller.addListener({ try { currentPlayer.value = controller.get() - isPlayerReady = true // This will dismiss the splash screen } catch (e: Exception) { // Handle error but still dismiss splash screen isPlayerReady = true @@ -75,6 +85,42 @@ class MainActivity : ComponentActivity() { }, MoreExecutors.directExecutor()) } + LaunchedEffect(selectedBook, currentPlayer.value) { + (selectedBook as? BookSelectionState.Selected)?.let { + currentPlayer.value?.let { player -> + val book = it.book + if (!player.isPlaying) { + if (book is MultipleBooks) { + player.prepareMultipleBooks( + currentPosition = book.currentBookPosition, + idList = book.bookFileList.map { it.id }, + progress = book.progress, + isPlaying = MutableStateFlow(false) + ) + } else { + player.prepareBook( + id = book.id, + progress = book.progress, + isPlaying = MutableStateFlow(false) + ) + } + } + } + } + } + + LaunchedEffect(currentPlayer.value) { + currentPlayer.value?.addListener( + object : Player.Listener { + override fun onPlaybackStateChanged(state: Int) { + if (state == Player.STATE_READY) { + isPlayerReady = true + } + } + } + ) + } + MyApplicationTheme { // A surface container using the 'background' color from the theme Surface( diff --git a/app/src/main/java/com/mobyle/abbay/presentation/MainViewModel.kt b/app/src/main/java/com/mobyle/abbay/presentation/MainViewModel.kt new file mode 100644 index 0000000..09129aa --- /dev/null +++ b/app/src/main/java/com/mobyle/abbay/presentation/MainViewModel.kt @@ -0,0 +1,62 @@ +package com.mobyle.abbay.presentation + +import android.Manifest +import android.os.Build +import androidx.lifecycle.viewModelScope +import com.mobyle.abbay.infra.common.BaseViewModel +import com.mobyle.abbay.presentation.utils.permissions.CheckPermissionsProvider +import com.model.Book +import com.usecase.GetCurrentSelectedBook +import com.usecase.ObserveBooksList +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.take +import javax.inject.Inject + +@HiltViewModel +class MainViewModel @Inject constructor( + observeBooksList: ObserveBooksList, + val getCurrentSelectedBook: GetCurrentSelectedBook, + checkPermissionsProvider: CheckPermissionsProvider, +) : BaseViewModel() { + private val hasPermissions = + MutableStateFlow(checkPermissionsProvider.areAllPermissionsGranted(getPermissionsList())) + + val selectedBook = MutableStateFlow(BookSelectionState.Idle) + + init { + observeBooksList() + .take(1) + .onEach { + if (hasPermissions.value) { + it.firstOrNull { + it.id == getCurrentSelectedBook() + }?.let { + selectedBook.tryEmit(BookSelectionState.Selected(it)) + } ?: kotlin.run { + selectedBook.tryEmit(BookSelectionState.None) + } + } + } + .launchIn(viewModelScope) + } + + private fun getPermissionsList() = if (Build.VERSION.SDK_INT >= 33) { + PERMISSIONS_LIST + } else { + API_32_OR_LESS_PERMISSIONS_LIST + } + + private companion object { + val API_32_OR_LESS_PERMISSIONS_LIST = listOf(Manifest.permission.READ_EXTERNAL_STORAGE) + val PERMISSIONS_LIST = listOf(Manifest.permission.READ_MEDIA_AUDIO) + } +} + +sealed class BookSelectionState { + data object None : BookSelectionState() + data class Selected(val book: Book) : BookSelectionState() + data object Idle : BookSelectionState() +} diff --git a/app/src/main/java/com/mobyle/abbay/presentation/booklist/BooksListScreen.kt b/app/src/main/java/com/mobyle/abbay/presentation/booklist/BooksListScreen.kt index 8b02220..fd4eef7 100644 --- a/app/src/main/java/com/mobyle/abbay/presentation/booklist/BooksListScreen.kt +++ b/app/src/main/java/com/mobyle/abbay/presentation/booklist/BooksListScreen.kt @@ -462,23 +462,6 @@ fun BooksListScreen( progress = book.progress, currentPosition = book.getBookPosition() ) - - if (!player.isPlaying) { - if (book is MultipleBooks) { - player.prepareMultipleBooks( - currentPosition = book.currentBookPosition, - idList = book.bookFileList.map { it.id }, - progress = book.progress, - isPlaying = viewModel.isPlaying - ) - } else { - player.prepareBook( - id = id, - progress = book.progress, - isPlaying = viewModel.isPlaying - ) - } - } } } } diff --git a/app/src/main/java/com/mobyle/abbay/presentation/booklist/BooksListViewModel.kt b/app/src/main/java/com/mobyle/abbay/presentation/booklist/BooksListViewModel.kt index 461e4fb..718487d 100644 --- a/app/src/main/java/com/mobyle/abbay/presentation/booklist/BooksListViewModel.kt +++ b/app/src/main/java/com/mobyle/abbay/presentation/booklist/BooksListViewModel.kt @@ -349,7 +349,7 @@ class BooksListViewModel @Inject constructor( data object NoPermissionsGranted : BooksListUiState() } - companion object { + private companion object { val API_32_OR_LESS_PERMISSIONS_LIST = listOf(Manifest.permission.READ_EXTERNAL_STORAGE) val PERMISSIONS_LIST = listOf(Manifest.permission.READ_MEDIA_AUDIO) } From cd821010cd43629a86aa6eda20d32b148abf02b7 Mon Sep 17 00:00:00 2001 From: Bruno Abe Date: Sun, 24 Aug 2025 12:44:22 -0300 Subject: [PATCH 2/5] Add player on start up --- STARTUP_OPTIMIZATION_README.md | 72 +++++++++++++++++++ app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 10 +++ .../abbay/infra/startup/PlayerInitializer.kt | 53 ++++++++++++++ .../mobyle/abbay/presentation/MainActivity.kt | 56 ++------------- .../presentation/booklist/BooksListScreen.kt | 17 +++++ gradle/libs.versions.toml | 4 +- 7 files changed, 161 insertions(+), 52 deletions(-) create mode 100644 STARTUP_OPTIMIZATION_README.md create mode 100644 app/src/main/java/com/mobyle/abbay/infra/startup/PlayerInitializer.kt diff --git a/STARTUP_OPTIMIZATION_README.md b/STARTUP_OPTIMIZATION_README.md new file mode 100644 index 0000000..7eaf36f --- /dev/null +++ b/STARTUP_OPTIMIZATION_README.md @@ -0,0 +1,72 @@ +# App Startup Optimization for Player + +## Overview + +This implementation uses Android's App Startup library to initialize the MediaController during app startup, significantly reducing the time it takes for the player to be ready when the user opens the app. + +## What Was Changed + +### 1. Dependencies Added +- Added `androidx.startup:startup-runtime:1.1.1` to the project dependencies +- Updated `gradle/libs.versions.toml` to include the startup library + +### 2. PlayerInitializer Class +- Created `app/src/main/java/com/mobyle/abbay/infra/startup/PlayerInitializer.kt` +- Implements `Initializer>` +- Pre-warms the MediaController during app startup in the background +- Handles errors gracefully during startup + +### 3. AndroidManifest.xml Updates +- Added the `InitializationProvider` with metadata for `PlayerInitializer` +- This ensures the player is initialized when the app starts + +### 4. MainActivity Refactoring +- Removed manual MediaController creation +- Now uses the pre-initialized player from App Startup +- Added better error handling and logging +- Improved splash screen management + +## How It Works + +1. **App Startup**: When the app launches, the `PlayerInitializer` runs automatically +2. **Background Initialization**: The MediaController is created and prepared in the background +3. **Pre-warming**: The controller is "warmed up" so it's ready to use immediately +4. **MainActivity**: When MainActivity starts, it gets the already-initialized controller +5. **Faster Player Ready**: The player is ready much faster, reducing splash screen time + +## Benefits + +- **Faster Startup**: Player is ready immediately when MainActivity loads +- **Better UX**: Reduced splash screen time and faster app responsiveness +- **Background Processing**: Player initialization doesn't block the UI thread +- **Error Resilience**: Better error handling and fallback mechanisms +- **Resource Management**: More efficient resource usage during startup + +## Performance Impact + +- **Before**: Player initialization happened in MainActivity, blocking UI +- **After**: Player initialization happens during app startup, in parallel +- **Result**: Significantly faster player readiness and better user experience + +## Technical Details + +- Uses `ListenableFuture` for asynchronous initialization +- Implements proper error handling and logging +- Maintains backward compatibility +- Follows Android best practices for startup optimization + +## Troubleshooting + +If you encounter issues: + +1. Check logcat for "PlayerInitializer" tags +2. Verify the manifest has the correct provider configuration +3. Ensure all dependencies are properly synced +4. Check that the PlayerService is properly configured + +## Future Enhancements + +- Add startup performance metrics +- Implement startup time monitoring +- Add configuration options for different startup strategies +- Consider lazy initialization for other heavy components diff --git a/app/build.gradle b/app/build.gradle index a9cab75..9244ed6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -62,6 +62,7 @@ dependencies { implementation libs.androidx.core.ktx implementation libs.bundles.lifecycle implementation libs.androidx.activity.compose + implementation libs.androidx.startup // Compose BOM and UI implementation platform(libs.androidx.compose.bom) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1fcf015..d38a120 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -38,6 +38,16 @@ + + + + \ No newline at end of file diff --git a/app/src/main/java/com/mobyle/abbay/infra/startup/PlayerInitializer.kt b/app/src/main/java/com/mobyle/abbay/infra/startup/PlayerInitializer.kt new file mode 100644 index 0000000..9ddd528 --- /dev/null +++ b/app/src/main/java/com/mobyle/abbay/infra/startup/PlayerInitializer.kt @@ -0,0 +1,53 @@ +package com.mobyle.abbay.infra.startup + +import android.content.ComponentName +import android.content.Context +import android.util.Log +import androidx.startup.Initializer +import androidx.media3.session.MediaController +import androidx.media3.session.SessionToken +import com.google.common.util.concurrent.ListenableFuture +import com.google.common.util.concurrent.MoreExecutors +import com.mobyle.abbay.presentation.common.service.PlayerService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch + +class PlayerInitializer : Initializer> { + + companion object { + private const val TAG = "PlayerInitializer" + } + + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + override fun create(context: Context): ListenableFuture { + Log.d(TAG, "Initializing MediaController during app startup") + + val controller = MediaController.Builder( + context, + SessionToken(context, ComponentName(context, PlayerService::class.java)) + ).buildAsync() + + // Pre-warm the controller in background to reduce startup time + scope.launch { + try { + // Wait for the controller to be ready + controller.get() + Log.d(TAG, "MediaController initialized successfully during startup") + // The controller is now ready and can be used immediately + } catch (e: Exception) { + Log.w(TAG, "Failed to initialize MediaController during startup: ${e.message}") + // Handle error silently during startup + // The error will be handled when the controller is actually used + } + } + + return controller + } + + override fun dependencies(): List>> { + return emptyList() + } +} diff --git a/app/src/main/java/com/mobyle/abbay/presentation/MainActivity.kt b/app/src/main/java/com/mobyle/abbay/presentation/MainActivity.kt index 26fbcb5..2a64395 100644 --- a/app/src/main/java/com/mobyle/abbay/presentation/MainActivity.kt +++ b/app/src/main/java/com/mobyle/abbay/presentation/MainActivity.kt @@ -4,35 +4,29 @@ import android.content.ComponentName import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.activity.viewModels import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import androidx.media3.common.Player import androidx.media3.session.MediaController import androidx.media3.session.SessionToken import androidx.navigation.compose.rememberNavController +import androidx.startup.AppInitializer import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.MoreExecutors import com.mobyle.abbay.infra.navigation.AbbayNavHost +import com.mobyle.abbay.infra.startup.PlayerInitializer import com.mobyle.abbay.presentation.common.service.PlayerService import com.mobyle.abbay.presentation.common.theme.MyApplicationTheme -import com.mobyle.abbay.presentation.utils.prepareBook -import com.mobyle.abbay.presentation.utils.prepareMultipleBooks -import com.model.MultipleBooks import com.usecase.IsPlayWhenAppIsClosedEnabled import com.usecase.UpdateAppLifeStatus import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.MutableStateFlow import javax.inject.Inject @ExperimentalMaterialApi @@ -45,8 +39,6 @@ class MainActivity : ComponentActivity() { @Inject lateinit var updateAppLifeStatus: UpdateAppLifeStatus - private val viewModel: MainViewModel by viewModels() - private lateinit var controller: ListenableFuture private var isPlayerReady = false @@ -62,22 +54,20 @@ class MainActivity : ComponentActivity() { updateAppLifeStatus(true) - controller = MediaController.Builder( - this, - SessionToken(this, ComponentName(this, PlayerService::class.java)) - ).buildAsync() + controller = AppInitializer.getInstance(this@MainActivity) + .initializeComponent(PlayerInitializer::class.java) setContent { val navController = rememberNavController() val currentPlayer = remember { mutableStateOf(null) } - val selectedBook by viewModel.selectedBook.collectAsState() LaunchedEffect(Unit) { controller.addListener({ try { currentPlayer.value = controller.get() + isPlayerReady = true // This will dismiss the splash screen } catch (e: Exception) { // Handle error but still dismiss splash screen isPlayerReady = true @@ -85,42 +75,6 @@ class MainActivity : ComponentActivity() { }, MoreExecutors.directExecutor()) } - LaunchedEffect(selectedBook, currentPlayer.value) { - (selectedBook as? BookSelectionState.Selected)?.let { - currentPlayer.value?.let { player -> - val book = it.book - if (!player.isPlaying) { - if (book is MultipleBooks) { - player.prepareMultipleBooks( - currentPosition = book.currentBookPosition, - idList = book.bookFileList.map { it.id }, - progress = book.progress, - isPlaying = MutableStateFlow(false) - ) - } else { - player.prepareBook( - id = book.id, - progress = book.progress, - isPlaying = MutableStateFlow(false) - ) - } - } - } - } - } - - LaunchedEffect(currentPlayer.value) { - currentPlayer.value?.addListener( - object : Player.Listener { - override fun onPlaybackStateChanged(state: Int) { - if (state == Player.STATE_READY) { - isPlayerReady = true - } - } - } - ) - } - MyApplicationTheme { // A surface container using the 'background' color from the theme Surface( diff --git a/app/src/main/java/com/mobyle/abbay/presentation/booklist/BooksListScreen.kt b/app/src/main/java/com/mobyle/abbay/presentation/booklist/BooksListScreen.kt index fd4eef7..8b02220 100644 --- a/app/src/main/java/com/mobyle/abbay/presentation/booklist/BooksListScreen.kt +++ b/app/src/main/java/com/mobyle/abbay/presentation/booklist/BooksListScreen.kt @@ -462,6 +462,23 @@ fun BooksListScreen( progress = book.progress, currentPosition = book.getBookPosition() ) + + if (!player.isPlaying) { + if (book is MultipleBooks) { + player.prepareMultipleBooks( + currentPosition = book.currentBookPosition, + idList = book.bookFileList.map { it.id }, + progress = book.progress, + isPlaying = viewModel.isPlaying + ) + } else { + player.prepareBook( + id = id, + progress = book.progress, + isPlaying = viewModel.isPlaying + ) + } + } } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 889d6df..fd6743d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -73,6 +73,7 @@ androidx-activity-compose = { group = "androidx.activity", name = "activity-comp androidx-documentfile = { group = "androidx.documentfile", name = "documentfile", version.ref = "documentfile" } androidx-splashscreen = { module = "androidx.core:core-splashscreen", version = "1.0.1" } androidx-workManager = { module = "androidx.work:work-runtime-ktx", version = "2.10.0" } +androidx-startup = { group = "androidx.startup", name = "startup-runtime", version = "1.1.1" } # Compose BOM and UI androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" } @@ -190,7 +191,8 @@ hilt-kapt = [ lifecycle = [ "androidx-lifecycle-runtime-ktx", - "androidx-lifecycle-runtime-compose" + "androidx-lifecycle-runtime-compose", + "androidx-startup" ] coil = [ From 9ec3758876ca59db1147f1d743c98aa4a2a99239 Mon Sep 17 00:00:00 2001 From: Bruno Abe Date: Sun, 24 Aug 2025 17:20:39 -0300 Subject: [PATCH 3/5] Add Player initializer --- .../abbay/infra/startup/PlayerInitializer.kt | 52 +++++----------- .../mobyle/abbay/presentation/MainActivity.kt | 41 ++++-------- .../abbay/presentation/MainViewModel.kt | 62 ------------------- .../presentation/booklist/BooksListScreen.kt | 17 ----- .../booklist/widgets/MiniPlayer.kt | 36 ++++------- .../abbay/presentation/utils/Extensions.kt | 4 +- 6 files changed, 40 insertions(+), 172 deletions(-) delete mode 100644 app/src/main/java/com/mobyle/abbay/presentation/MainViewModel.kt diff --git a/app/src/main/java/com/mobyle/abbay/infra/startup/PlayerInitializer.kt b/app/src/main/java/com/mobyle/abbay/infra/startup/PlayerInitializer.kt index 9ddd528..dc48c23 100644 --- a/app/src/main/java/com/mobyle/abbay/infra/startup/PlayerInitializer.kt +++ b/app/src/main/java/com/mobyle/abbay/infra/startup/PlayerInitializer.kt @@ -2,51 +2,29 @@ package com.mobyle.abbay.infra.startup import android.content.ComponentName import android.content.Context -import android.util.Log -import androidx.startup.Initializer import androidx.media3.session.MediaController import androidx.media3.session.SessionToken -import com.google.common.util.concurrent.ListenableFuture -import com.google.common.util.concurrent.MoreExecutors +import androidx.startup.Initializer import com.mobyle.abbay.presentation.common.service.PlayerService -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn -class PlayerInitializer : Initializer> { - - companion object { - private const val TAG = "PlayerInitializer" - } - - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - - override fun create(context: Context): ListenableFuture { - Log.d(TAG, "Initializing MediaController during app startup") - - val controller = MediaController.Builder( - context, - SessionToken(context, ComponentName(context, PlayerService::class.java)) - ).buildAsync() +class PlayerInitializer : Initializer> { + override fun create(context: Context): Flow { + return flow { + val controller = MediaController.Builder( + context, + SessionToken(context, ComponentName(context, PlayerService::class.java)) + ).buildAsync() - // Pre-warm the controller in background to reduce startup time - scope.launch { - try { - // Wait for the controller to be ready - controller.get() - Log.d(TAG, "MediaController initialized successfully during startup") - // The controller is now ready and can be used immediately - } catch (e: Exception) { - Log.w(TAG, "Failed to initialize MediaController during startup: ${e.message}") - // Handle error silently during startup - // The error will be handled when the controller is actually used - } - } + val player = controller.get() - return controller + emit(player) + }.flowOn(Dispatchers.IO) } - + override fun dependencies(): List>> { return emptyList() } diff --git a/app/src/main/java/com/mobyle/abbay/presentation/MainActivity.kt b/app/src/main/java/com/mobyle/abbay/presentation/MainActivity.kt index 2a64395..a3622f0 100644 --- a/app/src/main/java/com/mobyle/abbay/presentation/MainActivity.kt +++ b/app/src/main/java/com/mobyle/abbay/presentation/MainActivity.kt @@ -1,6 +1,5 @@ package com.mobyle.abbay.presentation -import android.content.ComponentName import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -9,20 +8,16 @@ import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.media3.session.MediaController -import androidx.media3.session.SessionToken import androidx.navigation.compose.rememberNavController import androidx.startup.AppInitializer import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.common.util.concurrent.ListenableFuture -import com.google.common.util.concurrent.MoreExecutors import com.mobyle.abbay.infra.navigation.AbbayNavHost import com.mobyle.abbay.infra.startup.PlayerInitializer -import com.mobyle.abbay.presentation.common.service.PlayerService import com.mobyle.abbay.presentation.common.theme.MyApplicationTheme import com.usecase.IsPlayWhenAppIsClosedEnabled import com.usecase.UpdateAppLifeStatus @@ -39,7 +34,7 @@ class MainActivity : ComponentActivity() { @Inject lateinit var updateAppLifeStatus: UpdateAppLifeStatus - private lateinit var controller: ListenableFuture + private var player: MediaController? = null private var isPlayerReady = false override fun onCreate(savedInstanceState: Bundle?) { @@ -47,41 +42,29 @@ class MainActivity : ComponentActivity() { // Keep splash screen visible until player is ready splashScreen.setKeepOnScreenCondition { - !isPlayerReady + !(isPlayerReady) } super.onCreate(savedInstanceState) updateAppLifeStatus(true) - controller = AppInitializer.getInstance(this@MainActivity) - .initializeComponent(PlayerInitializer::class.java) - setContent { val navController = rememberNavController() - val currentPlayer = remember { - mutableStateOf(null) - } + val playerState = AppInitializer.getInstance(this@MainActivity) + .initializeComponent(PlayerInitializer::class.java).collectAsState(null) - LaunchedEffect(Unit) { - controller.addListener({ - try { - currentPlayer.value = controller.get() - isPlayerReady = true // This will dismiss the splash screen - } catch (e: Exception) { - // Handle error but still dismiss splash screen - isPlayerReady = true - } - }, MoreExecutors.directExecutor()) + LaunchedEffect(playerState.value) { + player = playerState.value + isPlayerReady = playerState.value != null } MyApplicationTheme { - // A surface container using the 'background' color from the theme Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.primary ) { - currentPlayer.value?.let { player -> + playerState.value?.let { player -> AbbayNavHost( player = player, navController = navController @@ -98,9 +81,9 @@ class MainActivity : ComponentActivity() { updateAppLifeStatus(false) if (!isPlayWhenAppIsClosedEnabled()) { - if (::controller.isInitialized) { - controller.get().stop() - controller.get().release() + if (player != null) { + player?.stop() + player?.release() } } } diff --git a/app/src/main/java/com/mobyle/abbay/presentation/MainViewModel.kt b/app/src/main/java/com/mobyle/abbay/presentation/MainViewModel.kt deleted file mode 100644 index 09129aa..0000000 --- a/app/src/main/java/com/mobyle/abbay/presentation/MainViewModel.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.mobyle.abbay.presentation - -import android.Manifest -import android.os.Build -import androidx.lifecycle.viewModelScope -import com.mobyle.abbay.infra.common.BaseViewModel -import com.mobyle.abbay.presentation.utils.permissions.CheckPermissionsProvider -import com.model.Book -import com.usecase.GetCurrentSelectedBook -import com.usecase.ObserveBooksList -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.take -import javax.inject.Inject - -@HiltViewModel -class MainViewModel @Inject constructor( - observeBooksList: ObserveBooksList, - val getCurrentSelectedBook: GetCurrentSelectedBook, - checkPermissionsProvider: CheckPermissionsProvider, -) : BaseViewModel() { - private val hasPermissions = - MutableStateFlow(checkPermissionsProvider.areAllPermissionsGranted(getPermissionsList())) - - val selectedBook = MutableStateFlow(BookSelectionState.Idle) - - init { - observeBooksList() - .take(1) - .onEach { - if (hasPermissions.value) { - it.firstOrNull { - it.id == getCurrentSelectedBook() - }?.let { - selectedBook.tryEmit(BookSelectionState.Selected(it)) - } ?: kotlin.run { - selectedBook.tryEmit(BookSelectionState.None) - } - } - } - .launchIn(viewModelScope) - } - - private fun getPermissionsList() = if (Build.VERSION.SDK_INT >= 33) { - PERMISSIONS_LIST - } else { - API_32_OR_LESS_PERMISSIONS_LIST - } - - private companion object { - val API_32_OR_LESS_PERMISSIONS_LIST = listOf(Manifest.permission.READ_EXTERNAL_STORAGE) - val PERMISSIONS_LIST = listOf(Manifest.permission.READ_MEDIA_AUDIO) - } -} - -sealed class BookSelectionState { - data object None : BookSelectionState() - data class Selected(val book: Book) : BookSelectionState() - data object Idle : BookSelectionState() -} diff --git a/app/src/main/java/com/mobyle/abbay/presentation/booklist/BooksListScreen.kt b/app/src/main/java/com/mobyle/abbay/presentation/booklist/BooksListScreen.kt index 8b02220..fd4eef7 100644 --- a/app/src/main/java/com/mobyle/abbay/presentation/booklist/BooksListScreen.kt +++ b/app/src/main/java/com/mobyle/abbay/presentation/booklist/BooksListScreen.kt @@ -462,23 +462,6 @@ fun BooksListScreen( progress = book.progress, currentPosition = book.getBookPosition() ) - - if (!player.isPlaying) { - if (book is MultipleBooks) { - player.prepareMultipleBooks( - currentPosition = book.currentBookPosition, - idList = book.bookFileList.map { it.id }, - progress = book.progress, - isPlaying = viewModel.isPlaying - ) - } else { - player.prepareBook( - id = id, - progress = book.progress, - isPlaying = viewModel.isPlaying - ) - } - } } } } diff --git a/app/src/main/java/com/mobyle/abbay/presentation/booklist/widgets/MiniPlayer.kt b/app/src/main/java/com/mobyle/abbay/presentation/booklist/widgets/MiniPlayer.kt index 4c58411..4a9ac9e 100644 --- a/app/src/main/java/com/mobyle/abbay/presentation/booklist/widgets/MiniPlayer.kt +++ b/app/src/main/java/com/mobyle/abbay/presentation/booklist/widgets/MiniPlayer.kt @@ -647,26 +647,17 @@ private fun PlayerController( onPlayingChange(false) player.pause() } else { - if (player.playbackState == Player.STATE_READY) { - player.seekTo(position) - onPlayingChange(true) - player.play() - } else { - playerIcon.value = PlayingState.LOADING - if (!player.playWhenReady) { - player.prepareBook(id, position, MutableStateFlow(true)) - } - player.addListener(object : Player.Listener { - override fun onPlaybackStateChanged(state: Int) { - if (state == Player.STATE_READY) { - player.seekTo(position) - onPlayingChange(true) - player.play() - player.removeListener(this) - } + player.prepareBook(id, position) + player.addListener(object : Player.Listener { + override fun onPlaybackStateChanged(state: Int) { + if (state == Player.STATE_READY) { + player.seekTo(position) + onPlayingChange(true) + player.play() + player.removeListener(this) } - }) - } + } + }) } } } @@ -860,12 +851,7 @@ private fun BookImage( onPlayingChange(true) player.play() } else { - playerIcon.value = PlayingState.LOADING - - if (!player.playWhenReady) { - player.prepareBook(book.id, progress, MutableStateFlow(true)) - } - + player.prepareBook(book.id, progress, MutableStateFlow(true)) player.addListener(object : Player.Listener { override fun onPlaybackStateChanged(state: Int) { if (state == Player.STATE_READY) { diff --git a/app/src/main/java/com/mobyle/abbay/presentation/utils/Extensions.kt b/app/src/main/java/com/mobyle/abbay/presentation/utils/Extensions.kt index a262c9d..02c5dc1 100644 --- a/app/src/main/java/com/mobyle/abbay/presentation/utils/Extensions.kt +++ b/app/src/main/java/com/mobyle/abbay/presentation/utils/Extensions.kt @@ -162,9 +162,9 @@ fun MediaController.playMultipleBooks( fun MediaController.prepareBook( id: String, progress: Long, - isPlaying: MutableStateFlow, + isPlaying: MutableStateFlow? = null, ) { - isPlaying.value = false + isPlaying?.value = false pause() clearMediaItems() val uri = From b81d9761121ef7a23be12423eca713598d202705 Mon Sep 17 00:00:00 2001 From: Bruno Abe Date: Sun, 24 Aug 2025 17:23:00 -0300 Subject: [PATCH 4/5] Fix PR comments --- LOCALIZATION_README.md | 175 ------------------ STARTUP_OPTIMIZATION_README.md | 72 ------- .../mobyle/abbay/presentation/MainActivity.kt | 3 +- 3 files changed, 1 insertion(+), 249 deletions(-) delete mode 100644 LOCALIZATION_README.md delete mode 100644 STARTUP_OPTIMIZATION_README.md diff --git a/LOCALIZATION_README.md b/LOCALIZATION_README.md deleted file mode 100644 index f330f5f..0000000 --- a/LOCALIZATION_README.md +++ /dev/null @@ -1,175 +0,0 @@ -# Multi-Language Localization Setup - -This project now supports multiple languages: English, Portuguese, and Spanish. - -## 📁 File Structure - -``` -app/src/main/res/ -├── values/ -│ └── strings.xml # English strings (default) -├── values-pt/ -│ └── strings.xml # Portuguese strings -└── values-es/ - └── strings.xml # Spanish strings -``` - -## 🌐 How It Works - -### Automatic Language Detection -- The app automatically detects the device's language setting -- If the device is set to Portuguese, it will use `values-pt/strings.xml` -- If the device is set to Spanish, it will use `values-es/strings.xml` -- If the device is set to any other language, it will use `values/strings.xml` (English) - -### String Resources -All user-facing text in the app is now localized: -- **UI Labels**: Buttons, titles, descriptions -- **Dialog Messages**: Confirmations, errors, information -- **Content Descriptions**: Accessibility labels for screen readers -- **Settings**: Configuration options and descriptions - -## 🔧 Implementation Details - -### Compose UI -The app uses `stringResource()` calls throughout the Compose UI: -```kotlin -Text(stringResource(R.string.app_name)) -Text(stringResource(R.string.settings)) -``` - -### Android Manifest -The app name is localized: -```xml -android:label="@string/app_name" -``` - -## 📝 Adding New Strings - -### 1. Add to English (values/strings.xml) -```xml -New Feature -``` - -### 2. Add to Portuguese (values-pt/strings.xml) -```xml -Nova Funcionalidade -``` - -### 3. Add to Spanish (values-es/strings.xml) -```xml -Nueva Funcionalidad -``` - -### 4. Use in Code -```kotlin -Text(stringResource(R.string.new_feature_title)) -``` - -## 🌍 Supported Languages - -| Language | Code | File | Status | -|----------|------|------|---------| -| English | `en` | `values/strings.xml` | ✅ Complete | -| Portuguese | `pt` | `values-pt/strings.xml` | ✅ Complete | -| Spanish | `es` | `values-es/strings.xml` | ✅ Complete | - -## 🚀 Adding More Languages - -To add another language (e.g., French): - -1. Create `app/src/main/res/values-fr/strings.xml` -2. Copy all strings from `values/strings.xml` -3. Translate each string to French -4. The app will automatically support French when the device language is set to French - -## 📱 Testing Localization - -### Method 1: Device Settings -1. Go to Device Settings → System → Languages & input → Languages -2. Add your desired language and move it to the top -3. Restart the app -4. All text should now appear in the selected language - -### Method 2: Android Studio -1. Open Android Studio -2. Go to Run → Edit Configurations -3. In "Launch Options", set "Language" to your desired language code (e.g., "es" for Spanish) -4. Run the app - it will launch in the selected language - -### Method 3: ADB Command -```bash -# For Spanish -adb shell setprop persist.sys.language es -adb shell setprop persist.sys.country ES - -# For Portuguese -adb shell setprop persist.sys.language pt -adb shell setprop persist.sys.country BR - -# For English -adb shell setprop persist.sys.language en -adb shell setprop persist.sys.country US -``` - -## 🔍 Common Issues - -### Missing Translations -If a string is missing from a language file: -- The app will fall back to English -- Check the logcat for warnings about missing resources - -### String Format Issues -Some strings might need special handling for different languages: -- Date formats -- Number formats -- Pluralization rules -- Gender-specific language variations - -### RTL Support -All supported languages (English, Portuguese, Spanish) are left-to-right (LTR), so no special RTL handling is needed. - -## 📚 Best Practices - -1. **Never hardcode strings** - Always use `stringResource()` -2. **Keep translations up to date** - Update all language files when adding new features -3. **Test on real devices** - Emulator language switching can be unreliable -4. **Use descriptive names** - String keys should be self-explanatory -5. **Group related strings** - Use comments to organize strings by feature -6. **Consider cultural differences** - Some phrases may need adaptation beyond direct translation - -## 🛠️ Maintenance - -### Regular Tasks -- Review new strings added to English -- Translate new strings to Portuguese and Spanish -- Test all languages regularly -- Update this documentation when adding new languages - -### Quality Assurance -- Have native speakers review translations -- Test on different Android versions -- Verify accessibility with screen readers -- Check for text overflow in different languages -- Consider regional variations (e.g., European vs Latin American Spanish) - -## 🌎 Regional Considerations - -### Spanish -- **Spain (es-ES)**: Uses "vosotros" form and European Spanish vocabulary -- **Latin America (es-419)**: Uses "ustedes" form and Latin American vocabulary -- Current implementation uses neutral Latin American Spanish - -### Portuguese -- **Portugal (pt-PT)**: European Portuguese vocabulary and pronunciation -- **Brazil (pt-BR)**: Brazilian Portuguese vocabulary and pronunciation -- Current implementation uses Brazilian Portuguese - -## 📖 Resources - -- [Android Localization Guide](https://developer.android.com/guide/topics/resources/localization) -- [Android String Resources](https://developer.android.com/guide/topics/resources/string-resource) -- [Compose Localization](https://developer.android.com/jetpack/compose/localization) -- [Material Design Localization](https://material.io/design/usability/bidirectionality.html) -- [Spanish Language Resources](https://www.rae.es/) -- [Portuguese Language Resources](https://www.priberam.pt/) diff --git a/STARTUP_OPTIMIZATION_README.md b/STARTUP_OPTIMIZATION_README.md deleted file mode 100644 index 7eaf36f..0000000 --- a/STARTUP_OPTIMIZATION_README.md +++ /dev/null @@ -1,72 +0,0 @@ -# App Startup Optimization for Player - -## Overview - -This implementation uses Android's App Startup library to initialize the MediaController during app startup, significantly reducing the time it takes for the player to be ready when the user opens the app. - -## What Was Changed - -### 1. Dependencies Added -- Added `androidx.startup:startup-runtime:1.1.1` to the project dependencies -- Updated `gradle/libs.versions.toml` to include the startup library - -### 2. PlayerInitializer Class -- Created `app/src/main/java/com/mobyle/abbay/infra/startup/PlayerInitializer.kt` -- Implements `Initializer>` -- Pre-warms the MediaController during app startup in the background -- Handles errors gracefully during startup - -### 3. AndroidManifest.xml Updates -- Added the `InitializationProvider` with metadata for `PlayerInitializer` -- This ensures the player is initialized when the app starts - -### 4. MainActivity Refactoring -- Removed manual MediaController creation -- Now uses the pre-initialized player from App Startup -- Added better error handling and logging -- Improved splash screen management - -## How It Works - -1. **App Startup**: When the app launches, the `PlayerInitializer` runs automatically -2. **Background Initialization**: The MediaController is created and prepared in the background -3. **Pre-warming**: The controller is "warmed up" so it's ready to use immediately -4. **MainActivity**: When MainActivity starts, it gets the already-initialized controller -5. **Faster Player Ready**: The player is ready much faster, reducing splash screen time - -## Benefits - -- **Faster Startup**: Player is ready immediately when MainActivity loads -- **Better UX**: Reduced splash screen time and faster app responsiveness -- **Background Processing**: Player initialization doesn't block the UI thread -- **Error Resilience**: Better error handling and fallback mechanisms -- **Resource Management**: More efficient resource usage during startup - -## Performance Impact - -- **Before**: Player initialization happened in MainActivity, blocking UI -- **After**: Player initialization happens during app startup, in parallel -- **Result**: Significantly faster player readiness and better user experience - -## Technical Details - -- Uses `ListenableFuture` for asynchronous initialization -- Implements proper error handling and logging -- Maintains backward compatibility -- Follows Android best practices for startup optimization - -## Troubleshooting - -If you encounter issues: - -1. Check logcat for "PlayerInitializer" tags -2. Verify the manifest has the correct provider configuration -3. Ensure all dependencies are properly synced -4. Check that the PlayerService is properly configured - -## Future Enhancements - -- Add startup performance metrics -- Implement startup time monitoring -- Add configuration options for different startup strategies -- Consider lazy initialization for other heavy components diff --git a/app/src/main/java/com/mobyle/abbay/presentation/MainActivity.kt b/app/src/main/java/com/mobyle/abbay/presentation/MainActivity.kt index a3622f0..a836f96 100644 --- a/app/src/main/java/com/mobyle/abbay/presentation/MainActivity.kt +++ b/app/src/main/java/com/mobyle/abbay/presentation/MainActivity.kt @@ -15,7 +15,6 @@ import androidx.media3.session.MediaController import androidx.navigation.compose.rememberNavController import androidx.startup.AppInitializer import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.common.util.concurrent.ListenableFuture import com.mobyle.abbay.infra.navigation.AbbayNavHost import com.mobyle.abbay.infra.startup.PlayerInitializer import com.mobyle.abbay.presentation.common.theme.MyApplicationTheme @@ -42,7 +41,7 @@ class MainActivity : ComponentActivity() { // Keep splash screen visible until player is ready splashScreen.setKeepOnScreenCondition { - !(isPlayerReady) + !isPlayerReady } super.onCreate(savedInstanceState) From f39bd7c60c8a87a18238b25914c91bba81a10c43 Mon Sep 17 00:00:00 2001 From: Bruno Abe Date: Sun, 24 Aug 2025 17:27:08 -0300 Subject: [PATCH 5/5] Fix PR comments --- .../mobyle/abbay/presentation/booklist/widgets/MiniPlayer.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/src/main/java/com/mobyle/abbay/presentation/booklist/widgets/MiniPlayer.kt b/app/src/main/java/com/mobyle/abbay/presentation/booklist/widgets/MiniPlayer.kt index 4a9ac9e..e6b2594 100644 --- a/app/src/main/java/com/mobyle/abbay/presentation/booklist/widgets/MiniPlayer.kt +++ b/app/src/main/java/com/mobyle/abbay/presentation/booklist/widgets/MiniPlayer.kt @@ -393,10 +393,6 @@ private fun MultipleFilePlayer( val currentIndex = book.currentBookPosition val duration = book.bookFileList[book.currentBookPosition].duration - LaunchedEffect(showChapters.value) { - onDisableGesture(showChapters.value) - } - LaunchedEffect(swipeProgress) { if (swipeProgress < 1f) { showChapters.value = false