From 20168d1d7bde4376c60fc4f01126f89576afed09 Mon Sep 17 00:00:00 2001 From: Aaron Brethorst Date: Wed, 31 Jan 2024 18:05:28 -0800 Subject: [PATCH 1/2] Ask for GET_EXACT_ALARM Permission on Android 13 Fixes #1106 - Notifications and reminders of bus arrivals have stopped working --- .../android/tripservice/TripService.java | 89 +++++++++++++++---- .../android/ui/TripInfoActivity.java | 31 +++++++ .../src/main/res/values-es/strings.xml | 4 + .../src/main/res/values-fi/strings.xml | 4 + .../src/main/res/values-it/strings.xml | 4 + .../src/main/res/values/strings.xml | 4 + 6 files changed, 121 insertions(+), 15 deletions(-) diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/tripservice/TripService.java b/onebusaway-android/src/main/java/org/onebusaway/android/tripservice/TripService.java index 23024c4b6..3e00b27a8 100644 --- a/onebusaway-android/src/main/java/org/onebusaway/android/tripservice/TripService.java +++ b/onebusaway-android/src/main/java/org/onebusaway/android/tripservice/TripService.java @@ -30,6 +30,7 @@ import android.os.IBinder; import android.os.Parcel; import android.os.RemoteException; +import android.provider.Settings; import android.util.Log; import androidx.core.app.NotificationCompat; @@ -286,28 +287,86 @@ public static void scheduleAll(Context context, boolean startForeground) { } public static void pollTrip(Context context, Uri alertUri, long triggerTime) { - Intent intent = new Intent(TripService.ACTION_POLL, alertUri, - context, AlarmReceiver.class); + PendingIntent alarmIntent = createAlarmIntent(context, alertUri); + scheduleAlarm(context, triggerTime, alarmIntent); + } + + /** + * Informs the caller as to whether alarms can be scheduled, or if permission + * must be granted first. + * + * @apiNote On Android pre-Tiramisu, you could schedule an exact alarm without requesting + * any additional permissions. Newer versions of the OS, on the other hand, + * require the user to grant explicit permission. + * + * @param context + * @return True if an alarm can be scheduled and false if permission is required first. + */ + public static boolean canScheduleExactAlarms(Context context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + return true; + } + + AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + return alarmManager.canScheduleExactAlarms(); + } + + /** + * Requests the SCHEDULE_EXACT_ALARM permission on Android Tiramisu and newer. + * Does nothing on older versions. + * @param context + */ + public static void requestScheduleExactAlarmsPermission(Context context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + return; + } + + Intent intent = new Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM); + intent.setData(Uri.parse("package:" + context.getPackageName())); + context.startActivity(intent); + } + + private static void scheduleAlarm(Context context, long triggerTime, PendingIntent alarmIntent) { + AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + + // Android 5.1 and earlier just uses `set()`. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + alarmManager.set(AlarmManager.RTC_WAKEUP, triggerTime, alarmIntent); + return; + } + + // Android 6 - 12 can directly call `setExactAndAllowWhileIdle()`. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + setExactAlarm(alarmManager, triggerTime, alarmIntent); + return; + } + + if (canScheduleExactAlarms(context)) { + setExactAlarm(alarmManager, triggerTime, alarmIntent); + } + } + + private static void setExactAlarm(AlarmManager alarmManager, long triggerTime, PendingIntent alarmIntent) { + // Try to cut through Doze so alarm still triggers - See #558 + // Note that we intentionally do NOT use alarm.setAlarmClock() because this creates + // an alarm in the user's status bar and notification drawer which can be annoying - see + // https://stackoverflow.com/questions/34699662/how-does-alarmmanager-alarmclockinfos-pendingintent-work + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTime, alarmIntent); + } + } + + private static PendingIntent createAlarmIntent(Context context, Uri alertUri) { + Intent intent = new Intent(TripService.ACTION_POLL, alertUri, context, AlarmReceiver.class); + int flags; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { flags = PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_MUTABLE; } else { flags = PendingIntent.FLAG_ONE_SHOT; } - PendingIntent alarmIntent = PendingIntent.getBroadcast(context, 0, - intent, flags); - AlarmManager alarm = - (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - // Try to cut through Doze so alarm still triggers - See #558 - // Note that we intentionally do NOT use alarm.setAlarmClock() because this creates - // an alarm in the user's status bar and notification drawer which can be annoying - see - // https://stackoverflow.com/questions/34699662/how-does-alarmmanager-alarmclockinfos-pendingintent-work - alarm.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTime, alarmIntent); - } else { - alarm.set(AlarmManager.RTC_WAKEUP, triggerTime, alarmIntent); - } + return PendingIntent.getBroadcast(context, 0, intent, flags); } public static void notifyTrip(Context context, Uri alertUri, String notifyTitle, String notifyText) { diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/ui/TripInfoActivity.java b/onebusaway-android/src/main/java/org/onebusaway/android/ui/TripInfoActivity.java index c19655209..119c4c6d9 100644 --- a/onebusaway-android/src/main/java/org/onebusaway/android/ui/TripInfoActivity.java +++ b/onebusaway-android/src/main/java/org/onebusaway/android/ui/TripInfoActivity.java @@ -15,6 +15,7 @@ */ package org.onebusaway.android.ui; +import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.content.ContentResolver; @@ -26,6 +27,7 @@ import android.database.Cursor; import android.net.Uri; import android.os.Bundle; +import android.provider.Settings; import android.text.format.DateUtils; import android.util.Log; import android.view.LayoutInflater; @@ -415,6 +417,14 @@ public void saveTrip() { // Reminder time // Repeats // + + // On Android Tiramisu and up, we must request + // permission to schedule exact alarms. + if (!TripService.canScheduleExactAlarms(getActivity())) { + showRequestAlarmsPermissionDialog(getActivity()); + return; + } + View view = getView(); final Spinner reminderView = (Spinner) view.findViewById(R.id.trip_info_reminder_time); final TextView nameView = (TextView) view.findViewById(R.id.name); @@ -455,6 +465,27 @@ public void saveTrip() { finish(); } + private void showRequestAlarmsPermissionDialog(Context context) { + AlertDialog.Builder builder = new AlertDialog.Builder(context) + .setTitle(R.string.trip_info_grant_exact_alarms_permission_title) + .setCancelable(true) + .setMessage(R.string.trip_info_grant_exact_alarms_permission_message) + .setPositiveButton( + R.string.trip_info_grant_exact_alarms_permission_positive_button, + (dialog, which) -> { + TripService.requestScheduleExactAlarmsPermission(context); + dialog.dismiss(); + } + ) + .setNegativeButton( + R.string.trip_info_grant_exact_alarms_permission_negative_button, + (dialog, which) -> { + dialog.dismiss(); + }); + AlertDialog alertDialog = builder.create(); + alertDialog.show(); + } + void showReminderDaysDialog() { final boolean[] checks = ObaContract.Trips.daysToArray(mReminderDays); Bundle args = new Bundle(); diff --git a/onebusaway-android/src/main/res/values-es/strings.xml b/onebusaway-android/src/main/res/values-es/strings.xml index 06cabef9d..70ed0cda6 100644 --- a/onebusaway-android/src/main/res/values-es/strings.xml +++ b/onebusaway-android/src/main/res/values-es/strings.xml @@ -1047,4 +1047,8 @@ Bicicleta Compartida Toca para reservar una bicicleta Toca para reservar esta bicicleta + Se requiere permiso + Debes otorgar permiso a la aplicación para programar un recordatorio. + Otorgar permiso + Cancelar diff --git a/onebusaway-android/src/main/res/values-fi/strings.xml b/onebusaway-android/src/main/res/values-fi/strings.xml index a3a2e1820..3280a3483 100644 --- a/onebusaway-android/src/main/res/values-fi/strings.xml +++ b/onebusaway-android/src/main/res/values-fi/strings.xml @@ -631,4 +631,8 @@ (https://github.com/google/material-design-icons), " "licensed under Apache v2.0 (https://www.apache.org/licenses/LICENSE-2.0).\n\n" + Lupa tarvitaan + Sinun täytyy antaa sovellukselle lupa muistutuksen asettamiseen. + Myönnä lupa + Peruuta diff --git a/onebusaway-android/src/main/res/values-it/strings.xml b/onebusaway-android/src/main/res/values-it/strings.xml index 9035d4119..3eaeda59c 100644 --- a/onebusaway-android/src/main/res/values-it/strings.xml +++ b/onebusaway-android/src/main/res/values-it/strings.xml @@ -909,4 +909,8 @@ Rifiutare partecipazione alla ricerca? Se scegli di non partecipare alla ricerca non riceveremo più informazioni riguardo alle tue abitudini di viaggio per migliorare il servizio di trasporto pubblico. Annulla + Permesso necessario + Devi concedere all\'app il permesso di pianificare un promemoria. + Concedi il permesso + Annulla \ No newline at end of file diff --git a/onebusaway-android/src/main/res/values/strings.xml b/onebusaway-android/src/main/res/values/strings.xml index e688d0d99..183fda956 100644 --- a/onebusaway-android/src/main/res/values/strings.xml +++ b/onebusaway-android/src/main/res/values/strings.xml @@ -450,6 +450,10 @@ Show Route Show Stop - + Permission Needed + You must grant the app permission to schedule a reminder. + Grant Permission + Cancel Historically full From ee9293a4754aa32f42523f36f7a295cece4944a8 Mon Sep 17 00:00:00 2001 From: millanp Date: Mon, 19 Feb 2024 17:28:37 -0800 Subject: [PATCH 2/2] Ask for alarm and notification permissions to fix #1106 --- .../src/main/AndroidManifest.xml | 2 ++ .../android/ui/TripInfoActivity.java | 20 ++++++++++++++----- .../android/util/PermissionUtils.java | 1 + 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/onebusaway-android/src/main/AndroidManifest.xml b/onebusaway-android/src/main/AndroidManifest.xml index 594a251c2..98ace5da4 100644 --- a/onebusaway-android/src/main/AndroidManifest.xml +++ b/onebusaway-android/src/main/AndroidManifest.xml @@ -45,6 +45,8 @@ + + = Build.VERSION_CODES.TIRAMISU) { + if (!TripService.canScheduleExactAlarms(getActivity())) { + showRequestAlarmsPermissionDialog(getActivity()); + return; + } + ActivityCompat.requestPermissions(this.getActivity(), + new String[] {Manifest.permission.POST_NOTIFICATIONS}, + NOTIFICATION_PERMISSION_REQUEST); } View view = getView(); diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/util/PermissionUtils.java b/onebusaway-android/src/main/java/org/onebusaway/android/util/PermissionUtils.java index 2e4fc5182..b7a809cd7 100644 --- a/onebusaway-android/src/main/java/org/onebusaway/android/util/PermissionUtils.java +++ b/onebusaway-android/src/main/java/org/onebusaway/android/util/PermissionUtils.java @@ -30,6 +30,7 @@ public class PermissionUtils { public static final int SAVE_BACKUP_PERMISSION_REQUEST = 2; public static final int RESTORE_BACKUP_PERMISSION_REQUEST = 3; public static final int BACKGROUND_LOCATION_PERMISSION_REQUEST = 4; + public static final int NOTIFICATION_PERMISSION_REQUEST = 5; public static final String[] LOCATION_PERMISSIONS = { Manifest.permission.ACCESS_FINE_LOCATION,