Skip to content

Commit

Permalink
Fix issue related to new Alarm&Reminders permission on A14, closes ma…
Browse files Browse the repository at this point in the history
  • Loading branch information
GitGitro committed Dec 21, 2023
1 parent 363bb99 commit 0ceaf69
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.provider.Settings
import android.util.Log
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.RequiresApi
import com.maltaisn.notes.model.ReminderAlarmCallback
import com.maltaisn.notes.model.ReminderAlarmManager
import javax.inject.Inject
Expand All @@ -34,14 +38,24 @@ class ReceiverAlarmCallback @Inject constructor(
) : ReminderAlarmCallback {

private val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
private var requestPermissionLauncher: ActivityResultLauncher<String>? = null
private var TAG = "CrashAlarmPermission"

@RequiresApi(Build.VERSION_CODES.S)
override fun addAlarm(noteId: Long, time: Long) {
val alarmIntent = getAlarmPendingIndent(noteId)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP, time, alarmIntent)
} else {
alarmManager.setExact(AlarmManager.RTC_WAKEUP, time, alarmIntent)
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP, time, alarmIntent)
} else {
alarmManager.setExact(AlarmManager.RTC_WAKEUP, time, alarmIntent)
}
}catch (se: SecurityException){
Log.d(TAG,"Crash: the user removed the permission SCHEDULE_EXACT_ALARM at runtime " +
"or the android setting 'Pause app activity if unused' has been triggered")
//asking again
requestPermissionLauncher?.launch(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM)
}
}

Expand Down
13 changes: 12 additions & 1 deletion app/src/main/kotlin/com/maltaisn/notes/ui/home/HomeFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ package com.maltaisn.notes.ui.home

import android.Manifest
import android.app.ActivityManager
import android.app.AlarmManager
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.ActionMode
import android.view.MenuItem
import android.view.View
Expand Down Expand Up @@ -88,7 +90,16 @@ class HomeFragment : NoteFragment(), Toolbar.OnMenuItemClickListener {
}
}

viewModel.updateRestrictions(batteryRestricted, notificationRestricted)
var reminderRestricted = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE){
val alarmManager: AlarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
if (!alarmManager.canScheduleExactAlarms()){
Log.d("TAG","Crash" +alarmManager.canScheduleExactAlarms())
reminderRestricted = true
}
}

