Use SCHEDULE_EXACT_ALARM

pull/2928/head
Alex Baker 1 week ago
parent a4cd0829b0
commit d8186e5fe4

@ -30,8 +30,7 @@
<!-- ************* -->
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<!-- *************************** -->
<!-- google calendar integration -->
@ -469,7 +468,14 @@
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</receiver>
<receiver
android:name=".receivers.ScheduleExactAlarmsPermissionReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED" />
</intent-filter>
</receiver>

@ -88,6 +88,7 @@ object AndroidUtilities {
return Build.VERSION.SDK_INT >= VERSION_CODES.S
}
@JvmStatic
fun atLeastTiramisu(): Boolean {
return Build.VERSION.SDK_INT >= VERSION_CODES.TIRAMISU
}

@ -5,6 +5,7 @@
*/
package com.todoroo.astrid.activity
import android.Manifest
import android.app.Activity
import android.app.Activity.RESULT_OK
import android.content.BroadcastReceiver
@ -27,6 +28,7 @@ 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.ui.platform.LocalContext
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.ShareCompat
@ -45,9 +47,14 @@ import androidx.recyclerview.widget.LinearLayoutManager
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
import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.snackbar.Snackbar
import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.adapter.TaskAdapter
import com.todoroo.astrid.adapter.TaskAdapterProvider
@ -85,8 +92,10 @@ import org.tasks.billing.PurchaseActivity
import org.tasks.caldav.BaseCaldavCalendarSettingsActivity
import org.tasks.compose.FilterSelectionActivity.Companion.launch
import org.tasks.compose.FilterSelectionActivity.Companion.registerForListPickerResult
import org.tasks.compose.NotificationsDisabledBanner
import org.tasks.compose.SubscriptionNagBanner
import org.tasks.compose.collectAsStateLifecycleAware
import org.tasks.compose.rememberReminderPermissionState
import org.tasks.data.TaskContainer
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.TagDataDao
@ -103,6 +112,9 @@ import org.tasks.dialogs.DateTimePicker.Companion.newDateTimePicker
import org.tasks.dialogs.DialogBuilder
import org.tasks.dialogs.PriorityPicker.Companion.newPriorityPicker
import org.tasks.dialogs.SortSettingsActivity
import org.tasks.extensions.Context.canScheduleExactAlarms
import org.tasks.extensions.Context.openAppNotificationSettings
import org.tasks.extensions.Context.openReminderSettings
import org.tasks.extensions.Context.openUri
import org.tasks.extensions.Context.toast
import org.tasks.extensions.Fragment.safeStartActivityForResult
@ -116,6 +128,7 @@ import org.tasks.filters.PlaceFilter
import org.tasks.markdown.MarkdownProvider
import org.tasks.preferences.Device
import org.tasks.preferences.Preferences
import org.tasks.scheduling.NotificationSchedulerIntentService
import org.tasks.sync.SyncAdapters
import org.tasks.tags.TagPickerActivity
import org.tasks.tasklist.DragAndDropRecyclerAdapter
@ -168,7 +181,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
@Inject lateinit var taskEditEventBus: TaskEditEventBus
@Inject lateinit var database: Database
@Inject lateinit var markdown: MarkdownProvider
private val listViewModel: TaskListViewModel by viewModels()
private val mainViewModel: MainActivityViewModel by activityViewModels()
private lateinit var taskAdapter: TaskAdapter
@ -253,7 +266,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
.launchIn(viewLifecycleOwner.lifecycleScope)
}
@OptIn(ExperimentalAnimationApi::class)
@OptIn(ExperimentalAnimationApi::class, ExperimentalPermissionsApi::class)
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
requireActivity().onBackPressedDispatcher.addCallback(owner = viewLifecycleOwner) {
@ -289,7 +302,8 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
(recyclerView.itemAnimator as DefaultItemAnimator).supportsChangeAnimations = false
recyclerView.layoutManager = LinearLayoutManager(context)
lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
listViewModel.updateBannerState()
listViewModel.state.collect {
if (it.tasks is TaskListViewModel.TasksResults.Results) {
submitList(it.tasks.tasks)
@ -343,12 +357,41 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
setupMenu(toolbar)
binding.banner.setContent {
val context = LocalContext.current
val showBanner = listViewModel.state.collectAsStateLifecycleAware().value.begForSubscription
val state = listViewModel.state.collectAsStateLifecycleAware().value
TasksTheme {
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)
}
}
)
} else {
null
}
val showNotificationBanner = state.warnNotificationsDisabled &&
(!hasRemindersPermission || notificationPermissions?.status is PermissionStatus.Denied)
NotificationsDisabledBanner(
visible = showNotificationBanner,
settings = {
if (!context.canScheduleExactAlarms()) {
context.openReminderSettings()
} else if (notificationPermissions?.status?.shouldShowRationale == true) {
context.openAppNotificationSettings()
} else {
notificationPermissions?.launchPermissionRequest()
}
},
dismiss = { listViewModel.dismissNotificationBanner() },
)
SubscriptionNagBanner(
visible = showBanner,
visible = state.begForSubscription && !showNotificationBanner,
subscribe = {
listViewModel.dismissBanner(clickedPurchase = true)
listViewModel.dismissPurchaseBanner(clickedPurchase = true)
if (Tasks.IS_GOOGLE_PLAY) {
context.startActivity(Intent(context, PurchaseActivity::class.java))
} else {
@ -357,7 +400,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
}
},
dismiss = {
listViewModel.dismissBanner(clickedPurchase = false)
listViewModel.dismissPurchaseBanner(clickedPurchase = false)
},
)
}

