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/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..dc48c23 --- /dev/null +++ b/app/src/main/java/com/mobyle/abbay/infra/startup/PlayerInitializer.kt @@ -0,0 +1,31 @@ +package com.mobyle.abbay.infra.startup + +import android.content.ComponentName +import android.content.Context +import androidx.media3.session.MediaController +import androidx.media3.session.SessionToken +import androidx.startup.Initializer +import com.mobyle.abbay.presentation.common.service.PlayerService +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn + +class PlayerInitializer : Initializer> { + override fun create(context: Context): Flow { + return flow { + val controller = MediaController.Builder( + context, + SessionToken(context, ComponentName(context, PlayerService::class.java)) + ).buildAsync() + + val player = controller.get() + + 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 d00fb93..a836f96 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,18 +8,15 @@ 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.presentation.common.service.PlayerService +import com.mobyle.abbay.infra.startup.PlayerInitializer import com.mobyle.abbay.presentation.common.theme.MyApplicationTheme import com.usecase.IsPlayWhenAppIsClosedEnabled import com.usecase.UpdateAppLifeStatus @@ -37,7 +33,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?) { @@ -52,36 +48,22 @@ class MainActivity : ComponentActivity() { updateAppLifeStatus(true) - controller = MediaController.Builder( - this, - SessionToken(this, ComponentName(this, PlayerService::class.java)) - ).buildAsync() - 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 +80,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/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) } 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..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 @@ -647,26 +643,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 +847,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 = 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 = [