Refactor task list banners

pull/3226/head
Alex Baker 11 months ago
parent 80f24fdc17
commit 791041cc5d

@ -38,7 +38,7 @@ class TaskListViewModelTest : InjectingTestCase() {
override fun setUp() {
super.setUp()
viewModel = TaskListViewModel(
context = context,
applicationContext = context,
preferences = preferences,
taskDao = taskDao,
deletionDao = deletionDao,

@ -27,8 +27,9 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.runtime.getValue
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.ui.platform.LocalContext
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.ShareCompat
@ -49,7 +50,6 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionStatus
import com.google.accompanist.permissions.rememberPermissionState
import com.google.accompanist.permissions.shouldShowRationale
import com.google.android.material.appbar.AppBarLayout
@ -91,7 +91,6 @@ import org.tasks.compose.FilterSelectionActivity.Companion.registerForListPicker
import org.tasks.compose.NotificationsDisabledBanner
import org.tasks.compose.QuietHoursBanner
import org.tasks.compose.SubscriptionNagBanner
import org.tasks.compose.rememberReminderPermissionState
import org.tasks.data.TaskContainer
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.TagDataDao
@ -142,6 +141,7 @@ import org.tasks.themes.TasksTheme
import org.tasks.themes.Theme
import org.tasks.themes.ThemeColor
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import org.tasks.ui.Banner
import org.tasks.ui.TaskEditEvent
import org.tasks.ui.TaskEditEventBus
import org.tasks.ui.TaskListEvent
@ -269,7 +269,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
.launchIn(viewLifecycleOwner.lifecycleScope)
}
@OptIn(ExperimentalAnimationApi::class, ExperimentalPermissionsApi::class)
@OptIn(ExperimentalPermissionsApi::class)
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
requireActivity().onBackPressedDispatcher.addCallback(owner = viewLifecycleOwner) {
@ -362,65 +362,84 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
val context = LocalContext.current
val state = listViewModel.state.collectAsStateWithLifecycle().value
TasksTheme(theme = theme.themeBase.index) {
val hasRemindersPermission by rememberReminderPermissionState()
val notificationPermissions = if (AndroidUtilities.atLeastTiramisu()) {
rememberPermissionState(
Manifest.permission.POST_NOTIFICATIONS,
onPermissionResult = { success ->
if (success) {
NotificationSchedulerIntentService.enqueueWork(context)
listViewModel.dismissNotificationBanner(fix = true)
listViewModel.dismissBanner(tookAction = true)
}
}
)
} else {
null
}
val showNotificationBanner = state.warnNotificationsDisabled && notificationPermissions?.status is PermissionStatus.Denied
val showAlarmsBanner = !showNotificationBanner && state.warnNotificationsDisabled && !hasRemindersPermission
val showSubscriptionNag = !showNotificationBanner && !showAlarmsBanner && state.begForSubscription
val showQuietHoursWarning = !showNotificationBanner && !showAlarmsBanner && !showSubscriptionNag && state.warnQuietHoursEnabled
NotificationsDisabledBanner(
visible = showNotificationBanner,
settings = {
if (notificationPermissions?.status?.shouldShowRationale == true) {
context.openAppNotificationSettings()
} else {
notificationPermissions?.launchPermissionRequest()
}
},
dismiss = { listViewModel.dismissNotificationBanner() },
)
AlarmsDisabledBanner(
visible = showAlarmsBanner,
settings = { context.openReminderSettings() },
dismiss = { listViewModel.dismissNotificationBanner() },
)
SubscriptionNagBanner(
visible = showSubscriptionNag,
subscribe = {
listViewModel.dismissPurchaseBanner(clickedPurchase = true)
if (TasksApplication.IS_GOOGLE_PLAY) {
context.startActivity(Intent(context, PurchaseActivity::class.java))
} else {
preferences.lastSubscribeRequest = currentTimeMillis()
context.openUri(R.string.url_donate)
}
},
dismiss = {
listViewModel.dismissPurchaseBanner(clickedPurchase = false)
},
)
QuietHoursBanner(
visible = showQuietHoursWarning,
showSettings = {
listViewModel.dismissQuietHoursBanner()
context.startActivity(Intent(context, MainPreferences::class.java))
},
dismiss = {
listViewModel.dismissQuietHoursBanner()
AnimatedVisibility(
visible = state.banner != null,
enter = expandVertically(),
exit = shrinkVertically(),
) {
when (state.banner) {
is Banner.NotificationsDisabled ->
NotificationsDisabledBanner(
settings = {
if (notificationPermissions?.status?.shouldShowRationale == true) {
context.openAppNotificationSettings()
} else {
notificationPermissions?.launchPermissionRequest()
}
},
dismiss = { listViewModel.dismissBanner() },
)
Banner.AlarmsDisabled ->
AlarmsDisabledBanner(
settings = { context.openReminderSettings() },
dismiss = { listViewModel.dismissBanner() },
)
Banner.BegForMoney ->
SubscriptionNagBanner(
subscribe = {
listViewModel.dismissBanner(tookAction = true)
if (TasksApplication.IS_GOOGLE_PLAY) {
context.startActivity(
Intent(
context,
PurchaseActivity::class.java
)
)
} else {
preferences.lastSubscribeRequest = currentTimeMillis()
context.openUri(R.string.url_donate)
}
},
dismiss = {
listViewModel.dismissBanner()
},
)
Banner.QuietHoursEnabled ->
QuietHoursBanner(
showSettings = {
listViewModel.dismissBanner()
context.startActivity(
Intent(
context,
MainPreferences::class.java
)
)
},
dismiss = {
listViewModel.dismissBanner()
}
)
null -> {}
}
)
}
}
}
return binding.root

@ -1,24 +1,21 @@
package org.tasks.compose
import android.content.res.Configuration
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import org.tasks.R
import org.tasks.TasksApplication
import org.tasks.compose.components.AnimatedBanner
import org.tasks.compose.components.Banner
import org.tasks.themes.TasksTheme
@ExperimentalAnimationApi
@Composable
fun NotificationsDisabledBanner(
visible: Boolean,
settings: () -> Unit,
dismiss: () -> Unit,
) {
AnimatedBanner(
visible = visible,
Banner(
title = stringResource(id = R.string.enable_reminders),
body = stringResource(id = R.string.enable_reminders_description),
dismissText = stringResource(id = R.string.dismiss),
@ -28,15 +25,12 @@ fun NotificationsDisabledBanner(
)
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AlarmsDisabledBanner(
visible: Boolean,
settings: () -> Unit,
dismiss: () -> Unit,
) {
AnimatedBanner(
visible = visible,
Banner(
title = stringResource(id = R.string.enable_alarms),
body = stringResource(id = R.string.enable_alarms_description),
dismissText = stringResource(id = R.string.dismiss),
@ -47,15 +41,12 @@ fun AlarmsDisabledBanner(
}
@ExperimentalAnimationApi
@Composable
fun SubscriptionNagBanner(
visible: Boolean,
subscribe: () -> Unit,
dismiss: () -> Unit,
) {
AnimatedBanner(
visible = visible,
Banner(
title = stringResource(id = R.string.enjoying_tasks),
body = stringResource(id = if (TasksApplication.IS_GENERIC) {
R.string.donate_nag
@ -73,15 +64,12 @@ fun SubscriptionNagBanner(
)
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun QuietHoursBanner(
visible: Boolean,
showSettings: () -> Unit,
dismiss: () -> Unit,
) {
AnimatedBanner(
visible = visible,
Banner(
title = stringResource(R.string.quiet_hours_in_effect),
body = stringResource(R.string.quiet_hours_summary),
dismissText = stringResource(id = R.string.dismiss),
@ -91,7 +79,6 @@ fun QuietHoursBanner(
)
}
@ExperimentalAnimationApi
@Composable
fun BeastModeBanner(
visible: Boolean,
@ -109,15 +96,13 @@ fun BeastModeBanner(
)
}
@ExperimentalAnimationApi
@Preview(showBackground = true)
@Preview(showBackground = true, backgroundColor = 0x202124, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun NotificationsDisabledPreview() = TasksTheme {
NotificationsDisabledBanner(visible = true, settings = {}, dismiss = {})
NotificationsDisabledBanner(settings = {}, dismiss = {})
}
@ExperimentalAnimationApi
@Preview(showBackground = true)
@Preview(showBackground = true, backgroundColor = 0x202124, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
@ -125,18 +110,16 @@ private fun BeastModePreview() = TasksTheme {
BeastModeBanner(visible = true, showSettings = {}, dismiss = {})
}
@ExperimentalAnimationApi
@Preview(showBackground = true)
@Preview(showBackground = true, backgroundColor = 0x202124, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun SubscriptionNagPreview() = TasksTheme {
SubscriptionNagBanner(visible = true, subscribe = {}, dismiss = {})
SubscriptionNagBanner(subscribe = {}, dismiss = {})
}
@ExperimentalAnimationApi
@Preview(showBackground = true)
@Preview(showBackground = true, backgroundColor = 0x202124, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun QuietHoursPreview() = TasksTheme {
QuietHoursBanner(visible = true, showSettings = {}, dismiss = {})
QuietHoursBanner(showSettings = {}, dismiss = {})
}

@ -42,9 +42,13 @@ public class PermissionChecker {
return checkPermissions(backgroundPermissions().toArray(new String[0]));
}
public boolean hasNotificationPermission() {
return !atLeastTiramisu() || checkPermissions(permission.POST_NOTIFICATIONS);
}
public boolean canNotify() {
return org.tasks.extensions.Context.INSTANCE.canScheduleExactAlarms(context)
&& (!atLeastTiramisu() || checkPermissions(permission.POST_NOTIFICATIONS));
&& hasNotificationPermission();
}
private boolean checkPermissions(String... permissions) {

@ -562,6 +562,10 @@ class Preferences @JvmOverloads constructor(
get() = getBoolean(R.string.p_warn_notifications_disabled, true)
set(value) = setBoolean(R.string.p_warn_notifications_disabled, value)
var warnAlarmsDisabled: Boolean
get() = getBoolean(R.string.p_warn_alarms_disabled, true)
set(value) = setBoolean(R.string.p_warn_alarms_disabled, value)
var warnQuietHoursDisabled: Boolean
get() = getBoolean(R.string.p_warn_quiet_hours_enabled, true)
set(value) = setBoolean(R.string.p_warn_quiet_hours_enabled, value)

@ -1,6 +1,5 @@
package org.tasks.ui
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
@ -32,12 +31,14 @@ import org.tasks.data.dao.TaskDao
import org.tasks.data.entity.Task
import org.tasks.data.fetchTasks
import org.tasks.db.QueryUtils
import org.tasks.extensions.Context.canScheduleExactAlarms
import org.tasks.filters.AstridOrderingFilter
import org.tasks.filters.EmptyFilter
import org.tasks.filters.Filter
import org.tasks.filters.FilterImpl
import org.tasks.filters.MyTasksFilter
import org.tasks.filters.SearchFilter
import org.tasks.preferences.PermissionChecker
import org.tasks.preferences.Preferences
import org.tasks.preferences.QueryPreferences
import org.tasks.tasklist.SectionedDataSource
@ -45,10 +46,16 @@ import org.tasks.tasklist.TasksResults
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject
sealed interface Banner {
data object NotificationsDisabled : Banner
data object AlarmsDisabled : Banner
data object QuietHoursEnabled : Banner
data object BegForMoney : Banner
}
@HiltViewModel
@SuppressLint("StaticFieldLeak")
class TaskListViewModel @Inject constructor(
@ApplicationContext context: Context,
@ApplicationContext private val applicationContext: Context,
private val preferences: Preferences,
private val taskDao: TaskDao,
private val taskDeleter: TaskDeleter,
@ -56,19 +63,17 @@ class TaskListViewModel @Inject constructor(
private val localBroadcastManager: LocalBroadcastManager,
private val inventory: Inventory,
private val firebase: Firebase,
private val permissionChecker: PermissionChecker,
) : ViewModel() {
data class State(
val filter: Filter = EmptyFilter(),
val now: Long = currentTimeMillis(),
val searchQuery: String? = null,
val tasks: TasksResults = TasksResults.Loading,
val begForSubscription: Boolean = false,
val warnNotificationsDisabled: Boolean = false,
val syncOngoing: Boolean = false,
val warnQuietHoursEnabled: Boolean = false,
val collapsed: Set<Long> = setOf(SectionedDataSource.HEADER_COMPLETED),
val banner: Banner? = null
)
private val _state = MutableStateFlow(State())
@ -99,23 +104,6 @@ class TaskListViewModel @Inject constructor(
}
}
fun dismissNotificationBanner(
fix: Boolean = false,
) {
_state.update {
it.copy(warnNotificationsDisabled = false)
}
preferences.warnNotificationsDisabled = fix
}
fun dismissPurchaseBanner(clickedPurchase: Boolean) {
_state.update {
it.copy(begForSubscription = false)
}
preferences.lastSubscribeRequest = currentTimeMillis()
firebase.logEvent(R.string.event_banner_sub, R.string.param_click to clickedPurchase)
}
suspend fun getTasksToClear(): List<Long> {
val filter = _state.value.filter
val deleteFilter = FilterImpl(
@ -150,7 +138,7 @@ class TaskListViewModel @Inject constructor(
val filter = when {
it.searchQuery == null -> it.filter
it.searchQuery.isBlank() -> MyTasksFilter.create()
else -> context.createSearchQuery(it.searchQuery)
else -> applicationContext.createSearchQuery(it.searchQuery)
}
taskDao.fetchTasks { getQuery(preferences, filter) }
}
@ -200,26 +188,36 @@ class TaskListViewModel @Inject constructor(
fun updateBannerState() {
viewModelScope.launch(Dispatchers.IO) {
_state.update {
it.copy(
warnNotificationsDisabled = preferences.warnNotificationsDisabled,
warnQuietHoursEnabled = preferences.isCurrentlyQuietHours && preferences.warnQuietHoursDisabled,
)
val banner = when {
preferences.warnNotificationsDisabled && !permissionChecker.hasNotificationPermission() ->
Banner.NotificationsDisabled
preferences.warnAlarmsDisabled && !applicationContext.canScheduleExactAlarms() ->
Banner.AlarmsDisabled
(IS_GENERIC || !inventory.hasPro) && !firebase.subscribeCooldown ->
Banner.BegForMoney
preferences.isCurrentlyQuietHours && preferences.warnQuietHoursDisabled ->
Banner.QuietHoursEnabled
else -> null
}
if ((IS_GENERIC || !inventory.hasPro) && !firebase.subscribeCooldown) {
_state.update {
it.copy(begForSubscription = true)
}
_state.update {
it.copy(banner = banner)
}
}
}
fun dismissQuietHoursBanner() = viewModelScope.launch(Dispatchers.IO) {
preferences.warnQuietHoursDisabled = false
_state.update {
it.copy(warnQuietHoursEnabled = false)
fun dismissBanner(tookAction: Boolean = false) {
when (state.value.banner) {
Banner.NotificationsDisabled ->preferences.warnNotificationsDisabled = tookAction
Banner.AlarmsDisabled -> preferences.warnAlarmsDisabled = false
Banner.QuietHoursEnabled -> preferences.warnQuietHoursDisabled = false
Banner.BegForMoney -> {
preferences.lastSubscribeRequest = currentTimeMillis()
firebase.logEvent(R.string.event_banner_sub, R.string.param_click to tookAction)
}
else -> {}
}
updateBannerState()
}
companion object {

@ -407,6 +407,7 @@
<string name="p_install_date">install_date</string>
<string name="p_default_location">default_location</string>
<string name="p_warn_notifications_disabled">warn_notifications_disabled</string>
<string name="p_warn_alarms_disabled">warn_alarms_disabled</string>
<string name="p_warn_quiet_hours_enabled">warn_quiet_hours_enabled</string>
<string name="event_showed_purchase_dialog">showed_purchase_dialog</string>

@ -1,3 +1,5 @@
@file:OptIn(ExperimentalAnimationApi::class)
package org.tasks.compose.components
import androidx.compose.animation.AnimatedVisibility
@ -19,7 +21,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@ExperimentalAnimationApi
@Composable
fun AnimatedBanner(
visible: Boolean,
@ -30,8 +31,32 @@ fun AnimatedBanner(
action: String,
onAction: () -> Unit,
) {
AnimatedBanner(
AnimatedVisibility(
visible = visible,
enter = expandVertically(),
exit = shrinkVertically(),
) {
Banner(
title = title,
body = body,
dismissText = dismissText,
onDismiss = onDismiss,
action = action,
onAction = onAction,
)
}
}
@Composable
fun Banner(
title: String,
body: String,
dismissText: String? = null,
onDismiss: () -> Unit,
action: String,
onAction: () -> Unit,
) {
Banner(
content = {
Text(
text = title,
@ -58,31 +83,24 @@ fun AnimatedBanner(
@ExperimentalAnimationApi
@Composable
private fun AnimatedBanner(
visible: Boolean,
private fun Banner(
content: @Composable () -> Unit,
buttons: @Composable () -> Unit,
) {
AnimatedVisibility(
visible = visible,
enter = expandVertically(),
exit = shrinkVertically(),
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Card(
Spacer(modifier = Modifier.height(16.dp))
content()
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.padding(vertical = 8.dp, horizontal = 16.dp)
.align(Alignment.End),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Spacer(modifier = Modifier.height(16.dp))
content()
Row(
modifier = Modifier
.padding(vertical = 8.dp, horizontal = 16.dp)
.align(Alignment.End),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
buttons()
}
buttons()
}
}
}

Loading…
Cancel
Save