Author SHA1 Message Date
Illusion 37b5ca8917 feat: settings option to change the app language 2024-07-12 02:11:36 +02:00
Illusion 6b64ead713 remove spotify as lyrics source
In order to stay on f-droid spotify has to be removed. Doing different flavors is not worth it in my opinion.
2024-07-11 02:01:03 +02:00
12 changed files with 10 additions and 379 deletions
+1 -1
View File
@@ -118,7 +118,7 @@ dependencies {
implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3:1.3.0-beta04") implementation("androidx.compose.material3:material3:1.2.1")
implementation("com.google.android.material:material:1.12.0") implementation("com.google.android.material:material:1.12.0")
implementation("androidx.compose.material:material-icons-extended:1.6.8") implementation("androidx.compose.material:material-icons-extended:1.6.8")
implementation("androidx.core:core-splashscreen:1.0.1") implementation("androidx.core:core-splashscreen:1.0.1")
-2
View File
@@ -6,8 +6,6 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<application <application
android:name=".LGApp" android:name=".LGApp"
@@ -35,7 +35,6 @@ import com.illusionman1212.lyricsgrabbr.ui.screens.HomePage
import com.illusionman1212.lyricsgrabbr.ui.screens.LyricsPage import com.illusionman1212.lyricsgrabbr.ui.screens.LyricsPage
import com.illusionman1212.lyricsgrabbr.ui.theme.LyricsGrabbrTheme import com.illusionman1212.lyricsgrabbr.ui.theme.LyricsGrabbrTheme
import com.illusionman1212.lyricsgrabbr.ui.screens.SettingsPage import com.illusionman1212.lyricsgrabbr.ui.screens.SettingsPage
import com.illusionman1212.lyricsgrabbr.ui.screens.WhitelistPage
import com.illusionman1212.lyricsgrabbr.viewmodels.LyricsViewModel import com.illusionman1212.lyricsgrabbr.viewmodels.LyricsViewModel
import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsViewModel import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -131,17 +130,6 @@ class MainActivity : AppCompatActivity() {
} }
composable(Routes.Settings.route) { composable(Routes.Settings.route) {
SettingsPage( SettingsPage(
goBack = {
navController.popBackStack()
},
navigateToWhitelist = {
navController.navigate(Routes.Whitelist.route)
},
viewModel = settingsViewModel,
)
}
composable(Routes.Whitelist.route) {
WhitelistPage(
goBack = { goBack = {
navController.popBackStack() navController.popBackStack()
}, },
@@ -20,29 +20,15 @@ import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.os.BundleCompat import androidx.core.os.BundleCompat
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.illusionman1212.lyricsgrabbr.data.SettingsPreferencesRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
class NotificationListener: NotificationListenerService() { class NotificationListener: NotificationListenerService() {
private var notificationManager : NotificationManagerCompat? = null private var notificationManager : NotificationManagerCompat? = null
private lateinit var settingsPrefsRepo: SettingsPreferencesRepository
private val serviceScope = CoroutineScope(Dispatchers.IO)
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
initNotificationChannels(this) initNotificationChannels(this)
notificationManager = NotificationManagerCompat.from(applicationContext) notificationManager = NotificationManagerCompat.from(applicationContext)
settingsPrefsRepo = (application as LGApp).settingsPreferencesRepository
}
override fun onDestroy() {
super.onDestroy()
serviceScope.cancel()
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@@ -67,11 +53,7 @@ class NotificationListener: NotificationListenerService() {
} }
override fun onNotificationPosted(sbn: StatusBarNotification) { override fun onNotificationPosted(sbn: StatusBarNotification) {
serviceScope.launch { sendNotificationBroadcast(sbn, applicationContext)
val whitelist = settingsPrefsRepo.preferences.first().whitelist
if (sbn.packageName !in whitelist) return@launch
sendNotificationBroadcast(sbn, applicationContext)
}
} }
private fun getStopServiceNotificationAction(): PendingIntent { private fun getStopServiceNotificationAction(): PendingIntent {
@@ -109,13 +91,8 @@ class NotificationListener: NotificationListenerService() {
private fun getCurrentNotifications(context: Context) { private fun getCurrentNotifications(context: Context) {
val sbns = activeNotifications val sbns = activeNotifications
serviceScope.launch { for (sbn in sbns) {
val whitelist = settingsPrefsRepo.preferences.first().whitelist sendNotificationBroadcast(sbn, context)
for (sbn in sbns) {
if (sbn.packageName !in whitelist) continue
sendNotificationBroadcast(sbn, context)
}
} }
} }
@@ -1,9 +1,8 @@
package com.illusionman1212.lyricsgrabbr package com.illusionman1212.lyricsgrabbr
sealed class Routes(val route: String) { sealed class Routes(val route: String) {
data object Home : Routes("home") object Home : Routes("home")
data object Lyrics : Routes("lyrics") object Lyrics : Routes("lyrics")
data object Settings : Routes("settings") object Settings : Routes("settings")
data object Whitelist : Routes("settings/whitelist")
} }
@@ -4,7 +4,6 @@ import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringSetPreferencesKey
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
enum class Theme { enum class Theme {
@@ -15,7 +14,6 @@ enum class Theme {
data class SettingsPreferences( data class SettingsPreferences(
val appTheme: Int = Theme.SYSTEM.ordinal, val appTheme: Int = Theme.SYSTEM.ordinal,
val whitelist: Set<String> = emptySet(),
) )
class SettingsPreferencesRepository( class SettingsPreferencesRepository(
@@ -23,7 +21,6 @@ class SettingsPreferencesRepository(
) { ) {
companion object { companion object {
private val APP_THEME = intPreferencesKey("app_theme") private val APP_THEME = intPreferencesKey("app_theme")
private val WHITELIST = stringSetPreferencesKey("apps_whitelist")
} }
val preferences = dataStore.data.map { mapSettingsPreferences(it) } val preferences = dataStore.data.map { mapSettingsPreferences(it) }
@@ -34,22 +31,9 @@ class SettingsPreferencesRepository(
} }
} }
suspend fun addToWhitelist(packageName: String) {
dataStore.edit { prefs ->
prefs[WHITELIST] = (prefs[WHITELIST]?.toMutableSet() ?: emptySet()) + packageName
}
}
suspend fun removeFromWhitelist(packageName: String) {
dataStore.edit { prefs ->
prefs[WHITELIST] = (prefs[WHITELIST]?.toMutableSet() ?: emptySet()) - packageName
}
}
private fun mapSettingsPreferences(prefs: Preferences): SettingsPreferences { private fun mapSettingsPreferences(prefs: Preferences): SettingsPreferences {
return SettingsPreferences( return SettingsPreferences(
prefs[APP_THEME] ?: Theme.SYSTEM.ordinal, prefs[APP_THEME] ?: Theme.SYSTEM.ordinal,
prefs[WHITELIST] ?: emptySet(),
) )
} }
} }
@@ -433,6 +433,7 @@ fun LongClickBottomSheet(
dragHandle = {}, dragHandle = {},
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
sheetState = sheetState, sheetState = sheetState,
windowInsets = WindowInsets(top = 0.dp),
) { ) {
Column { Column {
if (artistUri != Uri.EMPTY) { if (artistUri != Uri.EMPTY) {
@@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Apps
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.outlined.Palette import androidx.compose.material.icons.outlined.Palette
import androidx.compose.material.icons.outlined.Translate import androidx.compose.material.icons.outlined.Translate
@@ -62,7 +61,6 @@ private val Themes = hashMapOf(
@Composable @Composable
fun SettingsPage( fun SettingsPage(
goBack: () -> Unit, goBack: () -> Unit,
navigateToWhitelist: () -> Unit,
viewModel: SettingsViewModel, viewModel: SettingsViewModel,
) { ) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
@@ -96,7 +94,6 @@ fun SettingsPage(
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
Appearance(viewModel, uiState) Appearance(viewModel, uiState)
Listening(navigateToWhitelist)
} }
} }
} }
@@ -141,35 +138,6 @@ fun Appearance(viewModel: SettingsViewModel, uiState: SettingsState) {
} }
} }
@Composable
private fun Listening(navigateToWhitelist: () -> Unit) {
SettingGroup(title = stringResource(id = R.string.listening)) {
Box(
Modifier
.fillMaxWidth()
.clickable { navigateToWhitelist() }
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 8.dp)
) {
Icon(imageVector = Icons.Default.Apps, contentDescription = null, modifier = Modifier.width(56.dp))
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(text = stringResource(id = R.string.whitelist_apps), color = MaterialTheme.colorScheme.onBackground)
Text(
text = stringResource(id = R.string.whitelist_desc),
color = ColorSecondaryLight,
fontSize = 14.sp,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(end = 24.dp)
)
}
}
}
}
}
@Composable @Composable
fun ThemeDialog( fun ThemeDialog(
uiState: SettingsState, uiState: SettingsState,
@@ -1,162 +0,0 @@
package com.illusionman1212.lyricsgrabbr.ui.screens
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.compose.AsyncImage
import com.illusionman1212.lyricsgrabbr.R
import com.illusionman1212.lyricsgrabbr.ui.components.LGIconButton
import com.illusionman1212.lyricsgrabbr.ui.theme.Typography
import com.illusionman1212.lyricsgrabbr.viewmodels.AppInfo
import com.illusionman1212.lyricsgrabbr.viewmodels.SettingsViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun WhitelistPage(goBack: () -> Unit, viewModel: SettingsViewModel) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
Scaffold(topBar = {
TopAppBar(
title = {
TextField(
value = uiState.whitelistSearchQuery,
onValueChange = {
viewModel.filterPackages(it)
},
modifier = Modifier.fillMaxWidth(),
placeholder = {
Text(text = stringResource(id = R.string.search), style = Typography.titleLarge)
},
trailingIcon = {
LGIconButton(tooltip = stringResource(id = R.string.clear), onClick = { viewModel.clearWhitelistSearch() }) {
Icon(Icons.Default.Clear, contentDescription = stringResource(id = R.string.clear))
}
},
singleLine = true,
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
disabledContainerColor = Color.Transparent,
cursorColor = MaterialTheme.colorScheme.primary,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
),
)
},
navigationIcon = {
LGIconButton(tooltip = stringResource(id = R.string.go_back), onClick = goBack) {
Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(
id = R.string.go_back
))
}
}
)
}) { padding ->
val context = LocalContext.current
val refreshState = rememberPullToRefreshState()
var isRefreshing by remember { mutableStateOf(false) }
if (uiState.applications.isNotEmpty()) {
isRefreshing = false
}
PullToRefreshBox(
state = refreshState,
isRefreshing = isRefreshing,
onRefresh = {
isRefreshing = true
viewModel.refreshPackages(context)
},
modifier = Modifier.padding(padding)
) {
LazyColumn(Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) {
if (uiState.applications.any { it.value.isWhitelisted }) {
for ((packageName, appInfo) in uiState.applications.filter { it.value.isWhitelisted }) {
item {
Application(packageName, appInfo, viewModel)
}
}
item {
HorizontalDivider(
Modifier
.width(84.dp)
.padding(vertical = 12.dp))
}
}
for ((packageName, appInfo) in uiState.applications.filter { !it.value.isWhitelisted }) {
item {
Application(packageName, appInfo, viewModel)
}
}
}
}
}
}
@Composable
fun Application(packageName: String, app: AppInfo, viewModel: SettingsViewModel) {
Box(
Modifier
.fillMaxWidth()
.clickable(
role = Role.Checkbox,
onClick = {
if (app.isWhitelisted) {
viewModel.removeFromWhitelist(packageName)
} else {
viewModel.addToWhitelist(packageName)
}
}
)) {
Row(
Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 5.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
AsyncImage(modifier = Modifier.size(32.dp), model = app.icon, contentDescription = null)
Text(app.displayName, Modifier.weight(1f))
Checkbox(
checked = app.isWhitelisted,
onCheckedChange = null,
)
}
}
}
@@ -1,8 +1,5 @@
package com.illusionman1212.lyricsgrabbr.viewmodels package com.illusionman1212.lyricsgrabbr.viewmodels
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
@@ -17,23 +14,13 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
data class AppInfo(
val displayName: String,
val icon: Drawable,
var isWhitelisted: Boolean,
)
data class SettingsState( data class SettingsState(
val appTheme: Int = Theme.SYSTEM.ordinal, val appTheme: Int = Theme.SYSTEM.ordinal,
val applications: Map<String, AppInfo> = emptyMap(),
val whitelistSearchQuery: String = "",
) )
class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesRepository, context: Context): ViewModel() { class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesRepository): ViewModel() {
private var immutablePackages: Map<String, AppInfo> = emptyMap()
private val _uiState = MutableStateFlow(SettingsState()) private val _uiState = MutableStateFlow(SettingsState())
val uiState = _uiState.asStateFlow() val uiState = _uiState.asStateFlow()
@@ -47,104 +34,7 @@ class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesReposi
} }
} }
private suspend fun getWhitelistedApps(): Set<String> {
return settingsPrefsRepo.preferences.first().whitelist
}
private fun getPackages(context: Context) {
if (immutablePackages.isNotEmpty()) return
viewModelScope.launch(Dispatchers.IO) {
val pm = context.packageManager
val packages = pm.getInstalledApplications(PackageManager.GET_META_DATA)
.filter { it.packageName != context.packageName && pm.getLaunchIntentForPackage(it.packageName) != null }
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.loadLabel(pm).toString() })
val apps: MutableMap<String, AppInfo> = mutableMapOf()
val whitelistedApps = getWhitelistedApps()
packages.forEach { pkg ->
val label = pkg.loadLabel(pm)
val icon = pkg.loadIcon(pm)
apps[pkg.packageName] = AppInfo(
displayName = label.toString(),
icon = icon,
isWhitelisted = pkg.packageName in whitelistedApps
)
}
immutablePackages = apps
_uiState.update {
SettingsState(
appTheme = uiState.value.appTheme,
applications = apps,
)
}
}
}
fun refreshPackages(context: Context) {
_uiState.update {
val apps = uiState.value.applications.toMutableMap()
apps.clear()
it.copy(whitelistSearchQuery = "", applications = apps)
}
val apps = immutablePackages.toMutableMap()
apps.clear()
immutablePackages = apps
getPackages(context)
}
fun filterPackages(query: String) {
_uiState.update { state ->
state.copy(
applications = immutablePackages.filter {
it.value.displayName.lowercase().contains(query.lowercase())
}.toMutableMap(),
whitelistSearchQuery = query,
)
}
}
fun clearWhitelistSearch() {
_uiState.update {
it.copy(applications = immutablePackages, whitelistSearchQuery = "")
}
}
fun addToWhitelist(packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
_uiState.update { state ->
val apps = immutablePackages.toMutableMap()
apps[packageName] = apps[packageName]!!.copy(isWhitelisted = true)
immutablePackages = apps
state.copy(
applications = immutablePackages.filter {
it.value.displayName.lowercase().contains(uiState.value.whitelistSearchQuery.lowercase())
}.toMutableMap(),
)
}
settingsPrefsRepo.addToWhitelist(packageName)
}
}
fun removeFromWhitelist(packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
_uiState.update { state ->
val apps = immutablePackages.toMutableMap()
apps[packageName] = apps[packageName]!!.copy(isWhitelisted = false)
immutablePackages = apps
state.copy(
applications = immutablePackages.filter {
it.value.displayName.lowercase().contains(uiState.value.whitelistSearchQuery.lowercase())
}.toMutableMap(),
)
}
settingsPrefsRepo.removeFromWhitelist(packageName)
}
}
init { init {
getPackages(context)
viewModelScope.launch { viewModelScope.launch {
settingsPrefsRepo.preferences.collectLatest { settingsPrefsRepo.preferences.collectLatest {
_uiState.value = _uiState.value.copy( _uiState.value = _uiState.value.copy(
@@ -158,7 +48,7 @@ class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesReposi
val Factory: ViewModelProvider.Factory = viewModelFactory { val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer { initializer {
val application = (this[APPLICATION_KEY] as LGApp) val application = (this[APPLICATION_KEY] as LGApp)
SettingsViewModel(application.settingsPreferencesRepository, application.applicationContext) SettingsViewModel(application.settingsPreferencesRepository)
} }
} }
} }
-6
View File
@@ -34,12 +34,6 @@
<string name="theme_system">استخدم مظهر النظام</string> <string name="theme_system">استخدم مظهر النظام</string>
<string name="language">اللغة</string> <string name="language">اللغة</string>
<string name="_default">الإفتراضي</string> <string name="_default">الإفتراضي</string>
<string name="listening">الإستماع</string>
<string name="whitelist">القائمة البيضاء</string>
<string name="whitelist_apps">تطبيقات القائمة البيضاء</string>
<string name="whitelist_desc">اختر التطبيقات التي سيستمع التطبيق إلى إشعاراتها</string>
<string name="search">بحث</string>
<string name="clear">مسح</string>
<!-- Lyrics --> <!-- Lyrics -->
<string name="lyrics">كلمات الأغنية</string> <string name="lyrics">كلمات الأغنية</string>
-6
View File
@@ -33,12 +33,6 @@
<string name="theme_system">Use System Theme</string> <string name="theme_system">Use System Theme</string>
<string name="language">Language</string> <string name="language">Language</string>
<string name="_default">Default</string> <string name="_default">Default</string>
<string name="listening">Listening</string>
<string name="whitelist">Whitelist</string>
<string name="whitelist_apps">Whitelist Apps</string>
<string name="whitelist_desc">Choose which apps Lyrics Grabbr will listen to</string>
<string name="search">Search</string>
<string name="clear">Clear</string>
<!-- Lyrics --> <!-- Lyrics -->
<string name="lyrics">Lyrics</string> <string name="lyrics">Lyrics</string>