diff --git a/app/.gitignore b/app/.gitignore index 67e07b8..411f30b 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1,2 +1,3 @@ /build /release +/nightly diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d117f67..ad9271f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -18,8 +18,8 @@ android { applicationId = "co.adityarajput.notifilter" minSdk = 29 targetSdk = 36 - versionCode = 19 - versionName = "4.2.0" + versionCode = 20 + versionName = "4.3.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/java/co/adityarajput/notifilter/data/FilterDao.kt b/app/src/main/java/co/adityarajput/notifilter/data/FilterDao.kt index d25bd56..707c2a6 100644 --- a/app/src/main/java/co/adityarajput/notifilter/data/FilterDao.kt +++ b/app/src/main/java/co/adityarajput/notifilter/data/FilterDao.kt @@ -18,7 +18,7 @@ interface FilterDao { @Query("UPDATE filters SET hits = hits + 1 WHERE id = :id") suspend fun registerHit(id: Int) - @Query("UPDATE filters SET historyEnabled = 1 - historyEnabled WHERE id = :id") + @Query("UPDATE filters SET historyEnabled = 1 - historyEnabled, hits = 0 WHERE id = :id") suspend fun toggleHistory(id: Int) @Query("UPDATE filters SET enabled = 1 - enabled WHERE id = :id") diff --git a/app/src/main/java/co/adityarajput/notifilter/data/NotificationDao.kt b/app/src/main/java/co/adityarajput/notifilter/data/NotificationDao.kt index 9ff23ff..9227942 100644 --- a/app/src/main/java/co/adityarajput/notifilter/data/NotificationDao.kt +++ b/app/src/main/java/co/adityarajput/notifilter/data/NotificationDao.kt @@ -1,6 +1,7 @@ package co.adityarajput.notifilter.data import androidx.room.Dao +import androidx.room.Delete import androidx.room.Query import androidx.room.Upsert import co.adityarajput.notifilter.data.models.Notification @@ -17,6 +18,12 @@ interface NotificationDao { @Query("SELECT COUNT(*) FROM notifications") suspend fun count(): Int + @Delete + suspend fun delete(notification: Notification) + @Query("DELETE FROM notifications WHERE id IN (SELECT id FROM notifications ORDER BY timestamp ASC LIMIT :count)") suspend fun trim(count: Int) + + @Query("DELETE FROM notifications") + suspend fun deleteAll() } diff --git a/app/src/main/java/co/adityarajput/notifilter/data/Repository.kt b/app/src/main/java/co/adityarajput/notifilter/data/Repository.kt index c8ee48d..ef0e224 100644 --- a/app/src/main/java/co/adityarajput/notifilter/data/Repository.kt +++ b/app/src/main/java/co/adityarajput/notifilter/data/Repository.kt @@ -33,5 +33,9 @@ class Repository( suspend fun delete(filter: Filter) = filterDao.delete(filter) + suspend fun delete(notification: Notification) = notificationDao.delete(notification) + suspend fun deleteFilters() = filterDao.deleteAll() + + suspend fun deleteNotifications() = notificationDao.deleteAll() } diff --git a/app/src/main/java/co/adityarajput/notifilter/viewmodels/FiltersViewModel.kt b/app/src/main/java/co/adityarajput/notifilter/viewmodels/FiltersViewModel.kt index 82293b6..a564034 100644 --- a/app/src/main/java/co/adityarajput/notifilter/viewmodels/FiltersViewModel.kt +++ b/app/src/main/java/co/adityarajput/notifilter/viewmodels/FiltersViewModel.kt @@ -21,7 +21,7 @@ class FiltersViewModel(private val repository: Repository) : ViewModel() { .map { State(it) } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), State()) - var dialogState by mutableStateOf(null) + var dialogState by mutableStateOf(null) var selectedFilter by mutableStateOf(null) @@ -47,4 +47,4 @@ class FiltersViewModel(private val repository: Repository) : ViewModel() { } } -enum class DialogState { TOGGLE_HISTORY, TOGGLE_FILTER, DELETE } +enum class FilterDialogState { TOGGLE_HISTORY, TOGGLE_FILTER, DELETE } diff --git a/app/src/main/java/co/adityarajput/notifilter/viewmodels/NotificationsViewModel.kt b/app/src/main/java/co/adityarajput/notifilter/viewmodels/NotificationsViewModel.kt index 6790125..3b7d0a1 100644 --- a/app/src/main/java/co/adityarajput/notifilter/viewmodels/NotificationsViewModel.kt +++ b/app/src/main/java/co/adityarajput/notifilter/viewmodels/NotificationsViewModel.kt @@ -1,19 +1,44 @@ package co.adityarajput.notifilter.viewmodels +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import co.adityarajput.notifilter.data.Repository import co.adityarajput.notifilter.data.models.Notification +import co.adityarajput.notifilter.utils.Logger import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch -class NotificationsViewModel(repository: Repository) : ViewModel() { +class NotificationsViewModel(val repository: Repository) : ViewModel() { data class State(val notifications: List? = null) val state: StateFlow = repository.notifications() .map { State(it) } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), State()) + + var selectedNotification by mutableStateOf(null) + + var dialogState by mutableStateOf(null) + + fun delete(notification: Notification) { + viewModelScope.launch { + Logger.d("NotificationsViewModel", "Deleting $notification") + repository.delete(notification) + } + } + + fun clearHistory() { + viewModelScope.launch { + Logger.d("NotificationsViewModel", "Deleting all notifications") + repository.deleteNotifications() + } + } } + +enum class NotificationDialogState { CLEAR_HISTORY } diff --git a/app/src/main/java/co/adityarajput/notifilter/views/components/ManageFilterDialog.kt b/app/src/main/java/co/adityarajput/notifilter/views/components/ManageFilterDialog.kt index 3196353..c057944 100644 --- a/app/src/main/java/co/adityarajput/notifilter/views/components/ManageFilterDialog.kt +++ b/app/src/main/java/co/adityarajput/notifilter/views/components/ManageFilterDialog.kt @@ -8,7 +8,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import co.adityarajput.notifilter.R import co.adityarajput.notifilter.utils.getToggleString -import co.adityarajput.notifilter.viewmodels.DialogState +import co.adityarajput.notifilter.viewmodels.FilterDialogState import co.adityarajput.notifilter.viewmodels.FiltersViewModel @Composable @@ -22,33 +22,33 @@ fun ManageFilterDialog(viewModel: FiltersViewModel) { title = { Text( when (dialogState) { - DialogState.TOGGLE_HISTORY -> stringResource( + FilterDialogState.TOGGLE_HISTORY -> stringResource( R.string.toggle_history, filter.historyEnabled.getToggleString(), ) - DialogState.TOGGLE_FILTER -> stringResource( + FilterDialogState.TOGGLE_FILTER -> stringResource( R.string.toggle_filter, filter.enabled.getToggleString(), ) - DialogState.DELETE -> stringResource(R.string.delete_filter) + FilterDialogState.DELETE -> stringResource(R.string.delete_filter) }, ) }, text = { Text( when (dialogState) { - DialogState.TOGGLE_HISTORY -> stringResource( + FilterDialogState.TOGGLE_HISTORY -> stringResource( if (filter.historyEnabled) R.string.disable_history_confirmation else R.string.enable_history_confirmation, ) - DialogState.TOGGLE_FILTER -> stringResource( + FilterDialogState.TOGGLE_FILTER -> stringResource( R.string.toggle_filter_confirmation, filter.enabled.getToggleString(), ) - DialogState.DELETE -> stringResource( + FilterDialogState.DELETE -> stringResource( R.string.delete_confirmation, if (filter.enabled) stringResource(R.string.disable_suggestion) else "", ) @@ -60,22 +60,22 @@ fun ManageFilterDialog(viewModel: FiltersViewModel) { TextButton( { when (dialogState) { - DialogState.TOGGLE_HISTORY -> viewModel.toggleHistory() - DialogState.TOGGLE_FILTER -> viewModel.toggleFilter() - DialogState.DELETE -> viewModel.deleteFilter() + FilterDialogState.TOGGLE_HISTORY -> viewModel.toggleHistory() + FilterDialogState.TOGGLE_FILTER -> viewModel.toggleFilter() + FilterDialogState.DELETE -> viewModel.deleteFilter() } hideDialog() }, colors = ButtonDefaults.textButtonColors( - contentColor = if (dialogState == DialogState.DELETE) MaterialTheme.colorScheme.tertiary + contentColor = if (dialogState == FilterDialogState.DELETE) MaterialTheme.colorScheme.tertiary else Color.Unspecified, ), ) { Text( when (dialogState) { - DialogState.TOGGLE_HISTORY -> filter.historyEnabled.getToggleString() - DialogState.TOGGLE_FILTER -> filter.enabled.getToggleString() - DialogState.DELETE -> stringResource(R.string.delete) + FilterDialogState.TOGGLE_HISTORY -> filter.historyEnabled.getToggleString() + FilterDialogState.TOGGLE_FILTER -> filter.enabled.getToggleString() + FilterDialogState.DELETE -> stringResource(R.string.delete) }, ) } diff --git a/app/src/main/java/co/adityarajput/notifilter/views/components/ManageHistoryDialog.kt b/app/src/main/java/co/adityarajput/notifilter/views/components/ManageHistoryDialog.kt new file mode 100644 index 0000000..3ec3e8f --- /dev/null +++ b/app/src/main/java/co/adityarajput/notifilter/views/components/ManageHistoryDialog.kt @@ -0,0 +1,33 @@ +package co.adityarajput.notifilter.views.components + +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import co.adityarajput.notifilter.R +import co.adityarajput.notifilter.viewmodels.NotificationsViewModel + +@Composable +fun ManageHistoryDialog(viewModel: NotificationsViewModel) { + val hideDialog = { viewModel.dialogState = null } + + AlertDialog( + hideDialog, + title = { Text(stringResource(R.string.clear_history)) }, + text = { Text(stringResource(R.string.clear_history_confirmation)) }, + confirmButton = { + Row { + TextButton( + { viewModel.clearHistory(); hideDialog() }, + colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.tertiary), + ) { Text(stringResource(R.string.clear_history)) } + } + }, + dismissButton = { + TextButton(hideDialog) { + Text(stringResource(R.string.cancel), fontWeight = FontWeight.Normal) + } + }, + ) +} diff --git a/app/src/main/java/co/adityarajput/notifilter/views/components/Tile.kt b/app/src/main/java/co/adityarajput/notifilter/views/components/Tile.kt index ef88e8e..f382825 100644 --- a/app/src/main/java/co/adityarajput/notifilter/views/components/Tile.kt +++ b/app/src/main/java/co/adityarajput/notifilter/views/components/Tile.kt @@ -12,15 +12,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.dimensionResource -import androidx.compose.ui.res.pluralStringResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp import co.adityarajput.notifilter.R -import co.adityarajput.notifilter.data.models.* -import co.adityarajput.notifilter.utils.getFirst -import co.adityarajput.notifilter.utils.toShortHumanReadableTime -import co.adityarajput.notifilter.views.Theme @Composable fun Tile( @@ -92,96 +85,3 @@ fun Tile( } } } - -@Preview -@Composable -private fun FilterTiles() { - val filters = listOf( - Filter( - App("Clock", "com.google.android.deskclock"), - "Upcoming alarm", - Action.DISMISS, - RegexTarget.TITLE, - enabled = false, - ), - Filter( - App("Software update", "com.wssyncmldm"), - "software update", - Action.TAP_BUTTON("Remind me"), - RegexTarget.CONTENT, - schedule = Schedule(days = setOf(2, 3, 4, 5, 6)), - hits = 23, - ), - Filter( - App("Gmail", "com.google.android.gm"), - "[Nn]ewsletter", - Action.BATCH(3), - RegexTarget.OR, - historyEnabled = false, - ), - Filter( - App("WhatsApp", "com.whatsapp"), - "Book Club", - Action.DELAY, - RegexTarget.AND, - "^Bob", - schedule = Schedule(start = 9 * 60, end = 17 * 60), - hits = 15, - ), - Filter( - App("WhatsApp", "com.whatsapp"), - "Roommate", - Action.DEBOUNCE(2), - RegexTarget.TITLE, - historyEnabled = false, - ), - ) - - Theme { - Column { - for (filter in filters) - Tile( - buildString { - append("/") - append(filter.regexPattern) - append("/") - - if (filter.regexTarget == RegexTarget.AND) { - append(" && /") - append(filter.secondaryRegexPattern) - append("/") - } - }, - filter.action.verb(), - filter.app.name.getFirst(30), - if (!filter.enabled) stringResource(R.string.filter_disabled) - else if (!filter.historyEnabled) stringResource(R.string.history_disabled) - else pluralStringResource(R.plurals.hit, filter.hits, filter.hits), - filter.schedule.description, - { }, - { Text("BUTTONS") }, - filter.enabled, - ) - } - } -} - -@Preview -@Composable -private fun NotificationTile() { - val notification = Notification( - "Notification Title", - "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.", - "App Name", - System.currentTimeMillis() - 12345600, - ) - - Theme { - Tile( - notification.title, - notification.content, - notification.origin.getFirst(30), - notification.timestamp.toShortHumanReadableTime(), - ) - } -} diff --git a/app/src/main/java/co/adityarajput/notifilter/views/screens/FiltersScreen.kt b/app/src/main/java/co/adityarajput/notifilter/views/screens/FiltersScreen.kt index c3a123d..618690a 100644 --- a/app/src/main/java/co/adityarajput/notifilter/views/screens/FiltersScreen.kt +++ b/app/src/main/java/co/adityarajput/notifilter/views/screens/FiltersScreen.kt @@ -23,7 +23,7 @@ import co.adityarajput.notifilter.data.models.Any import co.adityarajput.notifilter.data.models.RegexTarget import co.adityarajput.notifilter.utils.getFirst import co.adityarajput.notifilter.utils.getToggleString -import co.adityarajput.notifilter.viewmodels.DialogState +import co.adityarajput.notifilter.viewmodels.FilterDialogState import co.adityarajput.notifilter.viewmodels.FiltersViewModel import co.adityarajput.notifilter.viewmodels.Provider import co.adityarajput.notifilter.views.components.AppBar @@ -112,7 +112,11 @@ fun FiltersScreen( else viewModel.selectedFilter = it }, { - IconButton({ viewModel.dialogState = DialogState.TOGGLE_HISTORY }) { + IconButton( + { + viewModel.dialogState = FilterDialogState.TOGGLE_HISTORY + }, + ) { Icon( painterResource(R.drawable.manage_history), stringResource( @@ -121,7 +125,11 @@ fun FiltersScreen( ), ) } - IconButton({ viewModel.dialogState = DialogState.TOGGLE_FILTER }) { + IconButton( + { + viewModel.dialogState = FilterDialogState.TOGGLE_FILTER + }, + ) { Icon( if (it.enabled) painterResource(R.drawable.archive) else painterResource(R.drawable.unarchive), @@ -138,7 +146,7 @@ fun FiltersScreen( ) } IconButton( - { viewModel.dialogState = DialogState.DELETE }, + { viewModel.dialogState = FilterDialogState.DELETE }, colors = IconButtonDefaults.iconButtonColors( contentColor = MaterialTheme.colorScheme.tertiary, ), diff --git a/app/src/main/java/co/adityarajput/notifilter/views/screens/NotificationsScreen.kt b/app/src/main/java/co/adityarajput/notifilter/views/screens/NotificationsScreen.kt index 36ad70e..9664ff5 100644 --- a/app/src/main/java/co/adityarajput/notifilter/views/screens/NotificationsScreen.kt +++ b/app/src/main/java/co/adityarajput/notifilter/views/screens/NotificationsScreen.kt @@ -5,24 +5,24 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.lifecycle.viewmodel.compose.viewModel import co.adityarajput.notifilter.R import co.adityarajput.notifilter.utils.getFirst import co.adityarajput.notifilter.utils.toShortHumanReadableTime +import co.adityarajput.notifilter.viewmodels.NotificationDialogState import co.adityarajput.notifilter.viewmodels.NotificationsViewModel import co.adityarajput.notifilter.viewmodels.Provider import co.adityarajput.notifilter.views.components.AppBar +import co.adityarajput.notifilter.views.components.ManageHistoryDialog import co.adityarajput.notifilter.views.components.Tile @Composable @@ -34,11 +34,15 @@ fun NotificationsScreen( Scaffold( topBar = { - AppBar( - stringResource(R.string.history), - true, - goBack, - ) + AppBar(stringResource(R.string.history), true, goBack) { + IconButton({ viewModel.dialogState = NotificationDialogState.CLEAR_HISTORY }) { + Icon( + painterResource(R.drawable.clear_all), + stringResource(R.string.clear_history), + tint = MaterialTheme.colorScheme.onSurface, + ) + } + } }, ) { paddingValues -> if (state.value.notifications == null) { @@ -67,9 +71,31 @@ fun NotificationsScreen( it.content, it.origin.getFirst(30), it.timestamp.toShortHumanReadableTime(), + null, + { + if (viewModel.selectedNotification == it) viewModel.selectedNotification = + null + else viewModel.selectedNotification = it + }, + { + IconButton( + { viewModel.delete(it) }, + colors = IconButtonDefaults.iconButtonColors( + contentColor = MaterialTheme.colorScheme.tertiary, + ), + ) { + Icon( + painterResource(R.drawable.delete), + stringResource(R.string.delete), + ) + } + }, + viewModel.selectedNotification == it, ) } } } + if (viewModel.dialogState != null) + ManageHistoryDialog(viewModel) } } diff --git a/app/src/main/res/drawable/clear_all.xml b/app/src/main/res/drawable/clear_all.xml new file mode 100644 index 0000000..6327e6e --- /dev/null +++ b/app/src/main/res/drawable/clear_all.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d38e071..415c550 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,7 +1,7 @@ NotiFilter NotiFilter - 4.2.0 + 4.3.0 No filters added.\nTap + to get started. "on weekdays " @@ -92,7 +92,7 @@ Save %1$s history - Disable history for this filter? Dismissed notifications will not be saved. + Disable history and reset hits for this filter? Dismissed notifications will not be saved. Enable history for this filter? %1$s filter %1$s this filter? @@ -106,6 +106,9 @@ History No notifications blocked yet. + Clear history + Delete all notifications? + NotiFilter runs a low-impact background service that listens to all device notifications and quietly manages them according to your filters.\n\nNo data is sent off-device; NotiFilter doesn\'t have access to the internet. Grant permission On some devices, the \'battery optimization\' feature leads to delayed notification dismissal, so you may still experience the initial disruption.\n\nCertain manufacturers have even more aggressive optimizations, so you may need to enable \"Run in foreground\" from the app settings later. diff --git a/metadata/en-US/changelogs/20.txt b/metadata/en-US/changelogs/20.txt new file mode 100644 index 0000000..0d13fc3 --- /dev/null +++ b/metadata/en-US/changelogs/20.txt @@ -0,0 +1,2 @@ +• feat: Let user delete notifications +• fix: Reset filter's hits when disabling history diff --git a/metadata/en-US/images/phoneScreenshots/1.png b/metadata/en-US/images/phoneScreenshots/1.png index 28743ed..b078d8f 100644 Binary files a/metadata/en-US/images/phoneScreenshots/1.png and b/metadata/en-US/images/phoneScreenshots/1.png differ diff --git a/metadata/en-US/images/phoneScreenshots/2.png b/metadata/en-US/images/phoneScreenshots/2.png index 6772bdd..cab0dba 100644 Binary files a/metadata/en-US/images/phoneScreenshots/2.png and b/metadata/en-US/images/phoneScreenshots/2.png differ