viewModel.updateRestrictions(batteryRestricted, notificationRestricted, reminderRestricted)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
Expand Down
10 changes: 8 additions & 2 deletions app/src/main/kotlin/com/maltaisn/notes/ui/home/HomeViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class HomeViewModel @AssistedInject constructor(

private var batteryRestricted = false
private var notificationsRestricted = false
private var remindersRestricted = false

private val _fabShown = MutableLiveData<Boolean>()
val fabShown: LiveData<Boolean>
Expand Down Expand Up @@ -161,10 +162,11 @@ class HomeViewModel @AssistedInject constructor(
}

/** Update restrictions status so that appropriate warnings may be shown to user. */
fun updateRestrictions(battery: Boolean, notifications: Boolean) {
val updateList = battery != batteryRestricted || notifications != notificationsRestricted
fun updateRestrictions(battery: Boolean, notifications: Boolean, reminders: Boolean) {
val updateList = battery != batteryRestricted || notifications != notificationsRestricted || reminders != remindersRestricted
batteryRestricted = battery
notificationsRestricted = notifications
remindersRestricted = reminders
if (updateList) {
updateNoteList()
}
Expand Down Expand Up @@ -343,6 +345,9 @@ class HomeViewModel @AssistedInject constructor(
if (notes.isNotEmpty() && notificationsRestricted) {
this += MessageItem(NOTIFICATION_DENIED_ITEM_ID, R.string.reminder_notif_permission_denied)
}
if (notes.isNotEmpty() && remindersRestricted) {
this += MessageItem(REMINDER_DENIED_ITEM_ID, R.string.reminder_alarm_permission_denied)
}

var addedOverdueHeader = false
var addedTodayHeader = false
Expand Down Expand Up @@ -414,6 +419,7 @@ class HomeViewModel @AssistedInject constructor(
private const val BATTERY_RESTRICTED_ITEM_ID = -8L
private const val AUTO_EXPORT_FAIL_ITEM_ID = -9L
private const val NOTIFICATION_DENIED_ITEM_ID = -10L
private const val REMINDER_DENIED_ITEM_ID = -11L

val PINNED_HEADER_ITEM = HeaderItem(-2, R.string.note_pinned)
val NOT_PINNED_HEADER_ITEM = HeaderItem(-3, R.string.note_not_pinned)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ class ReminderDialog : DialogFragment(), RecurrenceListCallback, RecurrencePicke
private val recurrenceFormat = RecurrenceFormatter(dateFormat)

private var notificationPermission: NotificationPermission? = null
private var reminderPermission: ReminderPermission? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand Down Expand Up @@ -127,6 +128,12 @@ class ReminderDialog : DialogFragment(), RecurrenceListCallback, RecurrencePicke
deniedListener = { dismiss() }
request()
}
reminderPermission = getContext()?.let {
ReminderPermission(this, it).apply {
deniedListener = { dismiss() }
request()
}
}
}

viewModel.start(args.noteIds.toList())
Expand All @@ -137,6 +144,7 @@ class ReminderDialog : DialogFragment(), RecurrenceListCallback, RecurrencePicke
override fun onDestroy() {
super.onDestroy()
notificationPermission = null
reminderPermission = null
}

private fun setupViewModelObservers(binding: DialogReminderBinding) {
Expand Down Expand Up @@ -264,14 +272,17 @@ class ReminderDialog : DialogFragment(), RecurrenceListCallback, RecurrencePicke

override fun onDialogPositiveButtonClicked(tag: String?) {
notificationPermission?.onDialogPositiveButtonClicked(tag)
reminderPermission?.onDialogPositiveButtonClicked(tag)
}

override fun onDialogNegativeButtonClicked(tag: String?) {
notificationPermission?.onDialogNegativeButtonClicked(tag)
reminderPermission?.onDialogNegativeButtonClicked(tag)
}

override fun onDialogCancelled(tag: String?) {
notificationPermission?.onDialogCancelled(tag)
reminderPermission?.onDialogCancelled(tag)
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package com.maltaisn.notes.ui.reminder

import android.Manifest
import android.app.AlarmManager
import android.content.Context
import android.content.Intent
import android.os.Build
import android.provider.Settings
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment
import com.maltaisn.notes.R
import com.maltaisn.notes.ui.common.ConfirmDialog

class ReminderPermission(
val fragment: Fragment,
val context: Context
) : ConfirmDialog.Callback {

private var requestPermissionLauncher: ActivityResultLauncher<String>? = null
private var permissionRequested = false

/**
* Optional listener called if permission was denied or may have been denied.
*/
var deniedListener: (() -> Unit)? = null

init {
requestPermissionLauncher = fragment.registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (!isGranted) {
if (permissionRequested) {
// Explanation was just shown, user denied permission.
deniedListener?.invoke()
} else {
// Ask user to go to the app's notification settings to grant the permission.
// Only do this if the permission wasn't requested just before.
ConfirmDialog.newInstance(
message = R.string.reminder_alarm_permission,
btnPositive = R.string.action_ok,
).show(fragment.childFragmentManager,
ReminderPermission.REMINDER_PERMISSION_DENIED_DIALOG
)
}
}
}
}

fun request() {
if (Build.VERSION.SDK_INT < 34) {
return
}
val alarmManager: AlarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
when {
alarmManager.canScheduleExactAlarms() -> {
// OK, permission already granted.
}

fragment.shouldShowRequestPermissionRationale(Manifest.permission.SCHEDULE_EXACT_ALARM) -> {
// Show dialog explaning permission request.
ConfirmDialog.newInstance(
message = R.string.reminder_alarm_permission,
btnPositive = R.string.action_ok,
).show(fragment.childFragmentManager,
ReminderPermission.REMINDER_PERMISSION_DIALOG
)
permissionRequested = true
}

else -> {
requestPermissionLauncher?.launch(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM)
}
}
}

override fun onDialogPositiveButtonClicked(tag: String?) {
if (Build.VERSION.SDK_INT < 34) {
return
}

when (tag) {
ReminderPermission.REMINDER_PERMISSION_DIALOG -> {
// First time asking, can request normally.
requestPermissionLauncher?.launch(Manifest.permission.SCHEDULE_EXACT_ALARM)
}

ReminderPermission.REMINDER_PERMISSION_DENIED_DIALOG -> {
// Not first time asking, open notification settings window to let user do it.
val settingsIntent = Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(Settings.EXTRA_APP_PACKAGE, fragment.requireContext().packageName)
fragment.startActivity(settingsIntent)
// Dismiss immediately since at this point we can't know if user did enable notifications or not.
deniedListener?.invoke()
}
}
}

override fun onDialogNegativeButtonClicked(tag: String?) {
if (tag == REMINDER_PERMISSION_DIALOG || tag == REMINDER_PERMISSION_DENIED_DIALOG) {
// Notification permission was denied, no point in setting reminder.
deniedListener?.invoke()
}
}

override fun onDialogCancelled(tag: String?) {
onDialogNegativeButtonClicked(tag)
}

companion object {
private const val REMINDER_PERMISSION_DIALOG = "alarm-permission-dialog"
private const val REMINDER_PERMISSION_DENIED_DIALOG = "alarm-permission-denied-dialog"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import com.maltaisn.notes.ui.common.ConfirmDialog
import com.maltaisn.notes.ui.main.MainActivity
import com.maltaisn.notes.ui.notification.NotificationPermission
import com.maltaisn.notes.ui.observeEvent
import com.maltaisn.notes.ui.reminder.ReminderPermission
import com.maltaisn.notes.ui.viewModel
import com.mikepenz.aboutlibraries.LibsBuilder
import java.text.DateFormat
Expand All @@ -71,6 +72,7 @@ class SettingsFragment : PreferenceFragmentCompat(), ConfirmDialog.Callback, Exp
private var importDataLauncher: ActivityResultLauncher<Intent>? = null

private var notificationPermission: NotificationPermission? = null
private var reminderPermission: ReminderPermission? = null

override fun onCreate(state: Bundle?) {
super.onCreate(state)
Expand Down Expand Up @@ -133,6 +135,7 @@ class SettingsFragment : PreferenceFragmentCompat(), ConfirmDialog.Callback, Exp
}

notificationPermission = NotificationPermission(this)
reminderPermission = getContext()?.let { ReminderPermission(this, it) }

enterTransition = MaterialElevationScale(false).apply {
duration = resources.getInteger(RMaterial.integer.material_motion_duration_short_2).toLong()
Expand Down Expand Up @@ -183,6 +186,9 @@ class SettingsFragment : PreferenceFragmentCompat(), ConfirmDialog.Callback, Exp
viewModel.askNotificationPermission.observeEvent(viewLifecycleOwner) {
notificationPermission?.request()
}
viewModel.askReminderPermission.observeEvent(viewLifecycleOwner) {
reminderPermission?.request()
}
}

override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
Expand Down Expand Up @@ -292,6 +298,7 @@ class SettingsFragment : PreferenceFragmentCompat(), ConfirmDialog.Callback, Exp
importDataLauncher = null
autoExportLauncher = null
notificationPermission = null
reminderPermission = null
}

private fun showMessage(@StringRes messageId: Int) {
Expand Down Expand Up @@ -335,7 +342,8 @@ class SettingsFragment : PreferenceFragmentCompat(), ConfirmDialog.Callback, Exp
.addCategory(Intent.CATEGORY_OPENABLE)
autoExportLauncher?.launch(intent)
}
else -> notificationPermission?.onDialogPositiveButtonClicked(tag)
NOTIF_PERMISSION_DIALOG -> notificationPermission?.onDialogPositiveButtonClicked(tag)
else -> reminderPermission?.onDialogPositiveButtonClicked(tag)
}
}

Expand All @@ -345,7 +353,8 @@ class SettingsFragment : PreferenceFragmentCompat(), ConfirmDialog.Callback, Exp
// No file chosen for auto export, disable it.
autoExportPref.isChecked = false
}
else -> notificationPermission?.onDialogNegativeButtonClicked(tag)
NOTIF_PERMISSION_DIALOG -> notificationPermission?.onDialogNegativeButtonClicked(tag)
else -> reminderPermission?.onDialogPositiveButtonClicked(tag)
}
}

