Skip to content

Commit

Permalink
Merge branch 'master' into fosdem-2024
Browse files Browse the repository at this point in the history
  • Loading branch information
johnjohndoe committed Jan 27, 2024
2 parents 890ab43 + 0d72920 commit aadaba3
Show file tree
Hide file tree
Showing 15 changed files with 545 additions and 102 deletions.
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>

<application
android:icon="@mipmap/ic_launcher"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.S
import androidx.annotation.VisibleForTesting
import androidx.core.app.AlarmManagerCompat
import info.metadude.android.eventfahrplan.commons.logging.Logging
import info.metadude.android.eventfahrplan.commons.temporal.DateFormatter
import info.metadude.android.eventfahrplan.commons.temporal.Moment
Expand All @@ -31,7 +34,8 @@ class AlarmServices @VisibleForTesting constructor(
private val alarmTimeValues: List<String>,
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,

) {

Expand Down Expand Up @@ -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].
Expand All @@ -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
)
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Unit>()
val showAlarmTimePicker = mutableShowAlarmTimePicker
.receiveAsFlow()

private val mutableRequestPostNotificationsPermission = Channel<Unit>()
val requestPostNotificationsPermission = mutableRequestPostNotificationsPermission
.receiveAsFlow()

private val mutableNotificationsDisabled = Channel<Unit>()
val notificationsDisabled = mutableNotificationsDisabled
.receiveAsFlow()

private val mutableRequestScheduleExactAlarmsPermission = Channel<Unit>()
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 <E> SendChannel<E>.sendOneTimeEvent(event: E) {
viewModelScope.launch {
send(event)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -82,7 +86,8 @@ class SessionDetailsFragment : Fragment() {
}

}
private lateinit var permissionRequestLauncher: ActivityResultLauncher<String>
private lateinit var postNotificationsPermissionRequestLauncher: ActivityResultLauncher<String>
private lateinit var scheduleExactAlarmsPermissionRequestLauncher: ActivityResultLauncher<Intent>
private lateinit var appRepository: AppRepository
private lateinit var alarmServices: AlarmServices
private lateinit var notificationHelper: NotificationHelper
Expand All @@ -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() },
Expand All @@ -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)
}

Expand Down Expand Up @@ -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))
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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()
}
Expand Down
Loading

0 comments on commit aadaba3

Please sign in to comment.