mirror of
https://github.com/IllusionMan1212/lyrics-grabbr.git
synced 2026-10-24 02:12:27 +00:00
feat: whitelist for notification listening
This commit is contained in:
@@ -6,6 +6,8 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
<application
|
||||
android:name=".LGApp"
|
||||
|
||||
@@ -35,6 +35,7 @@ import com.illusionman1212.lyricsgrabbr.ui.screens.HomePage
|
||||
import com.illusionman1212.lyricsgrabbr.ui.screens.LyricsPage
|
||||
import com.illusionman1212.lyricsgrabbr.ui.theme.LyricsGrabbrTheme
|
||||
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.SettingsViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -130,6 +131,17 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
composable(Routes.Settings.route) {
|
||||
SettingsPage(
|
||||
goBack = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
navigateToWhitelist = {
|
||||
navController.navigate(Routes.Whitelist.route)
|
||||
},
|
||||
viewModel = settingsViewModel,
|
||||
)
|
||||
}
|
||||
composable(Routes.Whitelist.route) {
|
||||
WhitelistPage(
|
||||
goBack = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
|
||||
@@ -20,15 +20,29 @@ import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.os.BundleCompat
|
||||
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() {
|
||||
private var notificationManager : NotificationManagerCompat? = null
|
||||
private lateinit var settingsPrefsRepo: SettingsPreferencesRepository
|
||||
private val serviceScope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
initNotificationChannels(this)
|
||||
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 {
|
||||
@@ -53,7 +67,11 @@ class NotificationListener: NotificationListenerService() {
|
||||
}
|
||||
|
||||
override fun onNotificationPosted(sbn: StatusBarNotification) {
|
||||
sendNotificationBroadcast(sbn, applicationContext)
|
||||
serviceScope.launch {
|
||||
val whitelist = settingsPrefsRepo.preferences.first().whitelist
|
||||
if (sbn.packageName !in whitelist) return@launch
|
||||
sendNotificationBroadcast(sbn, applicationContext)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStopServiceNotificationAction(): PendingIntent {
|
||||
@@ -91,8 +109,13 @@ class NotificationListener: NotificationListenerService() {
|
||||
|
||||
private fun getCurrentNotifications(context: Context) {
|
||||
val sbns = activeNotifications
|
||||
for (sbn in sbns) {
|
||||
sendNotificationBroadcast(sbn, context)
|
||||
serviceScope.launch {
|
||||
val whitelist = settingsPrefsRepo.preferences.first().whitelist
|
||||
|
||||
for (sbn in sbns) {
|
||||
if (sbn.packageName !in whitelist) continue
|
||||
sendNotificationBroadcast(sbn, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package com.illusionman1212.lyricsgrabbr
|
||||
|
||||
sealed class Routes(val route: String) {
|
||||
object Home : Routes("home")
|
||||
object Lyrics : Routes("lyrics")
|
||||
object Settings : Routes("settings")
|
||||
data object Home : Routes("home")
|
||||
data object Lyrics : Routes("lyrics")
|
||||
data object Settings : Routes("settings")
|
||||
data object Whitelist : Routes("settings/whitelist")
|
||||
}
|
||||
|
||||
|
||||
+16
@@ -4,6 +4,7 @@ import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
enum class Theme {
|
||||
@@ -14,6 +15,7 @@ enum class Theme {
|
||||
|
||||
data class SettingsPreferences(
|
||||
val appTheme: Int = Theme.SYSTEM.ordinal,
|
||||
val whitelist: Set<String> = emptySet(),
|
||||
)
|
||||
|
||||
class SettingsPreferencesRepository(
|
||||
@@ -21,6 +23,7 @@ class SettingsPreferencesRepository(
|
||||
) {
|
||||
companion object {
|
||||
private val APP_THEME = intPreferencesKey("app_theme")
|
||||
private val WHITELIST = stringSetPreferencesKey("apps_whitelist")
|
||||
}
|
||||
|
||||
val preferences = dataStore.data.map { mapSettingsPreferences(it) }
|
||||
@@ -31,9 +34,22 @@ 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 {
|
||||
return SettingsPreferences(
|
||||
prefs[APP_THEME] ?: Theme.SYSTEM.ordinal,
|
||||
prefs[WHITELIST] ?: emptySet(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ 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.Apps
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.outlined.Palette
|
||||
import androidx.compose.material.icons.outlined.Translate
|
||||
@@ -61,6 +62,7 @@ private val Themes = hashMapOf(
|
||||
@Composable
|
||||
fun SettingsPage(
|
||||
goBack: () -> Unit,
|
||||
navigateToWhitelist: () -> Unit,
|
||||
viewModel: SettingsViewModel,
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
@@ -94,6 +96,7 @@ fun SettingsPage(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Appearance(viewModel, uiState)
|
||||
Listening(navigateToWhitelist)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,6 +141,35 @@ 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
|
||||
fun ThemeDialog(
|
||||
uiState: SettingsState,
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
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.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
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.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.viewmodels.SettingsViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun WhitelistPage(goBack: () -> Unit, viewModel: SettingsViewModel) {
|
||||
Scaffold(topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(text = stringResource(id = R.string.whitelist))
|
||||
},
|
||||
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 uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
Box(Modifier.padding(padding)) {
|
||||
LazyColumn {
|
||||
for ((packageName, appInfo) in uiState.applications) {
|
||||
item {
|
||||
var isWhitelisted by remember { mutableStateOf(appInfo.isWhitelisted) }
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
role = Role.Checkbox,
|
||||
onClick = {
|
||||
if (isWhitelisted) {
|
||||
viewModel.removeFromWhitelist(packageName)
|
||||
} else {
|
||||
viewModel.addToWhitelist(packageName)
|
||||
}
|
||||
isWhitelisted = !isWhitelisted
|
||||
}
|
||||
)) {
|
||||
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 = appInfo.icon, contentDescription = null)
|
||||
Text(appInfo.displayName, Modifier.weight(1f))
|
||||
Checkbox(
|
||||
checked = isWhitelisted,
|
||||
onCheckedChange = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
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.ViewModelProvider
|
||||
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
|
||||
@@ -14,13 +17,21 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class AppInfo(
|
||||
val displayName: String,
|
||||
val icon: Drawable,
|
||||
var isWhitelisted: Boolean,
|
||||
)
|
||||
|
||||
data class SettingsState(
|
||||
val appTheme: Int = Theme.SYSTEM.ordinal,
|
||||
val applications: MutableMap<String, AppInfo> = mutableMapOf(),
|
||||
)
|
||||
|
||||
class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesRepository): ViewModel() {
|
||||
class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesRepository, context: Context): ViewModel() {
|
||||
private val _uiState = MutableStateFlow(SettingsState())
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
@@ -34,7 +45,56 @@ class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesReposi
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getWhitelistedApps(): Set<String> {
|
||||
return settingsPrefsRepo.preferences.first().whitelist
|
||||
}
|
||||
|
||||
private fun getPackages(context: Context) {
|
||||
if (uiState.value.applications.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
|
||||
)
|
||||
}
|
||||
_uiState.update {
|
||||
SettingsState(
|
||||
appTheme = uiState.value.appTheme,
|
||||
applications = apps,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addToWhitelist(packageName: String) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
_uiState.value.applications[packageName]?.let { it.isWhitelisted = true }
|
||||
settingsPrefsRepo.addToWhitelist(packageName)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeFromWhitelist(packageName: String) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
_uiState.value.applications[packageName]?.let { it.isWhitelisted = false }
|
||||
settingsPrefsRepo.removeFromWhitelist(packageName)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
getPackages(context)
|
||||
viewModelScope.launch {
|
||||
settingsPrefsRepo.preferences.collectLatest {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
@@ -48,7 +108,7 @@ class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesReposi
|
||||
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
||||
initializer {
|
||||
val application = (this[APPLICATION_KEY] as LGApp)
|
||||
SettingsViewModel(application.settingsPreferencesRepository)
|
||||
SettingsViewModel(application.settingsPreferencesRepository, application.applicationContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,10 @@
|
||||
<string name="theme_system">استخدم مظهر النظام</string>
|
||||
<string name="language">اللغة</string>
|
||||
<string name="_default">الإفتراضي</string>
|
||||
<string name="listening">الإستماع</string>
|
||||
<string name="whitelist">القائمة البيضاء</string>
|
||||
<string name="whitelist_apps">تطبيقات القائمة البيضاء</string>
|
||||
<string name="whitelist_desc">اختر التطبيقات التي سيستمع التطبيق إلى إشعاراتها</string>
|
||||
|
||||
<!-- Lyrics -->
|
||||
<string name="lyrics">كلمات الأغنية</string>
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
<string name="theme_system">Use System Theme</string>
|
||||
<string name="language">Language</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>
|
||||
|
||||
<!-- Lyrics -->
|
||||
<string name="lyrics">Lyrics</string>
|
||||
|
||||
Reference in New Issue
Block a user