Expand All @@ -355,7 +364,8 @@ class SettingsFragment : PreferenceFragmentCompat(), ConfirmDialog.Callback, Exp
// No file chosen for auto export, disable it.
autoExportPref.isChecked = false
}
else -> notificationPermission?.onDialogCancelled(tag)
NOTIF_PERMISSION_DIALOG -> notificationPermission?.onDialogCancelled(tag)
else -> reminderPermission?.onDialogPositiveButtonClicked(tag)
}
}

Expand Down Expand Up @@ -384,5 +394,6 @@ class SettingsFragment : PreferenceFragmentCompat(), ConfirmDialog.Callback, Exp
private const val RESTART_DIALOG_TAG = "restart_dialog"
private const val CLEAR_DATA_DIALOG_TAG = "clear_data_dialog"
private const val AUTOMATIC_EXPORT_DIALOG_TAG = "automatic_export_dialog"
private const val NOTIF_PERMISSION_DIALOG = "notif-permission-dialog"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ class SettingsViewModel @AssistedInject constructor(
val askNotificationPermission: LiveData<Event<Unit>>
get() = _askNotificationPermission

private val _askReminderPermission = MutableLiveData<Event<Unit>>()
val askReminderPermission: LiveData<Event<Unit>>
get() = _askReminderPermission

private var importJsonData = savedStateHandle[KEY_IMPORTED_JSON_DATA] ?: ""
set(value) {
field = value
Expand Down Expand Up @@ -194,6 +198,7 @@ class SettingsViewModel @AssistedInject constructor(
notesRepository.getNotesWithReminder().firstOrNull() != null
) {
_askNotificationPermission.send()
_askReminderPermission.send()
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,13 @@
app has restricted battery usage. This can be changed in the app settings.</string>
<string name="reminder_notif_permission">To be able to send reminder notifications, the app needs a permission.
Click OK to show the permission request. You will be able to set a reminder afterwards.</string>
<string name="reminder_alarm_permission">To be able to send reminder notifications starting from
Android 14, the app needs another permission. Click OK to show the permission request.
You will be able to set reminder afterwards.</string>
<string name="reminder_notif_permission_denied">The notification permission has been denied.
Reminder alarms will not work until enabled.</string>
<string name="reminder_alarm_permission_denied">The reminder permission has been denied.
Reminder alarms will not work until enabled.</string>

<!-- Labels -->
<string name="label_create">Create new label</string>
Expand Down

0 comments on commit 0ceaf69

Please sign in to comment.