Skip to content

Commit

Permalink
Merge pull request #475 from amberin/473-show-snackbar-for-exact-alar…
Browse files Browse the repository at this point in the history
…ms-permission

Clean up handling of the "schedule exact alarms" permission
  • Loading branch information
amberin authored Jan 6, 2025
2 parents 6f1d904 + d18bf95 commit 38c2e5a
Show file tree
Hide file tree
Showing 10 changed files with 72 additions and 90 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ public void testRangeTaskMarkedDone() {

@Test
public void testMoveTaskWithRepeaterToTomorrow() {
EspressoUtils.grantAlarmsAndRemindersPermission();
EspressoUtils.grantAlarmsAndRemindersSpecialPermission();
DateTime tomorrow = DateTime.now().withTimeAtStartOfDay().plusDays(1);
scenario = defaultSetUp();
searchForTextCloseKeyboard(".it.done ad.7");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ public void testSchedulingMultipleNotes() {
"*** DONE Note #5.\n" +
"CLOSED: [2014-06-03 Tue 13:34]\n" +
"");
EspressoUtils.grantAlarmsAndRemindersPermission();
EspressoUtils.grantAlarmsAndRemindersSpecialPermission();
try (ActivityScenario<MainActivity> ignored = ActivityScenario.launch(MainActivity.class)) {
onView(allOf(withText("book-name"), isDisplayed())).perform(click());

Expand Down Expand Up @@ -272,7 +272,7 @@ public void testBookTitleMustBeDisplayedWhenOpeningBookFromDrawer() {

@Test
public void testTimestampDialogTimeButtonValueWhenToggling() {
EspressoUtils.grantAlarmsAndRemindersPermission();
EspressoUtils.grantAlarmsAndRemindersSpecialPermission();
testUtils.setupBook("book-name", "Sample book used for tests\n" +
"* TODO Note #1.\n" +
"SCHEDULED: <2015-01-18 04:05 +6d>\n" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
import static com.orgzly.android.espresso.util.EspressoUtils.contextualToolbarOverflowMenu;
import static com.orgzly.android.espresso.util.EspressoUtils.grantAlarmsAndRemindersPermission;
import static com.orgzly.android.espresso.util.EspressoUtils.grantAlarmsAndRemindersSpecialPermission;
import static com.orgzly.android.espresso.util.EspressoUtils.onActionItemClick;
import static com.orgzly.android.espresso.util.EspressoUtils.onBook;
import static com.orgzly.android.espresso.util.EspressoUtils.onNoteInBook;
Expand Down Expand Up @@ -257,7 +257,7 @@ public void testClickingNote() {
@Test
public void testSchedulingNote() {
defaultSetUp();
grantAlarmsAndRemindersPermission();
grantAlarmsAndRemindersSpecialPermission();

onView(withId(R.id.drawer_layout)).perform(open());
onView(withText("Scheduled")).perform(click());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -511,18 +511,25 @@ public void perform(final UiController uiController, final View view) {
};
}

public static void grantAlarmsAndRemindersPermission() {
public static void grantAlarmsAndRemindersSpecialPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
String shellCmd = "appops set --uid com.orgzlyrevived SCHEDULE_EXACT_ALARM allow";
getInstrumentation().getUiAutomation().executeShellCommand(shellCmd);
}
}

public static void denyAlarmsAndRemindersSpecialPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
String shellCmd = "appops set --uid com.orgzlyrevived SCHEDULE_EXACT_ALARM deny";
getInstrumentation().getUiAutomation().executeShellCommand(shellCmd);
}
}

/**
* Utility method for starting sync using drawer button.
*/
public static void sync() {
grantAlarmsAndRemindersPermission();
grantAlarmsAndRemindersSpecialPermission();
onView(withId(R.id.drawer_layout)).perform(open());
onView(withId(R.id.sync_button_container)).perform(click());
onView(withId(R.id.drawer_layout)).perform(close());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,13 @@ class RemindersScheduler @Inject constructor(val context: Application, val logs:
}
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (!alarmManager.canScheduleExactAlarms()) {
throw SecurityException("Missing permission to schedule alarm")
}
}

// TODO: Add preferences to control *how* to schedule the alarms
if (hasTime) {
if (!AppPermissions.canScheduleExactAlarms(context)) {
// We will not be allowed to schedule this reminder, but the user will
// hopefully grant the permission before our next scheduling attempt.
return
}
if (AppPreferences.remindersUseAlarmClockForTodReminders(context)) {
scheduleAlarmClock(alarmManager, intent, inMs, origin)
} else {
Expand All @@ -104,10 +103,10 @@ class RemindersScheduler @Inject constructor(val context: Application, val logs:
scheduleExact(alarmManager, intent, inMs, origin)
}
}

} else {
// Does not trigger while dozing
scheduleExact(alarmManager, intent, inMs, origin)
// This reminder does not contain clock time information; it's
// probably a daily reminder. Schedule an inexact alarm.
scheduleInExact(alarmManager, intent, inMs, origin)
}

// Intent received, notifications not displayed by default
Expand All @@ -130,6 +129,14 @@ class RemindersScheduler @Inject constructor(val context: Application, val logs:
logScheduled("setExact", origin, inMs)
}

private fun scheduleInExact(alarmManager: AlarmManager, intent: PendingIntent, inMs: Long, origin: String) {
alarmManager.set(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + inMs,
intent)
logScheduled("set", origin, inMs)
}

@RequiresApi(Build.VERSION_CODES.M)
private fun scheduleExactAndAllowWhileIdle(alarmManager: AlarmManager, intent: PendingIntent, inMs: Long, origin: String) {
alarmManager.setExactAndAllowWhileIdle(
Expand Down
36 changes: 5 additions & 31 deletions app/src/main/java/com/orgzly/android/sync/SyncWorker.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
package com.orgzly.android.sync

import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
Expand All @@ -17,9 +13,12 @@ import com.orgzly.android.data.logs.AppLogsRepository
import com.orgzly.android.db.entity.BookAction
import com.orgzly.android.prefs.AppPreferences
import com.orgzly.android.reminders.RemindersScheduler
import com.orgzly.android.repos.*
import com.orgzly.android.repos.DirectoryRepo
import com.orgzly.android.repos.RepoType
import com.orgzly.android.repos.RepoUtils
import com.orgzly.android.repos.SyncRepo
import com.orgzly.android.repos.TwoWaySyncRepo
import com.orgzly.android.ui.notifications.SyncNotifications
import com.orgzly.android.ui.util.getAlarmManager
import com.orgzly.android.ui.util.haveNetworkConnection
import com.orgzly.android.util.AppPermissions
import com.orgzly.android.util.LogMajorEvents
Expand Down Expand Up @@ -169,31 +168,6 @@ class SyncWorker(val context: Context, val params: WorkerParameters) :
}
}

/* Make sure we have permission to set alarms & reminders,
* since this typically happens when new books are parsed.
*/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (!context.getAlarmManager().canScheduleExactAlarms()) {
if (
AppPreferences.remindersForDeadlineEnabled(context) ||
AppPreferences.remindersForScheduledEnabled(context) ||
AppPreferences.remindersForEventsEnabled(context)
) {
if (App.getCurrentActivity() != null) {
val uri = Uri.parse("package:" + BuildConfig.APPLICATION_ID)
App.getCurrentActivity().startActivity(
Intent(
Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM,
uri
)
)
}
return SyncState.getInstance((SyncState.Type.FAILED_NO_ALARMS_PERMISSION))
}
}
}


return null
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@ package com.orgzly.android.ui.dialogs
import android.app.DatePickerDialog
import android.app.Dialog
import android.app.TimePickerDialog
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.text.format.DateFormat
import android.view.LayoutInflater
import android.view.View
Expand All @@ -19,13 +15,13 @@ import com.orgzly.BuildConfig
import com.orgzly.R
import com.orgzly.android.ui.TimeType
import com.orgzly.android.ui.util.KeyboardUtils
import com.orgzly.android.ui.util.getAlarmManager
import com.orgzly.android.util.LogUtils
import com.orgzly.android.util.UserTimeFormatter
import com.orgzly.databinding.DialogTimestampBinding
import com.orgzly.databinding.DialogTimestampTitleBinding
import com.orgzly.org.datetime.OrgDateTime
import java.util.*
import java.util.Calendar
import java.util.TreeSet

class TimestampDialogFragment : DialogFragment(), View.OnClickListener {
private var listener: OnDateTimeSetListener? = null
Expand Down Expand Up @@ -88,9 +84,6 @@ class TimestampDialogFragment : DialogFragment(), View.OnClickListener {
binding.timePickerButton.setOnClickListener(this)
binding.timeUsedCheckbox.setOnCheckedChangeListener { _, isChecked ->
viewModel.setIsTimeUsed(isChecked)
if (isChecked) {
ensureAlarmPermissions()
}
}

binding.endTimePickerButton.setOnClickListener(this)
Expand Down Expand Up @@ -123,14 +116,7 @@ class TimestampDialogFragment : DialogFragment(), View.OnClickListener {
.setCustomTitle(titleBinding.root)
.setView(binding.root)
.setPositiveButton(R.string.set) { _, _ ->
val time = viewModel.getOrgDateTime()
if (time != null && time.hasTime()) {
if (isAlarmPermissionGranted()) {
listener?.onDateTimeSet(dialogId, noteIds, time)
}
} else {
listener?.onDateTimeSet(dialogId, noteIds, time)
}
listener?.onDateTimeSet(dialogId, noteIds, viewModel.getOrgDateTime())
}
.setNeutralButton(R.string.clear) { _, _ ->
listener?.onDateTimeSet(dialogId, noteIds, null)
Expand All @@ -141,29 +127,6 @@ class TimestampDialogFragment : DialogFragment(), View.OnClickListener {
.show()
}

private fun isAlarmPermissionGranted(): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (!requireContext().getAlarmManager().canScheduleExactAlarms()) {
MaterialAlertDialogBuilder(requireContext())
.setTitle("Alarms & reminders permission needed")
.setMessage("The app needs the \"alarms & reminders\" permission to set exact times for scheduled/deadline. Please grant the permission in the \"app info\" screen.")
.setPositiveButton(R.string.ok, null)
.show()
return false
}
}
return true
}

private fun ensureAlarmPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (!requireContext().getAlarmManager().canScheduleExactAlarms()) {
val uri = Uri.parse("package:" + BuildConfig.APPLICATION_ID)
activity?.startActivity(Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM, uri))
}
}
}

/**
* Receives all dialog's clicks
*/
Expand Down
14 changes: 13 additions & 1 deletion app/src/main/java/com/orgzly/android/ui/util/ActivityUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import android.content.res.Configuration
import android.net.Uri
import android.os.Build
import android.util.DisplayMetrics
import android.view.*
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.menu.ActionMenuItemView
import androidx.appcompat.widget.Toolbar
Expand All @@ -36,6 +41,13 @@ object ActivityUtils {
activity.startActivity(intent)
}

@RequiresApi(Build.VERSION_CODES.S)
fun openAppScheduleExactAlarmsPermissionSetting(activity: Activity) {
val intent = Intent(android.provider.Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM)
intent.data = Uri.parse("package:" + BuildConfig.APPLICATION_ID)
activity.startActivity(intent)
}

@JvmStatic
fun mainActivityPendingIntent(context: Context, bookId: Long, noteId: Long): PendingIntent {
if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, bookId, noteId)
Expand Down
20 changes: 19 additions & 1 deletion app/src/main/java/com/orgzly/android/util/AppPermissions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.orgzly.BuildConfig
import com.orgzly.R
import com.orgzly.android.App
import com.orgzly.android.ui.CommonActivity
import com.orgzly.android.ui.showSnackbar
import com.orgzly.android.ui.util.ActivityUtils
import com.orgzly.android.ui.util.getAlarmManager

object AppPermissions {
private val TAG = AppPermissions::class.java.name
Expand Down Expand Up @@ -75,6 +77,7 @@ object AppPermissions {
else
Manifest.permission.READ_EXTERNAL_STORAGE
Usage.POST_NOTIFICATIONS -> Manifest.permission.POST_NOTIFICATIONS
Usage.SCHEDULE_EXACT_ALARM -> Manifest.permission.SCHEDULE_EXACT_ALARM
}
}

Expand All @@ -87,15 +90,30 @@ object AppPermissions {
Usage.SAVED_SEARCHES_EXPORT_IMPORT -> R.string.storage_permissions_missing
Usage.EXTERNAL_FILES_ACCESS -> R.string.permissions_rationale_for_external_files_access
Usage.POST_NOTIFICATIONS -> R.string.permissions_rationale_for_post_notifications
Usage.SCHEDULE_EXACT_ALARM -> R.string.permissions_rationale_for_schedule_exact_alarms
}
}

@JvmStatic
fun canScheduleExactAlarms(context: Context): Boolean {
var isGranted = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !context.getAlarmManager().canScheduleExactAlarms()) {
isGranted = false
val activity = App.getCurrentActivity()
activity?.showSnackbar(rationaleForRequest(Usage.SCHEDULE_EXACT_ALARM), R.string.settings) {
ActivityUtils.openAppScheduleExactAlarmsPermissionSetting(activity)
}
}
return isGranted
}

enum class Usage {
LOCAL_REPO,
BOOK_EXPORT,
SYNC_START,
SAVED_SEARCHES_EXPORT_IMPORT,
EXTERNAL_FILES_ACCESS,
POST_NOTIFICATIONS
POST_NOTIFICATIONS,
SCHEDULE_EXACT_ALARM,
}
}
3 changes: 2 additions & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,8 @@
<string name="permissions_rationale_for_book_export">Exporting notebook requires storage permission</string>
<string name="permissions_rationale_for_sync_start">Syncing with local repositories requires storage permission</string>
<string name="permissions_rationale_for_external_files_access">Accessing external files requires storage permission</string>
<string name="permissions_rationale_for_post_notifications">Permission is required to post notifications</string>
<string name="permissions_rationale_for_post_notifications">Permission needed to post notifications</string>
<string name="permissions_rationale_for_schedule_exact_alarms">Permission needed to schedule reminders at exact times</string>

<string name="cycle_visibility">Fold/Unfold All</string>
<string name="running_database_update">Upgrading database…</string>
Expand Down

0 comments on commit 38c2e5a

Please sign in to comment.