@ -23,11 +23,13 @@ import org.tasks.R
import org.tasks.activities.DateAndTimePickerActivity
import org.tasks.compose.collectAsStateLifecycleAware
import org.tasks.compose.edit.AlarmRow
import org.tasks.compose.rememberReminderPermissionState
import org.tasks.data.entity.Alarm
import org.tasks.data.entity.Alarm.Companion.TYPE_DATE_TIME
import org.tasks.date.DateTimeUtils
import org.tasks.dialogs.DialogBuilder
import org.tasks.dialogs.MyTimePickerDialog
import org.tasks.extensions.Context.openReminderSettings
import org.tasks.scheduling.NotificationSchedulerIntentService
import org.tasks.themes.TasksTheme
import org.tasks.ui.TaskEditControlFragment
@ -78,6 +80,7 @@ class ReminderControlSet : TaskEditControlFragment() {
setContent {
TasksTheme {
val ringMode by remember { this@ReminderControlSet.ringMode }
val hasReminderPermissions by rememberReminderPermissionState()
val notificationPermissions = if (AndroidUtilities.atLeastTiramisu()) {
rememberPermissionState(
Manifest.permission.POST_NOTIFICATIONS,
@ -103,10 +106,14 @@ class ReminderControlSet : TaskEditControlFragment() {
AlarmRow(
locale = locale,
alarms = viewModel.selectedAlarms.collectAsStateLifecycleAware().value,
permissionStatus = notificationPermissions?.status
?: PermissionStatus.Granted,
launchPermissionRequest = {
notificationPermissions?.launchPermissionRequest()
hasNotificationPermissions = hasReminderPermissions &&
(notificationPermissions == null || notificationPermissions.status == PermissionStatus.Granted),
fixNotificationPermissions = {
if (hasReminderPermissions) {
notificationPermissions?.launchPermissionRequest()
} else {
context.openReminderSettings()
}
},
ringMode = ringMode,
addAlarm = viewModel::addAlarm,

@ -1,61 +1,31 @@
package org.tasks.compose
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.tasks.R
import org.tasks.Tasks
import org.tasks.compose.components.AnimatedBanner
import org.tasks.themes.TasksTheme
@ExperimentalAnimationApi
@Composable
fun AnimatedBanner(
fun NotificationsDisabledBanner(
visible: Boolean,
content: @Composable () -> Unit,
buttons: @Composable () -> Unit,
settings: () -> Unit,
dismiss: () -> Unit,
) {
AnimatedVisibility(
AnimatedBanner(
visible = visible,
enter = expandVertically(),
exit = shrinkVertically(),
) {
Column(
modifier = Modifier
.fillMaxWidth(),
) {
Divider()
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()
}
Divider()
}
}
title = stringResource(id = R.string.enable_reminders),
body = stringResource(id = R.string.enable_reminders_description),
dismissText = stringResource(id = R.string.dismiss),
onDismiss = dismiss,
action = stringResource(id = R.string.TLA_menu_settings),
onAction = settings,
)
}
@ExperimentalAnimationApi
@ -67,42 +37,24 @@ fun SubscriptionNagBanner(
) {
AnimatedBanner(
visible = visible,
content = {
Text(
text = stringResource(
id = if (Tasks.IS_GENERIC) {
R.string.enjoying_tasks
} else {
R.string.tasks_needs_your_support
}
),
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.colorScheme.onSurface,
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = stringResource(
id = if (Tasks.IS_GENERIC) {
R.string.tasks_needs_your_support
} else {
R.string.support_development_subscribe
}
),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.colorScheme.onSurface,
)
},
buttons = {
BannerTextButton(text = R.string.dismiss, dismiss)
val res = if (Tasks.IS_GENERIC) {
R.string.TLA_menu_donate
} else {
R.string.button_subscribe
}
BannerTextButton(text = res, subscribe)
}
title = stringResource(id = if (Tasks.IS_GENERIC) {
R.string.enjoying_tasks
} else {
R.string.tasks_needs_your_support
}),
body = stringResource(id = if (Tasks.IS_GENERIC) {
R.string.tasks_needs_your_support
} else {
R.string.support_development_subscribe
}),
dismissText = stringResource(id = R.string.dismiss),
onDismiss = dismiss,
action = stringResource(id = if (Tasks.IS_GENERIC) {
R.string.TLA_menu_donate
} else {
R.string.button_subscribe
}),
onAction = subscribe
)
}
@ -115,38 +67,21 @@ fun BeastModeBanner(
) {
AnimatedBanner(
visible = visible,
content = {
Text(
text = stringResource(id = R.string.hint_customize_edit_title),
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.colorScheme.onSurface,
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = stringResource(id = R.string.hint_customize_edit_body),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.colorScheme.onSurface,
)
},
buttons = {
BannerTextButton(text = R.string.dismiss, onClick = dismiss)
BannerTextButton(text = R.string.TLA_menu_settings, onClick = showSettings)
}
title = stringResource(id = R.string.hint_customize_edit_title),
body = stringResource(id = R.string.hint_customize_edit_body),
dismissText = stringResource(id = R.string.dismiss),
onDismiss = dismiss,
action = stringResource(id = R.string.TLA_menu_settings),
onAction = showSettings,
)
}
@ExperimentalAnimationApi
@Preview(showBackground = true)
@Preview(showBackground = true, backgroundColor = 0x202124, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun BannerTextButton(text: Int, onClick: () -> Unit) {
TextButton(onClick = onClick) {
Text(
text = stringResource(id = text),
style = MaterialTheme.typography.labelLarge.copy(
color = MaterialTheme.colorScheme.onSurface
),
)
}
private fun NotificationsDisabledPreview() = TasksTheme {
NotificationsDisabledBanner(visible = true, settings = {}, dismiss = {})
}
@ExperimentalAnimationApi
@ -164,4 +99,3 @@ private fun BeastModePreview() = TasksTheme {
private fun SubscriptionNagPreview() = TasksTheme {
SubscriptionNagBanner(visible = true, subscribe = {}, dismiss = {})
}

@ -0,0 +1,31 @@
package org.tasks.compose
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import org.tasks.extensions.Context.canScheduleExactAlarms
@Composable
fun rememberReminderPermissionState(): State<Boolean> {
val context = LocalContext.current
val hasRemindersPermission = remember { mutableStateOf(true) }
val observer = remember {
LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
hasRemindersPermission.value = context.canScheduleExactAlarms()
}
}
}
val lifecycle = LocalLifecycleOwner.current.lifecycle
DisposableEffect(lifecycle, observer) {
lifecycle.addObserver(observer)
onDispose { lifecycle.removeObserver(observer) }
}
return hasRemindersPermission
}

@ -21,7 +21,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionStatus
import com.todoroo.astrid.ui.ReminderControlSetViewModel
import org.tasks.R
import org.tasks.compose.AddAlarmDialog
@ -35,12 +34,12 @@ import org.tasks.reminders.AlarmToString
import org.tasks.themes.TasksTheme
import java.util.Locale
@OptIn(ExperimentalPermissionsApi::class, ExperimentalComposeUiApi::class)
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun AlarmRow(
vm: ReminderControlSetViewModel = viewModel(),
permissionStatus: PermissionStatus,
launchPermissionRequest: () -> Unit,
hasNotificationPermissions: Boolean,
fixNotificationPermissions: () -> Unit,
alarms: List<Alarm>,
ringMode: Int,
locale: Locale,
@ -53,43 +52,38 @@ fun AlarmRow(
iconRes = R.drawable.ic_outline_notifications_24px,
content = {
val viewState = vm.viewState.collectAsStateLifecycleAware().value
when (permissionStatus) {
PermissionStatus.Granted -> {
Alarms(
alarms = alarms,
ringMode = ringMode,
locale = locale,
replaceAlarm = {
vm.setReplace(it)
vm.showAddAlarm(visible = true)
},
addAlarm = {
vm.showAddAlarm(visible = true)
},
deleteAlarm = deleteAlarm,
openRingType = openRingType,
if (hasNotificationPermissions) {
Alarms(
alarms = alarms,
ringMode = ringMode,
locale = locale,
replaceAlarm = {
vm.setReplace(it)
vm.showAddAlarm(visible = true)
},
addAlarm = {
vm.showAddAlarm(visible = true)
},
deleteAlarm = deleteAlarm,
openRingType = openRingType,
)
} else {
Column(
modifier = Modifier
.padding(end = 16.dp)
.clickable { fixNotificationPermissions() }
) {
Spacer(modifier = Modifier.height(20.dp))
Text(
text = stringResource(id = R.string.enable_reminders),
color = colorResource(id = R.color.red_500),
)
}
is PermissionStatus.Denied -> {
Column(
modifier = Modifier
.padding(end = 16.dp)
.clickable {
launchPermissionRequest()
}
) {
Spacer(modifier = Modifier.height(20.dp))
Text(
text = stringResource(id = R.string.enable_reminders),
color = colorResource(id = R.color.red_500),
)
Text(
text = stringResource(id = R.string.enable_reminders_description),
style = MaterialTheme.typography.bodySmall,
color = colorResource(id = R.color.red_500),
)
Spacer(modifier = Modifier.height(20.dp))
}
Text(
text = stringResource(id = R.string.enable_reminders_description),
style = MaterialTheme.typography.bodySmall,
color = colorResource(id = R.color.red_500),
)
Spacer(modifier = Modifier.height(20.dp))
}
}
@ -212,8 +206,8 @@ fun NoAlarms() {
addAlarm = {},
deleteAlarm = {},
openRingType = {},
permissionStatus = PermissionStatus.Granted,
launchPermissionRequest = {},
hasNotificationPermissions = true,
fixNotificationPermissions = {},
pickDateAndTime = {},
)
}
@ -232,8 +226,8 @@ fun PermissionDenied() {
addAlarm = {},
deleteAlarm = {},
openRingType = {},
permissionStatus = PermissionStatus.Denied(true),
launchPermissionRequest = {},
hasNotificationPermissions = false,
fixNotificationPermissions = {},
pickDateAndTime = {},
)
}

@ -1,6 +1,7 @@
package org.tasks.extensions
import android.app.Activity
import android.app.AlarmManager
import android.content.ActivityNotFoundException
import android.content.ContentResolver
import android.content.Context
@ -12,13 +13,17 @@ import android.content.res.Configuration
import android.net.ConnectivityManager
import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.annotation.AnyRes
import androidx.browser.customtabs.CustomTabsIntent
import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor
import com.todoroo.andlib.utility.AndroidUtilities.atLeastS
import org.tasks.R
import org.tasks.notifications.NotificationManager.Companion.NOTIFICATION_CHANNEL_DEFAULT
object Context {
private const val HTTP = "http"
@ -103,4 +108,35 @@ object Context {
}
return null
}
fun Context.canScheduleExactAlarms(): Boolean =
!atLeastS() || (getSystemService(Context.ALARM_SERVICE) as AlarmManager).canScheduleExactAlarms()
fun Context.openReminderSettings() {
if (atLeastS()) {
startActivity(
Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM)
.apply { data = Uri.parse("package:$packageName") }
)
}
}
fun Context.openAppNotificationSettings() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startActivity(
Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
.putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
)
}
}
fun Context.openChannelNotificationSettings() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startActivity(
Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
.putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
.putExtra(Settings.EXTRA_CHANNEL_ID, NOTIFICATION_CHANNEL_DEFAULT)
)
}
}
}

@ -156,7 +156,9 @@ class NotificationManager @Inject constructor(
fiveTimes: Boolean,
useGroupKey: Boolean
) {
if (!permissionChecker.canNotify()) {
if (permissionChecker.canNotify()) {
preferences.warnNotificationsDisabled = true
} else {
Timber.w("Notifications disabled")
return
}

@ -2,7 +2,7 @@ package org.tasks.preferences;
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastOreo;
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastQ;
import static com.todoroo.andlib.utility.AndroidUtilities.preTiramisu;
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastTiramisu;
import static java.util.Arrays.asList;
import android.Manifest.permission;
@ -43,7 +43,8 @@ public class PermissionChecker {
}
public boolean canNotify() {
return preTiramisu() || checkPermissions(permission.POST_NOTIFICATIONS);
return org.tasks.extensions.Context.INSTANCE.canScheduleExactAlarms(context)
&& (!atLeastTiramisu() || checkPermissions(permission.POST_NOTIFICATIONS));
}
private boolean checkPermissions(String... permissions) {

@ -7,7 +7,6 @@ import android.Manifest.permission;
public abstract class PermissionRequestor {
public static final int REQUEST_CALENDAR = 51;
public static final int REQUEST_GOOGLE_ACCOUNTS = 53;
public static final int REQUEST_BACKGROUND_LOCATION = 54;
public static final int REQUEST_FOREGROUND_LOCATION = 55;
@ -18,14 +17,6 @@ public abstract class PermissionRequestor {
this.permissionChecker = permissionChecker;
}
public boolean requestCalendarPermissions() {
if (permissionChecker.canAccessCalendars()) {
return true;
}
requestPermissions(REQUEST_CALENDAR, permission.READ_CALENDAR, permission.WRITE_CALENDAR);
return false;
}
public boolean requestAccountPermissions() {
if (permissionChecker.canAccessAccounts()) {
return true;

@ -17,13 +17,13 @@ import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.activity.BeastModePreferences
import com.todoroo.astrid.core.SortHelper
import org.tasks.data.entity.Task
import org.tasks.data.entity.Task.Companion.NOTIFY_AFTER_DEADLINE
import org.tasks.data.entity.Task.Companion.NOTIFY_AT_DEADLINE
import org.tasks.BuildConfig
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.billing.Purchase
import org.tasks.data.entity.Task
import org.tasks.data.entity.Task.Companion.NOTIFY_AFTER_DEADLINE
import org.tasks.data.entity.Task.Companion.NOTIFY_AT_DEADLINE
import org.tasks.data.entity.TaskAttachment
import org.tasks.extensions.Context.getResourceUri
import org.tasks.themes.ColorProvider
@ -570,6 +570,10 @@ class Preferences @JvmOverloads constructor(
get() = getLong(R.string.p_last_review_request, 0L)
set(value) = setLong(R.string.p_last_review_request, value)
var warnNotificationsDisabled: Boolean
get() = getBoolean(R.string.p_warn_notifications_disabled, true)
set(value) = setBoolean(R.string.p_warn_notifications_disabled, value)
var lastSubscribeRequest: Long
get() = getLong(R.string.p_last_subscribe_request, 0L)
set(value) = setLong(R.string.p_last_subscribe_request, value)

@ -5,7 +5,6 @@ import android.content.Context.POWER_SERVICE
import android.content.Intent
import android.media.RingtoneManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.PowerManager
import android.provider.Settings
@ -24,8 +23,8 @@ import org.tasks.compose.FilterSelectionActivity.Companion.launch
import org.tasks.compose.FilterSelectionActivity.Companion.registerForListPickerResult
import org.tasks.dialogs.MyTimePickerDialog.Companion.newTimePicker
import org.tasks.extensions.Context.getResourceUri
import org.tasks.extensions.Context.openChannelNotificationSettings
import org.tasks.injection.InjectingPreferenceFragment
import org.tasks.notifications.NotificationManager
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.Preferences
import org.tasks.receivers.ShortcutBadger
@ -115,13 +114,7 @@ class Notifications : InjectingPreferenceFragment() {
}
findPreference(R.string.more_settings).setOnPreferenceClickListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startActivity(
Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
.putExtra(Settings.EXTRA_CHANNEL_ID, NotificationManager.NOTIFICATION_CHANNEL_DEFAULT)
)
}
requireContext().openChannelNotificationSettings()
true
}

@ -0,0 +1,27 @@
package org.tasks.receivers
import android.app.AlarmManager.ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.extensions.Context.canScheduleExactAlarms
import org.tasks.jobs.WorkManager
import org.tasks.scheduling.NotificationSchedulerIntentService
import javax.inject.Inject
@AndroidEntryPoint
class ScheduleExactAlarmsPermissionReceiver : BroadcastReceiver() {
@Inject lateinit var workManager: WorkManager
override fun onReceive(context: Context, intent: Intent?) {
if (intent?.action != ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED) {
return
}
if (context.canScheduleExactAlarms()) {
NotificationSchedulerIntentService.enqueueWork(context)
}
}
}

@ -72,6 +72,7 @@ class TaskListViewModel @Inject constructor(
val searchQuery: String? = null,
val tasks: TasksResults = TasksResults.Loading,
val begForSubscription: Boolean = false,
val warnNotificationsDisabled: Boolean = false,
val syncOngoing: Boolean = false,
val collapsed: Set<Long> = setOf(SectionedDataSource.HEADER_COMPLETED),
)
@ -104,7 +105,16 @@ class TaskListViewModel @Inject constructor(
}
}
fun dismissBanner(clickedPurchase: Boolean) {
fun dismissNotificationBanner(
fix: Boolean = false,
) {
_state.update {
it.copy(warnNotificationsDisabled = false)
}
preferences.warnNotificationsDisabled = fix
}
fun dismissPurchaseBanner(clickedPurchase: Boolean) {
_state.update {
it.copy(begForSubscription = false)
}
@ -170,14 +180,6 @@ class TaskListViewModel @Inject constructor(
}
.flowOn(Dispatchers.Default)
.launchIn(viewModelScope)
viewModelScope.launch(Dispatchers.Default) {
if (!inventory.hasPro && !firebase.subscribeCooldown) {
_state.update {
it.copy(begForSubscription = true)
}
}
}
}
override fun onCleared() {
@ -202,6 +204,20 @@ class TaskListViewModel @Inject constructor(
}
}
fun updateBannerState() {
viewModelScope.launch(Dispatchers.Default) {
_state.update {
it.copy(warnNotificationsDisabled = preferences.warnNotificationsDisabled)
}
if (!inventory.hasPro && !firebase.subscribeCooldown) {
_state.update {
it.copy(begForSubscription = true)
}
}
}
}
companion object {
fun Context.createSearchQuery(query: String): Filter =
SearchFilter(getString(R.string.FLA_search_filter, query), query)

@ -414,6 +414,7 @@
<string name="p_install_version">install_version</string>
<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="event_showed_purchase_dialog">showed_purchase_dialog</string>
<string name="event_purchase_result">billing_flow_result</string>

@ -0,0 +1,98 @@
package org.tasks.compose.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@ExperimentalAnimationApi
@Composable
fun AnimatedBanner(
visible: Boolean,
title: String,
body: String,
dismissText: String,
onDismiss: () -> Unit,
action: String,
onAction: () -> Unit,
) {
AnimatedBanner(
visible = visible,
content = {
Text(
text = title,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.colorScheme.onSurface,
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = body,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.colorScheme.onSurface,
)
},
buttons = {
BannerTextButton(text = dismissText, onDismiss)
BannerTextButton(text = action, onAction)
}
)
}
@ExperimentalAnimationApi
@Composable
private fun AnimatedBanner(
visible: Boolean,
content: @Composable () -> Unit,
buttons: @Composable () -> Unit,
) {
AnimatedVisibility(
visible = visible,
enter = expandVertically(),
exit = shrinkVertically(),
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.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()
}
}
}
}
@Composable
fun BannerTextButton(text: String, onClick: () -> Unit) {
TextButton(onClick = onClick) {
Text(
text = text,
style = MaterialTheme.typography.labelLarge.copy(
color = MaterialTheme.colorScheme.onSurface
),
)
}
}
Loading…
Cancel
Save