From edd5b24d924d8cd39204f41d9ced1d7763ef7bf4 Mon Sep 17 00:00:00 2001 From: IllusionMan1212 Date: Fri, 12 Jul 2024 02:11:36 +0200 Subject: [PATCH] feat: settings option to change the app language --- app/build.gradle.kts | 7 +- app/src/main/AndroidManifest.xml | 13 +++- .../lyricsgrabbr/MainActivity.kt | 6 +- .../lyricsgrabbr/NotificationListener.kt | 12 ++-- .../lyricsgrabbr/ui/screens/Home.kt | 9 +-- .../lyricsgrabbr/ui/screens/Settings.kt | 72 +++++++++++++++++-- .../lyricsgrabbr/utils/Resources.kt | 40 +++++++++++ .../lyricsgrabbr/viewmodels/HomeViewModel.kt | 5 +- app/src/main/res/values-ar/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/locales_config.xml | 5 ++ 11 files changed, 144 insertions(+), 29 deletions(-) create mode 100644 app/src/main/res/xml/locales_config.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 23269ab..a547982 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -9,8 +9,8 @@ plugins { } val keystoreProperties = Properties() -val keystorePropertiesFile: File? = rootProject.file("keystore.properties") -if (keystorePropertiesFile?.exists() == true) { +val keystorePropertiesFile: File = rootProject.file("keystore.properties") +if (keystorePropertiesFile.exists()) { keystoreProperties.load(FileInputStream(keystorePropertiesFile)) } @@ -30,6 +30,9 @@ android { useSupportLibrary = true } signingConfig = signingConfigs.getByName("debug") + + // https://developer.android.com/guide/topics/resources/app-languages#gradle-config + resourceConfigurations.plus(listOf("en", "ar")) } signingConfigs { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4783888..c071f10 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" + android:localeConfig="@xml/locales_config" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" @@ -21,7 +22,7 @@ @@ -45,5 +46,15 @@ android:name="android.app.NOTIFICATION_LISTENER" android:value="Media playback notification listener service" /> + + + \ No newline at end of file diff --git a/app/src/main/java/com/illusionman1212/lyricsgrabbr/MainActivity.kt b/app/src/main/java/com/illusionman1212/lyricsgrabbr/MainActivity.kt index 3540200..a511206 100644 --- a/app/src/main/java/com/illusionman1212/lyricsgrabbr/MainActivity.kt +++ b/app/src/main/java/com/illusionman1212/lyricsgrabbr/MainActivity.kt @@ -8,6 +8,7 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -45,10 +46,9 @@ data class NotificationEvent( val artist: String, val title: String, val playbackState: Int, - val duration: Long ) -class MainActivity : ComponentActivity() { +class MainActivity : AppCompatActivity() { private var lastNotification: MutableStateFlow = MutableStateFlow(null) private var isReceiverRunning = false @@ -175,14 +175,12 @@ class MainActivity : ComponentActivity() { val artist = intent.getStringExtra(NotificationListener.NOTIFICATION_PACKAGE_ARTIST) val songTitle = intent.getStringExtra(NotificationListener.NOTIFICATION_PACKAGE_SONG_TITLE) val playbackState = intent.getIntExtra(NotificationListener.NOTIFICATION_PACKAGE_PLAYBACK_STATE, -1) - val duration = intent.getLongExtra(NotificationListener.NOTIFICATION_PACKAGE_DURATION, -1) val notifPackageName = intent.getStringExtra(NotificationListener.NOTIFICATION_PACKAGE_NAME) lastNotification.update { NotificationEvent( title = songTitle!!, artist = artist!!, playbackState = playbackState, - duration = duration, packageName = notifPackageName!!, ) } } diff --git a/app/src/main/java/com/illusionman1212/lyricsgrabbr/NotificationListener.kt b/app/src/main/java/com/illusionman1212/lyricsgrabbr/NotificationListener.kt index 352bb60..0e34795 100644 --- a/app/src/main/java/com/illusionman1212/lyricsgrabbr/NotificationListener.kt +++ b/app/src/main/java/com/illusionman1212/lyricsgrabbr/NotificationListener.kt @@ -18,6 +18,7 @@ import android.service.notification.StatusBarNotification import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat +import androidx.core.os.BundleCompat import androidx.localbroadcastmanager.content.LocalBroadcastManager class NotificationListener: NotificationListenerService() { @@ -113,14 +114,11 @@ class NotificationListener: NotificationListenerService() { if (ContextCompat.checkSelfPermission(this, POST_NOTIFICATIONS) != PERMISSION_GRANTED) { return } - // Apparently the new getParcelable API is buggy and throws an exception if used. - // so we continue to use the deprecated API even on newer versions. - // https://issuetracker.google.com/issues/240585930 - val mediaSessionToken = sbn.notification.extras.getParcelable(Notification.EXTRA_MEDIA_SESSION) + + val mediaSessionToken = BundleCompat.getParcelable(sbn.notification.extras, Notification.EXTRA_MEDIA_SESSION, MediaSession.Token::class.java) ?: return val mediaCtrl = MediaController(context, mediaSessionToken) - val duration = mediaCtrl.metadata?.getLong(MediaMetadata.METADATA_KEY_DURATION)?.div(1000) val playbackState = mediaCtrl.playbackState?.state val albumArt = mediaCtrl.metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART) @@ -133,7 +131,6 @@ class NotificationListener: NotificationListenerService() { intent.putExtra(NOTIFICATION_PACKAGE_NAME, sbn.packageName) intent.putExtra(NOTIFICATION_PACKAGE_ARTIST, packageArtist) intent.putExtra(NOTIFICATION_PACKAGE_SONG_TITLE, packageSongTitle) - intent.putExtra(NOTIFICATION_PACKAGE_DURATION, duration) intent.putExtra(NOTIFICATION_PACKAGE_PLAYBACK_STATE, playbackState) if (playbackState == PlaybackState.STATE_PAUSED) { @@ -166,9 +163,8 @@ class NotificationListener: NotificationListenerService() { const val NOTIFICATION_PACKAGE_ARTIST = "package_artist" const val NOTIFICATION_PACKAGE_SONG_TITLE = "package_song_title" const val NOTIFICATION_PACKAGE_PLAYBACK_STATE = "package_playback_state" - const val NOTIFICATION_PACKAGE_DURATION = "package_duration" const val NOTIFICATION_ID = 1 const val NOTIFICATION_CHANNEL_ID = "Lyrics" const val START_FOREGROUND_SERVICE_ACTION = "START_SERVICE" } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/illusionman1212/lyricsgrabbr/ui/screens/Home.kt b/app/src/main/java/com/illusionman1212/lyricsgrabbr/ui/screens/Home.kt index 161407f..fb63f93 100644 --- a/app/src/main/java/com/illusionman1212/lyricsgrabbr/ui/screens/Home.kt +++ b/app/src/main/java/com/illusionman1212/lyricsgrabbr/ui/screens/Home.kt @@ -30,9 +30,9 @@ import androidx.compose.material.icons.filled.MusicNote import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme @@ -48,7 +48,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -86,7 +85,6 @@ import com.illusionman1212.lyricsgrabbr.viewmodels.HomeViewModel import com.illusionman1212.lyricsgrabbr.viewmodels.SearchResult import kotlinx.coroutines.flow.MutableStateFlow import coil.compose.AsyncImage -import com.illusionman1212.lyricsgrabbr.LGApp import com.illusionman1212.lyricsgrabbr.ui.components.forwardingPainter import com.illusionman1212.lyricsgrabbr.utils.annotatedStringResource import com.illusionman1212.lyricsgrabbr.viewmodels.LyricsViewModel @@ -106,7 +104,6 @@ fun HomePage( lastNotification: MutableStateFlow ) { val context = LocalContext.current - val application = (context as Activity).application as LGApp val lifecycleOwner = LocalLifecycleOwner.current val lifecycleState by lifecycleOwner.lifecycle.currentStateAsState() @@ -124,7 +121,7 @@ fun HomePage( ) } var shouldShowNotificationPermissionRationale by remember { mutableStateOf( ActivityCompat.shouldShowRequestPermissionRationale( - context, + context as Activity, POST_NOTIFICATIONS ) )} @@ -415,7 +412,7 @@ fun Results( ) if (it != results.last()) - Divider() + HorizontalDivider() } } } diff --git a/app/src/main/java/com/illusionman1212/lyricsgrabbr/ui/screens/Settings.kt b/app/src/main/java/com/illusionman1212/lyricsgrabbr/ui/screens/Settings.kt index 3b47751..c3f8cdc 100644 --- a/app/src/main/java/com/illusionman1212/lyricsgrabbr/ui/screens/Settings.kt +++ b/app/src/main/java/com/illusionman1212/lyricsgrabbr/ui/screens/Settings.kt @@ -1,5 +1,6 @@ package com.illusionman1212.lyricsgrabbr.ui.screens +import androidx.appcompat.app.AppCompatDelegate import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -12,10 +13,12 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.outlined.Palette +import androidx.compose.material.icons.outlined.Translate import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -32,10 +35,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.core.os.LocaleListCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.illusionman1212.lyricsgrabbr.R import com.illusionman1212.lyricsgrabbr.ui.components.LGAlertDialog @@ -43,7 +48,7 @@ import com.illusionman1212.lyricsgrabbr.ui.components.LGRadioButton import com.illusionman1212.lyricsgrabbr.ui.theme.ColorSecondaryLight import com.illusionman1212.lyricsgrabbr.data.Theme import com.illusionman1212.lyricsgrabbr.ui.components.LGIconButton -import com.illusionman1212.lyricsgrabbr.utils.mirror +import com.illusionman1212.lyricsgrabbr.utils.getLangPreferenceDropdownEntries import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsState import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsViewModel @@ -72,8 +77,7 @@ fun SettingsPage( ) { LGIconButton(tooltip = stringResource(id = R.string.go_back), onClick = goBack) { Icon( - modifier = Modifier.mirror(), - imageVector = Icons.Filled.ArrowBack, + imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource( id = R.string.go_back ) ) @@ -97,6 +101,7 @@ fun SettingsPage( @Composable fun Appearance(viewModel: SettingsViewModel, uiState: SettingsState) { var themeOpenDialog by remember { mutableStateOf(false) } + var languageOpenDialog by remember { mutableStateOf(false) } SettingGroup(title = stringResource(id = R.string.appearance)) { DialogSetting( @@ -105,6 +110,17 @@ fun Appearance(viewModel: SettingsViewModel, uiState: SettingsState) { icon = Icons.Outlined.Palette, onClick = { themeOpenDialog = true } ) + DialogSetting( + title = stringResource(id = R.string.language), + value = AppCompatDelegate.getApplicationLocales()[0]?.let { + AppCompatDelegate.getApplicationLocales()[0]?.getDisplayLanguage( + it + ) + } + ?: stringResource(id = R.string._default), + icon = Icons.Outlined.Translate, + onClick = { languageOpenDialog = true } + ) } if (themeOpenDialog) { ThemeDialog( @@ -115,6 +131,11 @@ fun Appearance(viewModel: SettingsViewModel, uiState: SettingsState) { } ) } + if (languageOpenDialog) { + LanguageDialog( + onDismiss = { languageOpenDialog = false }, + ) + } } @Composable @@ -159,6 +180,47 @@ fun ThemeDialog( } } +@Composable +fun LanguageDialog( + onDismiss: () -> Unit, +) { + val langs = LocalContext.current.getLangPreferenceDropdownEntries() + + LGAlertDialog( + onDismiss = onDismiss, + title = stringResource(id = R.string.language), + buttons = { + TextButton(onClick = onDismiss) { + Text(stringResource(id = R.string.cancel)) + } + }, + modifier = Modifier.fillMaxWidth() + ) { + LazyColumn { + item { + LGRadioButton( + text = stringResource(id = R.string._default), + onClick = { + AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList()) + }, + selected = AppCompatDelegate.getApplicationLocales().isEmpty, + ) + } + for ((key, value) in langs) { + item { + LGRadioButton( + text = key, + onClick = { + AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(value)) + }, + selected = AppCompatDelegate.getApplicationLocales()[0]?.language == value, + ) + } + } + } + } +} + @Composable fun SettingGroup(title: String, content: @Composable () -> Unit) { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { @@ -169,7 +231,7 @@ fun SettingGroup(title: String, content: @Composable () -> Unit) { } @Composable -fun Header(title: String) { +private fun Header(title: String) { Row(Modifier.padding(start = 56.dp)) { Text(text = title, color = MaterialTheme.colorScheme.primary) } diff --git a/app/src/main/java/com/illusionman1212/lyricsgrabbr/utils/Resources.kt b/app/src/main/java/com/illusionman1212/lyricsgrabbr/utils/Resources.kt index 5c1d602..60c46ba 100644 --- a/app/src/main/java/com/illusionman1212/lyricsgrabbr/utils/Resources.kt +++ b/app/src/main/java/com/illusionman1212/lyricsgrabbr/utils/Resources.kt @@ -1,5 +1,6 @@ package com.illusionman1212.lyricsgrabbr.utils +import android.content.Context import android.text.Annotation import android.text.SpannedString import androidx.annotation.StringRes @@ -7,6 +8,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle +import androidx.core.os.LocaleListCompat +import com.illusionman1212.lyricsgrabbr.R +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserException +import java.io.IOException // Native stylization is coming soon // https://issuetracker.google.com/issues/139320238 @@ -31,4 +37,38 @@ fun annotatedStringResource( spanStyles(annotation)?.let { resultBuilder.addStyle(it, spanStart, spanEnd) } } return resultBuilder.toAnnotatedString() +} + +fun Context.getLocaleListFromXml(): LocaleListCompat { + val tagsList = mutableListOf() + try { + val xpp: XmlPullParser = resources.getXml(R.xml.locales_config) + while (xpp.eventType != XmlPullParser.END_DOCUMENT) { + if (xpp.eventType == XmlPullParser.START_TAG) { + if (xpp.name == "locale") { + tagsList.add(xpp.getAttributeValue(0)) + } + } + xpp.next() + } + } catch (e: XmlPullParserException) { + e.printStackTrace() + } catch (e: IOException) { + e.printStackTrace() + } + + return LocaleListCompat.forLanguageTags(tagsList.joinToString(",")) +} + + +fun Context.getLangPreferenceDropdownEntries(): Map { + val localeList = getLocaleListFromXml() + val map = mutableMapOf() + + for (a in 0 until localeList.size()) { + localeList[a].let { + it?.let { it1 -> map.put(it1.getDisplayName(it), it.toLanguageTag()) } + } + } + return map } \ No newline at end of file diff --git a/app/src/main/java/com/illusionman1212/lyricsgrabbr/viewmodels/HomeViewModel.kt b/app/src/main/java/com/illusionman1212/lyricsgrabbr/viewmodels/HomeViewModel.kt index c91a989..469cc80 100644 --- a/app/src/main/java/com/illusionman1212/lyricsgrabbr/viewmodels/HomeViewModel.kt +++ b/app/src/main/java/com/illusionman1212/lyricsgrabbr/viewmodels/HomeViewModel.kt @@ -1,6 +1,5 @@ package com.illusionman1212.lyricsgrabbr.viewmodels -import android.content.Context import android.net.Uri import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -85,7 +84,7 @@ data class HomeState( val error: String? = null, ) -class HomeViewModel(private val context: Context, private val homeRepository: HomeRepository): ViewModel() { +class HomeViewModel(private val homeRepository: HomeRepository): ViewModel() { private val _uiState = MutableStateFlow(HomeState()) val uiState = _uiState.asStateFlow() private val json = Json { @@ -134,7 +133,7 @@ class HomeViewModel(private val context: Context, private val homeRepository: Ho val Factory: ViewModelProvider.Factory = viewModelFactory { initializer { val application = (this[APPLICATION_KEY] as LGApp) - HomeViewModel(application.applicationContext, application.homeRepository) + HomeViewModel(application.homeRepository) } } } diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 02277b2..337341c 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -32,6 +32,8 @@ نهار ليلي استخدم مظهر النظام + اللغة + الإفتراضي كلمات الأغنية diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2c7ba71..a47faa0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -31,6 +31,8 @@ Light Dark Use System Theme + Language + Default Lyrics diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml new file mode 100644 index 0000000..d3b984b --- /dev/null +++ b/app/src/main/res/xml/locales_config.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file