diff --git a/app/build.gradle b/app/build.gradle
index 00ebb7aa5d..45917dc6d3 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -19,8 +19,8 @@ android {
buildToolsVersion = Android.buildToolsVersion
defaultConfig {
- versionCode 98
- versionName "1.63.0"
+ versionCode 99
+ versionName "1.63.1"
minSdk Android.minSdkVersion
targetSdk Android.targetSdkVersion
archivesBaseName = "Fahrplan-$versionName"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0a06e4d66f..542253a131 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -8,6 +8,7 @@
+
,
private val logging: Logging,
private val pendingIntentDelegate: PendingIntentDelegate = PendingIntentProvider,
- private val formattingDelegate: FormattingDelegate = DateFormatterDelegate
+ private val formattingDelegate: FormattingDelegate = DateFormatterDelegate,
+ private val runsAtLeastOnAndroidSnowCone: Boolean = SDK_INT >= S,
) {
@@ -143,6 +147,17 @@ class AlarmServices @VisibleForTesting constructor(
session.hasAlarm = false
}
+ /**
+ * Returns `true` if the app is allowed to schedule exact alarms. Otherwise `false`.
+ * Automatically allowed before Android 12, SnowCone (API level 31) but might be withdrawn
+ * by the user via the system settings.
+ *
+ * See: [AlarmManager.canScheduleExactAlarms].
+ */
+ val canScheduleExactAlarms: Boolean
+ @SuppressLint("NewApi")
+ get() = if (runsAtLeastOnAndroidSnowCone) alarmManager.canScheduleExactAlarms() else true
+
/**
* Schedules the given [alarm] via the [AlarmManager].
* Existing alarms for the associated session are discarded if configured via [discardExisting].
@@ -161,15 +176,12 @@ class AlarmServices @VisibleForTesting constructor(
if (discardExisting) {
alarmManager.cancel(pendingIntent)
}
- // Alarms scheduled here are treated as inexact as of targeting Android 4.4 (API level 19).
- // See https://developer.android.com/training/scheduling/alarms
- // and https://developer.android.com/reference/android/os/Build.VERSION_CODES#KITKAT
- // SCHEDULE_EXACT_ALARM permission is needed when switching to exact alarms as of targeting Android 12 (API level 31).
- // See https://developer.android.com/about/versions/12/behavior-changes-12#exact-alarm-permission
- // USE_EXACT_ALARM permission is needed when switching to exact alarms as of targeting Android 13 (API level 33).
- // See https://developer.android.com/about/versions/13/features#use-exact-alarm-permission
- // and https://support.google.com/googleplay/android-developer/answer/12253906#exact_alarm_preview
- alarmManager.set(AlarmManager.RTC_WAKEUP, alarm.startTime, pendingIntent)
+ AlarmManagerCompat.setExactAndAllowWhileIdle(
+ alarmManager,
+ AlarmManager.RTC_WAKEUP,
+ alarm.startTime,
+ pendingIntent
+ )
}
/**
diff --git a/app/src/main/java/nerd/tuxmobil/fahrplan/congress/alarms/SessionAlarmViewModelDelegate.kt b/app/src/main/java/nerd/tuxmobil/fahrplan/congress/alarms/SessionAlarmViewModelDelegate.kt
new file mode 100644
index 0000000000..2f50e78900
--- /dev/null
+++ b/app/src/main/java/nerd/tuxmobil/fahrplan/congress/alarms/SessionAlarmViewModelDelegate.kt
@@ -0,0 +1,69 @@
+package nerd.tuxmobil.fahrplan.congress.alarms
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.SendChannel
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.launch
+import nerd.tuxmobil.fahrplan.congress.models.Session
+import nerd.tuxmobil.fahrplan.congress.notifications.NotificationHelper
+
+/**
+ * Wraps all concerns related to adding and deleting sessions alarms
+ * and the related permission checks. This handling is centralized here
+ * to avoid code duplication in the individual view models.
+ */
+internal class SessionAlarmViewModelDelegate(
+ private val viewModelScope: CoroutineScope,
+ private val notificationHelper: NotificationHelper,
+ private val alarmServices: AlarmServices,
+ private val runsAtLeastOnAndroidTiramisu: Boolean,
+) {
+
+ private val mutableShowAlarmTimePicker = Channel()
+ val showAlarmTimePicker = mutableShowAlarmTimePicker
+ .receiveAsFlow()
+
+ private val mutableRequestPostNotificationsPermission = Channel()
+ val requestPostNotificationsPermission = mutableRequestPostNotificationsPermission
+ .receiveAsFlow()
+
+ private val mutableNotificationsDisabled = Channel()
+ val notificationsDisabled = mutableNotificationsDisabled
+ .receiveAsFlow()
+
+ private val mutableRequestScheduleExactAlarmsPermission = Channel()
+ val requestScheduleExactAlarmsPermission = mutableRequestScheduleExactAlarmsPermission
+ .receiveAsFlow()
+
+ fun canAddAlarms() = alarmServices.canScheduleExactAlarms
+
+ fun addAlarmWithChecks() {
+ if (notificationHelper.notificationsEnabled) {
+ if (alarmServices.canScheduleExactAlarms) {
+ mutableShowAlarmTimePicker.sendOneTimeEvent(Unit)
+ } else {
+ mutableRequestScheduleExactAlarmsPermission.sendOneTimeEvent(Unit)
+ }
+ } else {
+ // Check runtime version here because requesting the POST_NOTIFICATION permission
+ // before Android 13 (Tiramisu) has no effect nor error message.
+ when (runsAtLeastOnAndroidTiramisu) {
+ true -> mutableRequestPostNotificationsPermission.sendOneTimeEvent(Unit)
+ false -> mutableNotificationsDisabled.sendOneTimeEvent(Unit)
+ }
+ }
+ }
+
+ fun addAlarm(session: Session, alarmTimesIndex: Int) =
+ alarmServices.addSessionAlarm(session, alarmTimesIndex)
+
+ fun deleteAlarm(session: Session) = alarmServices.deleteSessionAlarm(session)
+
+ private fun SendChannel.sendOneTimeEvent(event: E) {
+ viewModelScope.launch {
+ send(event)
+ }
+ }
+
+}
diff --git a/app/src/main/java/nerd/tuxmobil/fahrplan/congress/details/SessionDetailsFragment.kt b/app/src/main/java/nerd/tuxmobil/fahrplan/congress/details/SessionDetailsFragment.kt
index 059393b891..16a44502f9 100644
--- a/app/src/main/java/nerd/tuxmobil/fahrplan/congress/details/SessionDetailsFragment.kt
+++ b/app/src/main/java/nerd/tuxmobil/fahrplan/congress/details/SessionDetailsFragment.kt
@@ -3,10 +3,12 @@ package nerd.tuxmobil.fahrplan.congress.details
import android.Manifest.permission.POST_NOTIFICATIONS
import android.annotation.SuppressLint
import android.app.Activity
+import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.Intent
import android.graphics.Typeface
import android.os.Bundle
+import android.provider.Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
@@ -17,11 +19,13 @@ import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
+import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.annotation.CallSuper
import androidx.annotation.IdRes
import androidx.annotation.LayoutRes
import androidx.annotation.MainThread
import androidx.core.content.ContextCompat
+import androidx.core.net.toUri
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
@@ -82,7 +86,8 @@ class SessionDetailsFragment : Fragment() {
}
}
- private lateinit var permissionRequestLauncher: ActivityResultLauncher
+ private lateinit var postNotificationsPermissionRequestLauncher: ActivityResultLauncher
+ private lateinit var scheduleExactAlarmsPermissionRequestLauncher: ActivityResultLauncher
private lateinit var appRepository: AppRepository
private lateinit var alarmServices: AlarmServices
private lateinit var notificationHelper: NotificationHelper
@@ -108,7 +113,7 @@ class SessionDetailsFragment : Fragment() {
R.id.menu_item_add_to_calendar to { addToCalendar() },
R.id.menu_item_flag_as_favorite to { favorSession() },
R.id.menu_item_unflag_as_favorite to { unfavorSession() },
- R.id.menu_item_set_alarm to { setAlarm() },
+ R.id.menu_item_set_alarm to { addAlarmWithChecks() },
R.id.menu_item_delete_alarm to { deleteAlarm() },
R.id.menu_item_close_session_details to { closeDetails() },
R.id.menu_item_navigate to { navigateToRoom() },
@@ -132,14 +137,32 @@ class SessionDetailsFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- permissionRequestLauncher = registerForActivityResult(RequestPermission()) { isGranted ->
+ postNotificationsPermissionRequestLauncher = registerForActivityResult(RequestPermission()) { isGranted ->
if (isGranted) {
- viewModel.setAlarm()
+ viewModel.addAlarmWithChecks()
} else {
showMissingPostNotificationsPermissionError()
}
}
+ scheduleExactAlarmsPermissionRequestLauncher =
+ registerForActivityResult(StartActivityForResult()) { result ->
+ if (result.resultCode == RESULT_OK) {
+ // User granted the permission earlier.
+ viewModel.addAlarmWithChecks()
+ } else {
+ // User granted the permission for the first time.
+ // Screen is resumed with RESULT_CANCELED, no indication
+ // of whether the permission was granted or not.
+ // Hence the following ugly view model bypass.
+ if (viewModel.canAddAlarms()) {
+ viewModel.addAlarmWithChecks()
+ } else {
+ showMissingScheduleExactAlarmsPermissionError()
+ }
+ }
+ }
+
setHasOptionsMenu(true)
}
@@ -192,15 +215,8 @@ class SessionDetailsFragment : Fragment() {
viewModel.addToCalendar.observe(viewLifecycleOwner) { session ->
CalendarSharing(requireContext()).addToCalendar(session)
}
- viewModel.setAlarm.observe(viewLifecycleOwner) {
- AlarmTimePickerFragment.show(this, SESSION_DETAILS_FRAGMENT_REQUEST_KEY) { requestKey, result ->
- if (requestKey == SESSION_DETAILS_FRAGMENT_REQUEST_KEY &&
- result.containsKey(AlarmTimePickerFragment.ALARM_TIMES_INDEX_BUNDLE_KEY)
- ) {
- val alarmTimesIndex = result.getInt(AlarmTimePickerFragment.ALARM_TIMES_INDEX_BUNDLE_KEY)
- viewModel.addAlarm(alarmTimesIndex)
- }
- }
+ viewModel.showAlarmTimePicker.observe(viewLifecycleOwner) {
+ showAlarmTimePicker()
}
viewModel.navigateToRoom.observe(viewLifecycleOwner) { uri ->
startActivity(Intent(Intent.ACTION_VIEW, uri))
@@ -212,10 +228,15 @@ class SessionDetailsFragment : Fragment() {
}
}
viewModel.requestPostNotificationsPermission.observe(viewLifecycleOwner) {
- permissionRequestLauncher.launch(POST_NOTIFICATIONS)
+ postNotificationsPermissionRequestLauncher.launch(POST_NOTIFICATIONS)
+ }
+ viewModel.notificationsDisabled.observe(viewLifecycleOwner) {
+ showNotificationsDisabledError()
}
- viewModel.missingPostNotificationsPermission.observe(viewLifecycleOwner) {
- showMissingPostNotificationsPermissionError()
+ viewModel.requestScheduleExactAlarmsPermission.observe(viewLifecycleOwner) {
+ val intent = Intent(ACTION_REQUEST_SCHEDULE_EXACT_ALARM)
+ .setData("package:${BuildConfig.APPLICATION_ID}".toUri())
+ scheduleExactAlarmsPermissionRequestLauncher.launch(intent)
}
}
@@ -359,10 +380,29 @@ class SessionDetailsFragment : Fragment() {
this.isVisible = true
}
+ private fun showAlarmTimePicker() {
+ AlarmTimePickerFragment.show(this, SESSION_DETAILS_FRAGMENT_REQUEST_KEY) { requestKey, result ->
+ if (requestKey == SESSION_DETAILS_FRAGMENT_REQUEST_KEY &&
+ result.containsKey(AlarmTimePickerFragment.ALARM_TIMES_INDEX_BUNDLE_KEY)
+ ) {
+ val alarmTimesIndex = result.getInt(AlarmTimePickerFragment.ALARM_TIMES_INDEX_BUNDLE_KEY)
+ viewModel.addAlarm(alarmTimesIndex)
+ }
+ }
+ }
+
private fun showMissingPostNotificationsPermissionError() {
Toast.makeText(requireContext(), R.string.alarms_disabled_notifications_permission_missing, Toast.LENGTH_LONG).show()
}
+ private fun showNotificationsDisabledError() {
+ Toast.makeText(requireContext(), R.string.alarms_disabled_notifications_are_disabled, Toast.LENGTH_LONG).show()
+ }
+
+ private fun showMissingScheduleExactAlarmsPermissionError() {
+ Toast.makeText(requireContext(), R.string.alarms_disabled_schedule_exact_alarm_permission_missing, Toast.LENGTH_LONG).show()
+ }
+
private fun updateOptionsMenu() {
requireActivity().invalidateOptionsMenu()
}
diff --git a/app/src/main/java/nerd/tuxmobil/fahrplan/congress/details/SessionDetailsViewModel.kt b/app/src/main/java/nerd/tuxmobil/fahrplan/congress/details/SessionDetailsViewModel.kt
index 87e2151d39..a96598bc49 100644
--- a/app/src/main/java/nerd/tuxmobil/fahrplan/congress/details/SessionDetailsViewModel.kt
+++ b/app/src/main/java/nerd/tuxmobil/fahrplan/congress/details/SessionDetailsViewModel.kt
@@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import nerd.tuxmobil.fahrplan.congress.alarms.AlarmServices
+import nerd.tuxmobil.fahrplan.congress.alarms.SessionAlarmViewModelDelegate
import nerd.tuxmobil.fahrplan.congress.dataconverters.toRoom
import nerd.tuxmobil.fahrplan.congress.models.Session
import nerd.tuxmobil.fahrplan.congress.navigation.IndoorNavigation
@@ -34,8 +35,8 @@ internal class SessionDetailsViewModel(
private val repository: AppRepository,
private val executionContext: ExecutionContext,
- private val alarmServices: AlarmServices,
- private val notificationHelper: NotificationHelper,
+ alarmServices: AlarmServices,
+ notificationHelper: NotificationHelper,
private val sessionFormatter: SessionFormatter,
private val simpleSessionFormat: SimpleSessionFormat,
private val jsonSessionFormat: JsonSessionFormat,
@@ -46,7 +47,7 @@ internal class SessionDetailsViewModel(
private val formattingDelegate: FormattingDelegate = DateFormattingDelegate(),
private val defaultEngelsystemRoomName: String,
private val customEngelsystemRoomName: String,
- private val runsAtLeastOnAndroidTiramisu: Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
+ runsAtLeastOnAndroidTiramisu: Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
) : ViewModel() {
@@ -74,6 +75,14 @@ internal class SessionDetailsViewModel(
}
+ private var sessionAlarmViewModelDelegate: SessionAlarmViewModelDelegate =
+ SessionAlarmViewModelDelegate(
+ viewModelScope,
+ notificationHelper,
+ alarmServices,
+ runsAtLeastOnAndroidTiramisu,
+ )
+
val abstractFont = Font.Roboto.Bold
val descriptionFont = Font.Roboto.Regular
val linksFont = Font.Roboto.Regular
@@ -99,18 +108,22 @@ internal class SessionDetailsViewModel(
val shareJson = mutableShareJson.receiveAsFlow()
private val mutableAddToCalendar = Channel()
val addToCalendar = mutableAddToCalendar.receiveAsFlow()
- private val mutableSetAlarm = Channel()
- val setAlarm = mutableSetAlarm.receiveAsFlow()
private val mutableNavigateToRoom = Channel()
val navigateToRoom = mutableNavigateToRoom.receiveAsFlow()
private val mutableCloseDetails = Channel()
val closeDetails = mutableCloseDetails.receiveAsFlow()
- private val mutableRequestPostNotificationsPermission = Channel()
- val requestPostNotificationsPermission =
- mutableRequestPostNotificationsPermission.receiveAsFlow()
- private val mutableMissingPostNotificationsPermission = Channel()
- val missingPostNotificationsPermission =
- mutableMissingPostNotificationsPermission.receiveAsFlow()
+
+ val requestPostNotificationsPermission = sessionAlarmViewModelDelegate
+ .requestPostNotificationsPermission
+
+ val notificationsDisabled = sessionAlarmViewModelDelegate
+ .notificationsDisabled
+
+ val requestScheduleExactAlarmsPermission = sessionAlarmViewModelDelegate
+ .requestScheduleExactAlarmsPermission
+
+ val showAlarmTimePicker = sessionAlarmViewModelDelegate
+ .showAlarmTimePicker
private fun SelectedSessionParameter.customizeEngelsystemRoomName() = copy(
roomName = if (roomName == defaultEngelsystemRoomName) customEngelsystemRoomName else roomName
@@ -207,26 +220,23 @@ internal class SessionDetailsViewModel(
}
}
- fun setAlarm() {
- if (notificationHelper.notificationsEnabled) {
- mutableSetAlarm.sendOneTimeEvent(Unit)
- } else {
- when (runsAtLeastOnAndroidTiramisu) {
- true -> mutableRequestPostNotificationsPermission.sendOneTimeEvent(Unit)
- false -> mutableMissingPostNotificationsPermission.sendOneTimeEvent(Unit)
- }
- }
+ fun canAddAlarms(): Boolean {
+ return sessionAlarmViewModelDelegate.canAddAlarms()
+ }
+
+ fun addAlarmWithChecks() {
+ sessionAlarmViewModelDelegate.addAlarmWithChecks()
}
fun addAlarm(alarmTimesIndex: Int) {
loadSelectedSession { session ->
- alarmServices.addSessionAlarm(session, alarmTimesIndex)
+ sessionAlarmViewModelDelegate.addAlarm(session, alarmTimesIndex)
}
}
fun deleteAlarm() {
loadSelectedSession { session ->
- alarmServices.deleteSessionAlarm(session)
+ sessionAlarmViewModelDelegate.deleteAlarm(session)
}
}
diff --git a/app/src/main/java/nerd/tuxmobil/fahrplan/congress/notifications/NotificationHelper.kt b/app/src/main/java/nerd/tuxmobil/fahrplan/congress/notifications/NotificationHelper.kt
index a63a1cb230..639adebba9 100644
--- a/app/src/main/java/nerd/tuxmobil/fahrplan/congress/notifications/NotificationHelper.kt
+++ b/app/src/main/java/nerd/tuxmobil/fahrplan/congress/notifications/NotificationHelper.kt
@@ -18,6 +18,11 @@ internal class NotificationHelper(context: Context) : ContextWrapper(context) {
getNotificationManager()
}
+ /**
+ * Returns `true` if notifications in general are enabled for the app.
+ * If individual channels are disabled, this method still returns `true`.
+ * Hence, firing an alarm for a disabled channel will not show a notification.
+ */
val notificationsEnabled: Boolean
get() = notificationManager.areNotificationsEnabled()
diff --git a/app/src/main/java/nerd/tuxmobil/fahrplan/congress/schedule/FahrplanFragment.kt b/app/src/main/java/nerd/tuxmobil/fahrplan/congress/schedule/FahrplanFragment.kt
index 2a561a3a61..3f0cc5d5ed 100644
--- a/app/src/main/java/nerd/tuxmobil/fahrplan/congress/schedule/FahrplanFragment.kt
+++ b/app/src/main/java/nerd/tuxmobil/fahrplan/congress/schedule/FahrplanFragment.kt
@@ -2,10 +2,13 @@ package nerd.tuxmobil.fahrplan.congress.schedule
import android.Manifest.permission.POST_NOTIFICATIONS
import android.annotation.SuppressLint
+import android.app.Activity.RESULT_OK
import android.content.Context
+import android.content.Intent
import android.graphics.Typeface
import android.os.Build
import android.os.Bundle
+import android.provider.Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM
import android.text.TextUtils.TruncateAt
import android.view.ContextMenu
import android.view.ContextMenu.ContextMenuInfo
@@ -26,10 +29,12 @@ import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
+import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.appcompat.app.ActionBar.NAVIGATION_MODE_LIST
import androidx.appcompat.app.ActionBar.OnNavigationListener
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
+import androidx.core.net.toUri
import androidx.core.view.updatePadding
import androidx.core.widget.NestedScrollView
import androidx.core.widget.NestedScrollView.OnScrollChangeListener
@@ -94,7 +99,8 @@ class FahrplanFragment : Fragment(), SessionViewEventsHandler {
}
- private lateinit var permissionRequestLauncher: ActivityResultLauncher
+ private lateinit var postNotificationsPermissionRequestLauncher: ActivityResultLauncher
+ private lateinit var scheduleExactAlarmsPermissionRequestLauncher: ActivityResultLauncher
private lateinit var inflater: LayoutInflater
private lateinit var sessionViewDrawer: SessionViewDrawer
private lateinit var errorMessageFactory: ErrorMessage.Factory
@@ -140,14 +146,32 @@ class FahrplanFragment : Fragment(), SessionViewEventsHandler {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- permissionRequestLauncher = registerForActivityResult(RequestPermission()) { isGranted ->
+ postNotificationsPermissionRequestLauncher = registerForActivityResult(RequestPermission()) { isGranted ->
if (isGranted) {
- showAlarmTimePicker()
+ viewModel.addAlarmWithChecks()
} else {
showMissingPostNotificationsPermissionError()
}
}
+ scheduleExactAlarmsPermissionRequestLauncher =
+ registerForActivityResult(StartActivityForResult()) { result ->
+ // User granted the permission earlier.
+ if (result.resultCode == RESULT_OK) {
+ viewModel.addAlarmWithChecks()
+ } else {
+ // User granted the permission for the first time.
+ // Screen is resumed with RESULT_CANCELED, no indication
+ // of whether the permission was granted or not.
+ // Hence the following ugly view model bypass.
+ if (viewModel.canAddAlarms()) {
+ viewModel.addAlarmWithChecks()
+ } else {
+ showMissingScheduleExactAlarmsPermissionError()
+ }
+ }
+ }
+
setHasOptionsMenu(true)
val context = requireContext()
roomTitleTypeFace = TypefaceFactory.getNewInstance(context).getTypeface(Font.Roboto.Light)
@@ -214,10 +238,15 @@ class FahrplanFragment : Fragment(), SessionViewEventsHandler {
scrollTo(sessionId, verticalPosition, roomIndex)
}
viewModel.requestPostNotificationsPermission.observe(viewLifecycleOwner) {
- permissionRequestLauncher.launch(POST_NOTIFICATIONS)
+ postNotificationsPermissionRequestLauncher.launch(POST_NOTIFICATIONS)
+ }
+ viewModel.notificationsDisabled.observe(viewLifecycleOwner) {
+ showNotificationsDisabledError()
}
- viewModel.missingPostNotificationsPermission.observe(viewLifecycleOwner) {
- showMissingPostNotificationsPermissionError()
+ viewModel.requestScheduleExactAlarmsPermission.observe(viewLifecycleOwner) {
+ val intent = Intent(ACTION_REQUEST_SCHEDULE_EXACT_ALARM)
+ .setData("package:${BuildConfig.APPLICATION_ID}".toUri())
+ scheduleExactAlarmsPermissionRequestLauncher.launch(intent)
}
viewModel.showAlarmTimePicker.observe(viewLifecycleOwner) {
showAlarmTimePicker()
@@ -481,7 +510,7 @@ class FahrplanFragment : Fragment(), SessionViewEventsHandler {
updateMenuItems()
}
CONTEXT_MENU_ITEM_ID_SET_ALARM -> {
- viewModel.showAlarmTimePickerWithChecks()
+ viewModel.addAlarmWithChecks()
}
CONTEXT_MENU_ITEM_ID_DELETE_ALARM -> {
viewModel.deleteAlarm(session)
@@ -553,6 +582,14 @@ class FahrplanFragment : Fragment(), SessionViewEventsHandler {
Toast.makeText(requireContext(), R.string.alarms_disabled_notifications_permission_missing, Toast.LENGTH_LONG).show()
}
+ private fun showNotificationsDisabledError() {
+ Toast.makeText(requireContext(), R.string.alarms_disabled_notifications_are_disabled, Toast.LENGTH_LONG).show()
+ }
+
+ private fun showMissingScheduleExactAlarmsPermissionError() {
+ Toast.makeText(requireContext(), R.string.alarms_disabled_schedule_exact_alarm_permission_missing, Toast.LENGTH_LONG).show()
+ }
+
private inner class OnDaySelectedListener(private val numDays: Int) : OnNavigationListener {
private var isSynthetic = true
diff --git a/app/src/main/java/nerd/tuxmobil/fahrplan/congress/schedule/FahrplanViewModel.kt b/app/src/main/java/nerd/tuxmobil/fahrplan/congress/schedule/FahrplanViewModel.kt
index 2d064733ad..aab2ed6d2f 100644
--- a/app/src/main/java/nerd/tuxmobil/fahrplan/congress/schedule/FahrplanViewModel.kt
+++ b/app/src/main/java/nerd/tuxmobil/fahrplan/congress/schedule/FahrplanViewModel.kt
@@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import nerd.tuxmobil.fahrplan.congress.alarms.AlarmServices
+import nerd.tuxmobil.fahrplan.congress.alarms.SessionAlarmViewModelDelegate
import nerd.tuxmobil.fahrplan.congress.models.Alarm
import nerd.tuxmobil.fahrplan.congress.models.ScheduleData
import nerd.tuxmobil.fahrplan.congress.models.Session
@@ -36,15 +37,15 @@ internal class FahrplanViewModel(
private val repository: AppRepository,
private val executionContext: ExecutionContext,
private val logging: Logging,
- private val alarmServices: AlarmServices,
- private val notificationHelper: NotificationHelper,
+ alarmServices: AlarmServices,
+ notificationHelper: NotificationHelper,
private val navigationMenuEntriesGenerator: NavigationMenuEntriesGenerator,
private val simpleSessionFormat: SimpleSessionFormat,
private val jsonSessionFormat: JsonSessionFormat,
private val scrollAmountCalculator: ScrollAmountCalculator,
private val defaultEngelsystemRoomName: String,
private val customEngelsystemRoomName: String,
- private val runsAtLeastOnAndroidTiramisu: Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
+ runsAtLeastOnAndroidTiramisu: Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
) : ViewModel() {
@@ -52,6 +53,14 @@ internal class FahrplanViewModel(
const val LOG_TAG = "FahrplanViewModel"
}
+ private var sessionAlarmViewModelDelegate: SessionAlarmViewModelDelegate =
+ SessionAlarmViewModelDelegate(
+ viewModelScope,
+ notificationHelper,
+ alarmServices,
+ runsAtLeastOnAndroidTiramisu,
+ )
+
val fahrplanParameter = combine(
repository.uncanceledSessionsForDayIndex.filter { it.allSessions.isNotEmpty() },
repository.sessionsWithoutShifts.filterNotNull(),
@@ -82,14 +91,17 @@ internal class FahrplanViewModel(
private val mutableScrollToSessionParameter = Channel()
val scrollToSessionParameter = mutableScrollToSessionParameter.receiveAsFlow()
- private val mutableRequestPostNotificationsPermission = Channel()
- val requestPostNotificationsPermission = mutableRequestPostNotificationsPermission.receiveAsFlow()
+ val requestPostNotificationsPermission = sessionAlarmViewModelDelegate
+ .requestPostNotificationsPermission
+
+ val notificationsDisabled = sessionAlarmViewModelDelegate
+ .notificationsDisabled
- private val mutableMissingPostNotificationsPermission = Channel()
- val missingPostNotificationsPermission = mutableMissingPostNotificationsPermission.receiveAsFlow()
+ val requestScheduleExactAlarmsPermission = sessionAlarmViewModelDelegate
+ .requestScheduleExactAlarmsPermission
- private val mutableShowAlarmTimePicker = Channel()
- val showAlarmTimePicker = mutableShowAlarmTimePicker.receiveAsFlow()
+ val showAlarmTimePicker = sessionAlarmViewModelDelegate
+ .showAlarmTimePicker
var preserveVerticalScrollPosition: Boolean = false
@@ -97,17 +109,6 @@ internal class FahrplanViewModel(
updateUncanceledSessions()
}
- fun showAlarmTimePickerWithChecks() {
- if (notificationHelper.notificationsEnabled) {
- mutableShowAlarmTimePicker.sendOneTimeEvent(Unit)
- } else {
- when (runsAtLeastOnAndroidTiramisu) {
- true -> mutableRequestPostNotificationsPermission.sendOneTimeEvent(Unit)
- false -> mutableMissingPostNotificationsPermission.sendOneTimeEvent(Unit)
- }
- }
- }
-
private fun updateUncanceledSessions() {
launch {
repository.uncanceledSessionsForDayIndex.collect { scheduleData ->
@@ -248,15 +249,23 @@ internal class FahrplanViewModel(
}
}
+ fun canAddAlarms(): Boolean {
+ return sessionAlarmViewModelDelegate.canAddAlarms()
+ }
+
+ fun addAlarmWithChecks() {
+ sessionAlarmViewModelDelegate.addAlarmWithChecks()
+ }
+
fun addAlarm(session: Session, alarmTimesIndex: Int) {
launch {
- alarmServices.addSessionAlarm(session, alarmTimesIndex)
+ sessionAlarmViewModelDelegate.addAlarm(session, alarmTimesIndex)
}
}
fun deleteAlarm(session: Session) {
launch {
- alarmServices.deleteSessionAlarm(session)
+ sessionAlarmViewModelDelegate.deleteAlarm(session)
}
}
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index c9d1330ebb..58c8570429 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -203,6 +203,8 @@
60 Minuten vorher
Alarme sind deaktiviert. Bitte erteile die \"Benachrichtigungen\" Berechtigung.
+ Alarme sind deaktiviert. Bitte aktiviere Benachrichtigungen.
+ Alarme sind deaktiviert. Bitte \"Erlaube, Wecker & Erinnerungen einzurichten\".
%d min.
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3e0bd067d2..ce6ae0be59 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -234,6 +234,8 @@
60 minutes before
Alarms are disabled. Please grant the \"Notifications\" permission.
+ Alarms are disabled. Please enable notifications.
+ Alarms are disabled. Please \"Allow setting alarms & reminders\".
%d min.
diff --git a/app/src/test/java/nerd/tuxmobil/fahrplan/congress/alarms/AlarmServicesTest.kt b/app/src/test/java/nerd/tuxmobil/fahrplan/congress/alarms/AlarmServicesTest.kt
index 1a9dc57b44..3425973f02 100644
--- a/app/src/test/java/nerd/tuxmobil/fahrplan/congress/alarms/AlarmServicesTest.kt
+++ b/app/src/test/java/nerd/tuxmobil/fahrplan/congress/alarms/AlarmServicesTest.kt
@@ -124,6 +124,54 @@ class AlarmServicesTest {
assertThat(session.hasAlarm).isFalse()
}
+ @Test
+ fun `canScheduleExactAlarms returns true before SnowCone 1`() {
+ val alarmManager = mock {
+ on { canScheduleExactAlarms() } doReturn false // not relevant
+ }
+ val alarmServices = createAlarmServices(
+ alarmManager = alarmManager,
+ runsAtLeastOnAndroidSnowCone = false,
+ )
+ assertThat(alarmServices.canScheduleExactAlarms).isTrue()
+ }
+
+ @Test
+ fun `canScheduleExactAlarms returns true before SnowCone 2`() {
+ val alarmManager = mock {
+ on { canScheduleExactAlarms() } doReturn true // not relevant
+ }
+ val alarmServices = createAlarmServices(
+ alarmManager = alarmManager,
+ runsAtLeastOnAndroidSnowCone = false,
+ )
+ assertThat(alarmServices.canScheduleExactAlarms).isTrue()
+ }
+
+ @Test
+ fun `canScheduleExactAlarms returns false as of SnowCone and alarmManager returns false`() {
+ val alarmManager = mock {
+ on { canScheduleExactAlarms() } doReturn false
+ }
+ val alarmServices = createAlarmServices(
+ alarmManager = alarmManager,
+ runsAtLeastOnAndroidSnowCone = true,
+ )
+ assertThat(alarmServices.canScheduleExactAlarms).isFalse()
+ }
+
+ @Test
+ fun `canScheduleExactAlarms returns true as of SnowCone and alarmManager returns true`() {
+ val alarmManager = mock {
+ on { canScheduleExactAlarms() } doReturn true
+ }
+ val alarmServices = createAlarmServices(
+ alarmManager = alarmManager,
+ runsAtLeastOnAndroidSnowCone = true,
+ )
+ assertThat(alarmServices.canScheduleExactAlarms).isTrue()
+ }
+
@Test
fun `scheduleSessionAlarm invokes cancel then set when discardExisting is true`() {
val pendingIntent = mock()
@@ -201,7 +249,9 @@ class AlarmServicesTest {
private fun createAlarmServices(
pendingIntentDelegate: PendingIntentDelegate = mock(),
- formattingDelegate: FormattingDelegate = mock()
+ formattingDelegate: FormattingDelegate = mock(),
+ alarmManager: AlarmManager = this.alarmManager,
+ runsAtLeastOnAndroidSnowCone: Boolean = true,
) = AlarmServices(
context = mockContext,
repository = repository,
@@ -209,7 +259,8 @@ class AlarmServicesTest {
alarmTimeValues = alarmTimesValues,
logging = NoLogging,
pendingIntentDelegate = pendingIntentDelegate,
- formattingDelegate = formattingDelegate
+ formattingDelegate = formattingDelegate,
+ runsAtLeastOnAndroidSnowCone = runsAtLeastOnAndroidSnowCone,
)
}
diff --git a/app/src/test/java/nerd/tuxmobil/fahrplan/congress/alarms/SessionAlarmViewModelDelegateTest.kt b/app/src/test/java/nerd/tuxmobil/fahrplan/congress/alarms/SessionAlarmViewModelDelegateTest.kt
new file mode 100644
index 0000000000..0776e21bfe
--- /dev/null
+++ b/app/src/test/java/nerd/tuxmobil/fahrplan/congress/alarms/SessionAlarmViewModelDelegateTest.kt
@@ -0,0 +1,120 @@
+package nerd.tuxmobil.fahrplan.congress.alarms
+
+import app.cash.turbine.test
+import com.google.common.truth.Truth.assertThat
+import info.metadude.android.eventfahrplan.commons.testing.MainDispatcherTestRule
+import info.metadude.android.eventfahrplan.commons.testing.verifyInvokedOnce
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import nerd.tuxmobil.fahrplan.congress.models.Session
+import nerd.tuxmobil.fahrplan.congress.notifications.NotificationHelper
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+
+class SessionAlarmViewModelDelegateTest {
+
+ private companion object {
+ val SESSION = Session("session-23")
+ }
+
+ @get:Rule
+ val mainDispatcherTestRule = MainDispatcherTestRule()
+
+ @Test
+ fun `addAlarm() invokes addSessionAlarm() function`() = runTest {
+ val alarmServices = mock()
+ val delegate = createDelegate(alarmServices = alarmServices)
+ delegate.addAlarm(SESSION, alarmTimesIndex = 0)
+ verifyInvokedOnce(alarmServices).addSessionAlarm(SESSION, alarmTimesIndex = 0)
+ }
+
+ @Test
+ fun `deleteAlarm() invokes deleteSessionAlarm() function`() = runTest {
+ val alarmServices = mock()
+ val delegate = createDelegate(alarmServices = alarmServices)
+ delegate.deleteAlarm(SESSION)
+ verifyInvokedOnce(alarmServices).deleteSessionAlarm(SESSION)
+ }
+
+ @Test
+ fun `canAddAlarms() invokes canScheduleExactAlarms property`() = runTest {
+ val alarmServices = mock()
+ val delegate = createDelegate(alarmServices = alarmServices)
+ delegate.canAddAlarms()
+ verifyInvokedOnce(alarmServices).canScheduleExactAlarms
+ }
+
+ @Test
+ fun `addAlarmWithChecks() posts to showAlarmTimePicker`() = runTest {
+ val notificationHelper = mock {
+ on { notificationsEnabled } doReturn true
+ }
+ val alarmServices = mock {
+ on { canScheduleExactAlarms } doReturn true
+ }
+ val delegate = createDelegate(notificationHelper, alarmServices)
+ delegate.addAlarmWithChecks()
+ delegate.showAlarmTimePicker.test {
+ assertThat(awaitItem()).isEqualTo(Unit)
+ }
+ }
+
+ @Test
+ fun `addAlarmWithChecks() posts to requestScheduleExactAlarmsPermission`() = runTest {
+ val notificationHelper = mock {
+ on { notificationsEnabled } doReturn true
+ }
+ val alarmServices = mock {
+ on { canScheduleExactAlarms } doReturn false
+ }
+ val delegate = createDelegate(notificationHelper, alarmServices)
+ delegate.addAlarmWithChecks()
+ delegate.requestScheduleExactAlarmsPermission.test {
+ assertThat(awaitItem()).isEqualTo(Unit)
+ }
+ }
+
+ @Test
+ fun `addAlarmWithChecks() posts to requestPostNotificationsPermission as of Android 13`() = runTest {
+ val notificationHelper = mock {
+ on { notificationsEnabled } doReturn false
+ }
+ val delegate = createDelegate(
+ notificationHelper = notificationHelper,
+ runsAtLeastOnAndroidTiramisu = true,
+ )
+ delegate.addAlarmWithChecks()
+ delegate.requestPostNotificationsPermission.test {
+ assertThat(awaitItem()).isEqualTo(Unit)
+ }
+ }
+
+ @Test
+ fun `addAlarmWithChecks() posts to notificationsDisabled before Android 13`() = runTest {
+ val notificationHelper = mock {
+ on { notificationsEnabled } doReturn false
+ }
+ val delegate = createDelegate(
+ notificationHelper = notificationHelper,
+ runsAtLeastOnAndroidTiramisu = false,
+ )
+ delegate.addAlarmWithChecks()
+ delegate.notificationsDisabled.test {
+ assertThat(awaitItem()).isEqualTo(Unit)
+ }
+ }
+
+ private fun TestScope.createDelegate(
+ notificationHelper: NotificationHelper = mock(),
+ alarmServices: AlarmServices = mock(),
+ runsAtLeastOnAndroidTiramisu: Boolean = true,
+ ) = SessionAlarmViewModelDelegate(
+ viewModelScope = this,
+ notificationHelper = notificationHelper,
+ alarmServices = alarmServices,
+ runsAtLeastOnAndroidTiramisu = runsAtLeastOnAndroidTiramisu,
+ )
+
+}
diff --git a/app/src/test/java/nerd/tuxmobil/fahrplan/congress/details/SessionDetailsViewModelTest.kt b/app/src/test/java/nerd/tuxmobil/fahrplan/congress/details/SessionDetailsViewModelTest.kt
index 9aaaca6a7a..6e2bd1ef61 100644
--- a/app/src/test/java/nerd/tuxmobil/fahrplan/congress/details/SessionDetailsViewModelTest.kt
+++ b/app/src/test/java/nerd/tuxmobil/fahrplan/congress/details/SessionDetailsViewModelTest.kt
@@ -305,20 +305,61 @@ class SessionDetailsViewModelTest {
}
@Test
- fun `setAlarm() posts to setAlarm`() = runTest {
+ fun `canAddAlarms invokes canScheduleExactAlarms property`() {
+ val repository = createRepository()
+ val alarmServices = mock()
+ val viewModel = createViewModel(repository = repository, alarmServices = alarmServices)
+ viewModel.canAddAlarms()
+ verifyInvokedOnce(alarmServices).canScheduleExactAlarms
+ }
+
+ @Test
+ fun `addAlarmWithChecks() posts to showAlarmTimePicker`() = runTest {
+ val notificationHelper = mock {
+ on { notificationsEnabled } doReturn true
+ }
+ val alarmServices = mock {
+ on { canScheduleExactAlarms } doReturn true
+ }
+ val repository = createRepository()
+ val viewModel = createViewModel(
+ repository = repository,
+ notificationHelper = notificationHelper,
+ alarmServices = alarmServices,
+ )
+ viewModel.addAlarmWithChecks()
+ viewModel.showAlarmTimePicker.test {
+ assertThat(awaitItem()).isEqualTo(Unit)
+ }
+ verifyInvokedOnce(notificationHelper).notificationsEnabled
+ verifyInvokedOnce(alarmServices).canScheduleExactAlarms
+ }
+
+ @Test
+ fun `addAlarmWithChecks() posts to requestScheduleExactAlarmsPermission`() = runTest {
val notificationHelper = mock {
on { notificationsEnabled } doReturn true
}
+ val alarmServices = mock {
+ on { canScheduleExactAlarms } doReturn false
+ }
val repository = createRepository()
- val viewModel = createViewModel(repository, notificationHelper = notificationHelper)
- viewModel.setAlarm()
- viewModel.setAlarm.test {
+ val viewModel = createViewModel(
+ repository = repository,
+ notificationHelper = notificationHelper,
+ alarmServices = alarmServices,
+ runsAtLeastOnAndroidTiramisu = true, // not relevant
+ )
+ viewModel.addAlarmWithChecks()
+ viewModel.requestScheduleExactAlarmsPermission.test {
assertThat(awaitItem()).isEqualTo(Unit)
}
+ verifyInvokedOnce(notificationHelper).notificationsEnabled
+ verifyInvokedOnce(alarmServices).canScheduleExactAlarms
}
@Test
- fun `setAlarm() posts to requestPostNotificationsPermission`() = runTest {
+ fun `addAlarmWithChecks() posts to requestPostNotificationsPermission as of Android 13`() = runTest {
val notificationHelper = mock {
on { notificationsEnabled } doReturn false
}
@@ -326,16 +367,17 @@ class SessionDetailsViewModelTest {
val viewModel = createViewModel(
repository = repository,
notificationHelper = notificationHelper,
- runsAtLeastOnAndroidTiramisu = true
+ runsAtLeastOnAndroidTiramisu = true,
)
- viewModel.setAlarm()
+ viewModel.addAlarmWithChecks()
viewModel.requestPostNotificationsPermission.test {
assertThat(awaitItem()).isEqualTo(Unit)
}
+ verifyInvokedOnce(notificationHelper).notificationsEnabled
}
@Test
- fun `setAlarm() posts to missingPostNotificationsPermission`() = runTest {
+ fun `addAlarmWithChecks() posts to notificationsDisabled before Android 13`() = runTest {
val notificationHelper = mock {
on { notificationsEnabled } doReturn false
}
@@ -343,10 +385,10 @@ class SessionDetailsViewModelTest {
val viewModel = createViewModel(
repository = repository,
notificationHelper = notificationHelper,
- runsAtLeastOnAndroidTiramisu = false
+ runsAtLeastOnAndroidTiramisu = false,
)
- viewModel.setAlarm()
- viewModel.missingPostNotificationsPermission.test {
+ viewModel.addAlarmWithChecks()
+ viewModel.notificationsDisabled.test {
assertThat(awaitItem()).isEqualTo(Unit)
}
}
diff --git a/app/src/test/java/nerd/tuxmobil/fahrplan/congress/schedule/FahrplanViewModelTest.kt b/app/src/test/java/nerd/tuxmobil/fahrplan/congress/schedule/FahrplanViewModelTest.kt
index 871c3598d5..001da5194a 100644
--- a/app/src/test/java/nerd/tuxmobil/fahrplan/congress/schedule/FahrplanViewModelTest.kt
+++ b/app/src/test/java/nerd/tuxmobil/fahrplan/congress/schedule/FahrplanViewModelTest.kt
@@ -257,20 +257,62 @@ class FahrplanViewModelTest {
}
@Test
- fun `showAlarmTimePickerWithChecks() posts to showAlarmTimePicker`() = runTest {
+ fun `canAddAlarms invokes canScheduleExactAlarms property`() {
+ val repository = createRepository()
+ val alarmServices = mock()
+ val viewModel = createViewModel(repository = repository, alarmServices = alarmServices)
+ viewModel.canAddAlarms()
+ verifyInvokedOnce(alarmServices).canScheduleExactAlarms
+ }
+
+ @Test
+ fun `addAlarmWithChecks() posts to showAlarmTimePicker`() = runTest {
val notificationHelper = mock {
on { notificationsEnabled } doReturn true
}
+ val alarmServices = mock {
+ on { canScheduleExactAlarms } doReturn true
+ }
val repository = createRepository()
- val viewModel = createViewModel(repository, notificationHelper = notificationHelper)
- viewModel.showAlarmTimePickerWithChecks()
+ val viewModel = createViewModel(
+ repository = repository,
+ notificationHelper = notificationHelper,
+ alarmServices = alarmServices,
+ runsAtLeastOnAndroidTiramisu = true, // not relevant
+ )
+ viewModel.addAlarmWithChecks()
viewModel.showAlarmTimePicker.test {
assertThat(awaitItem()).isEqualTo(Unit)
}
+ verifyInvokedOnce(notificationHelper).notificationsEnabled
+ verifyInvokedOnce(alarmServices).canScheduleExactAlarms
+ }
+
+ @Test
+ fun `addAlarmWithChecks() posts to requestScheduleExactAlarmsPermission`() = runTest {
+ val notificationHelper = mock {
+ on { notificationsEnabled } doReturn true
+ }
+ val alarmServices = mock {
+ on { canScheduleExactAlarms } doReturn false
+ }
+ val repository = createRepository()
+ val viewModel = createViewModel(
+ repository = repository,
+ notificationHelper = notificationHelper,
+ alarmServices = alarmServices,
+ runsAtLeastOnAndroidTiramisu = true, // not relevant
+ )
+ viewModel.addAlarmWithChecks()
+ viewModel.requestScheduleExactAlarmsPermission.test {
+ assertThat(awaitItem()).isEqualTo(Unit)
+ }
+ verifyInvokedOnce(notificationHelper).notificationsEnabled
+ verifyInvokedOnce(alarmServices).canScheduleExactAlarms
}
@Test
- fun `showAlarmTimePickerWithChecks() posts to requestPostNotificationsPermission`() = runTest {
+ fun `addAlarmWithChecks() posts to requestPostNotificationsPermission as of Android 13`() = runTest {
val notificationHelper = mock {
on { notificationsEnabled } doReturn false
}
@@ -278,16 +320,17 @@ class FahrplanViewModelTest {
val viewModel = createViewModel(
repository = repository,
notificationHelper = notificationHelper,
- runsAtLeastOnAndroidTiramisu = true
+ runsAtLeastOnAndroidTiramisu = true,
)
- viewModel.showAlarmTimePickerWithChecks()
+ viewModel.addAlarmWithChecks()
viewModel.requestPostNotificationsPermission.test {
assertThat(awaitItem()).isEqualTo(Unit)
}
+ verifyInvokedOnce(notificationHelper).notificationsEnabled
}
@Test
- fun `showAlarmTimePickerWithChecks() posts to missingPostNotificationsPermission`() = runTest {
+ fun `addAlarmWithChecks() posts to notificationsDisabled before Android 13`() = runTest {
val notificationHelper = mock {
on { notificationsEnabled } doReturn false
}
@@ -295,10 +338,10 @@ class FahrplanViewModelTest {
val viewModel = createViewModel(
repository = repository,
notificationHelper = notificationHelper,
- runsAtLeastOnAndroidTiramisu = false
+ runsAtLeastOnAndroidTiramisu = false,
)
- viewModel.showAlarmTimePickerWithChecks()
- viewModel.missingPostNotificationsPermission.test {
+ viewModel.addAlarmWithChecks()
+ viewModel.notificationsDisabled.test {
assertThat(awaitItem()).isEqualTo(Unit)
}
}