Skip to content

Commit

Permalink
feat(android): Expose api to handle Android 12 REQUIRE_EXACT_ALARM pe…
Browse files Browse the repository at this point in the history
…rmission (#332)
  • Loading branch information
vincent-paing authored Apr 6, 2022
1 parent 421faff commit f9dbf21
Show file tree
Hide file tree
Showing 16 changed files with 325 additions and 151 deletions.
9 changes: 8 additions & 1 deletion android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
android:name=".ForegroundService"
android:exported="false" />

<receiver
<receiver
android:name=".RebootBroadcastReceiver"
android:exported="false">
<intent-filter>
Expand All @@ -40,6 +40,13 @@
</intent-filter>
</receiver>

<receiver
android:name=".AlarmPermissionBroadcastReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.app.action.SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED" />
</intent-filter>
</receiver>

<receiver
android:name=".NotificationAlarmReceiver"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package app.notifee.core;

import static android.app.AlarmManager.ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

public class AlarmPermissionBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {

if (intent.getAction().equals(ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED)) {
Log.i("AlarmPermissionReceiver", "Received alarm permission state changed event");
new NotifeeAlarmManager().rescheduleNotifications();
}
}
}
18 changes: 18 additions & 0 deletions android/src/main/java/app/notifee/core/Notifee.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import app.notifee.core.model.ChannelGroupModel;
import app.notifee.core.model.ChannelModel;
import app.notifee.core.model.NotificationModel;
import app.notifee.core.utility.AlarmUtils;
import app.notifee.core.utility.PowerManagerUtils;
import java.util.ArrayList;
import java.util.List;
Expand Down Expand Up @@ -119,6 +120,12 @@ public void cancelAllNotificationsWithIds(
});
}

@KeepForSdk
public void openAlarmPermissionSettings(Activity activity, MethodCallResult<Void> result) {
AlarmUtils.openAlarmPermissionSettings(activity);
result.onComplete(null, null);
}

@KeepForSdk
public void createChannel(Bundle channelMap, MethodCallResult<Void> result) {
ChannelModel channelModel = ChannelModel.fromBundle(channelMap);
Expand Down Expand Up @@ -392,6 +399,17 @@ public void getNotificationSettings(MethodCallResult<Bundle> result) {
} else {
notificationSettingsBundle.putInt("authorizationStatus", 0);
}

boolean canScheduleExactAlarms = AlarmUtils.canScheduleExactAlarms();
Bundle androidSettingsBundle = new Bundle();

if (canScheduleExactAlarms) {
androidSettingsBundle.putInt("alarm", 1);
} else {
androidSettingsBundle.putInt("alarm", 0);
}

notificationSettingsBundle.putBundle("android", androidSettingsBundle);
result.onComplete(null, notificationSettingsBundle);
}

Expand Down
12 changes: 5 additions & 7 deletions android/src/main/java/app/notifee/core/NotifeeAlarmManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import app.notifee.core.database.WorkDataRepository;
import app.notifee.core.model.NotificationModel;
import app.notifee.core.model.TimestampTriggerModel;
import app.notifee.core.utility.AlarmUtils;
import app.notifee.core.utility.ObjectUtils;
import com.google.android.gms.tasks.Continuation;
import com.google.android.gms.tasks.Task;
Expand Down Expand Up @@ -118,10 +119,6 @@ static void displayScheduledNotification(Bundle alarmManagerNotification) {
});
}

private static AlarmManager getAlarmManager() {
return (AlarmManager) getApplicationContext().getSystemService(Context.ALARM_SERVICE);
}

public static PendingIntent getAlarmManagerIntentForNotification(String notificationId) {
try {
Context context = getApplicationContext();
Expand All @@ -145,13 +142,14 @@ static void scheduleTimestampTriggerNotification(

PendingIntent pendingIntent = getAlarmManagerIntentForNotification(notificationModel.getId());

AlarmManager alarmManager = getAlarmManager();
AlarmManager alarmManager = AlarmUtils.getAlarmManager();

// Verify we can call setExact APIs to avoid a crash, but it requires an Android S+ symbol
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (!alarmManager.canScheduleExactAlarms()) {
System.err.println(
"Missing SCHEDULE_EXACT_ALARM permission. Trigger not scheduled. Issue #239");
"Missing SCHEDULE_EXACT_ALARM permission. Trigger not scheduled. See:"
+ " https://notifee.app/react-native/docs/triggers#android-12-limitations");
return;
}
}
Expand All @@ -172,7 +170,7 @@ Task<List<WorkDataEntity>> getScheduledNotifications() {

public static void cancelNotification(String notificationId) {
PendingIntent pendingIntent = getAlarmManagerIntentForNotification(notificationId);
AlarmManager alarmManager = getAlarmManager();
AlarmManager alarmManager = AlarmUtils.getAlarmManager();
if (pendingIntent != null) {
alarmManager.cancel(pendingIntent);
}
Expand Down
52 changes: 52 additions & 0 deletions android/src/main/java/app/notifee/core/utility/AlarmUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package app.notifee.core.utility;

import static app.notifee.core.ContextHolder.getApplicationContext;

import android.app.Activity;
import android.app.AlarmManager;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import app.notifee.core.ContextHolder;
import app.notifee.core.Logger;

public class AlarmUtils {
private static final String TAG = "AlarmUtils";

public static AlarmManager getAlarmManager() {
return (AlarmManager) getApplicationContext().getSystemService(Context.ALARM_SERVICE);
}

/**
* Attempts to open the device's alarm special access settings
*
* @param activity
*/
public static void openAlarmPermissionSettings(Activity activity) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
return;
}

try {
Intent intent = new Intent();
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM);
Context context = ContextHolder.getApplicationContext();
String packageName = context.getPackageName();
intent.setData(Uri.parse("package:" + packageName));

IntentUtils.startActivityOnUiThread(activity, intent);
} catch (Exception e) {
Logger.e(TAG, "An error occurred whilst trying to open alarm permission settings", e);
}
}

public static boolean canScheduleExactAlarms() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
return getAlarmManager().canScheduleExactAlarms();
}
return true;
}
}
1 change: 1 addition & 0 deletions docs/navigator.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
["/react-native/docs/android/foreground-service", "Foreground Service"],
["/react-native/docs/android/grouping-and-sorting", "Grouping & Sorting"],
["/react-native/docs/android/interaction", "Interaction"],
["/react-native/docs/android/permissions", "Permissions"],
["/react-native/docs/android/progress-indicators", "Progress Indicators"],
["/react-native/docs/android/styles", "Styles"],
["/react-native/docs/android/timers", "Timers"],
Expand Down
40 changes: 29 additions & 11 deletions docs/react-native/docs/triggers.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,15 @@ Let's update our trigger we created previously to occur weekly.
import notifee, { TimestampTrigger, TriggerType } from '@notifee/react-native';

async function onCreateTriggerNotification() {
const date = new Date(Date.now());
date.setHours(11);
date.setMinutes(10);
const date = new Date(Date.now());
date.setHours(11);
date.setMinutes(10);

const trigger: TimestampTrigger = {
type: TriggerType.TIMESTAMP,
timestamp: date.getTime(),
repeatFrequency: RepeatFrequency.WEEKLY
};
const trigger: TimestampTrigger = {
type: TriggerType.TIMESTAMP,
timestamp: date.getTime(),
repeatFrequency: RepeatFrequency.WEEKLY,
};

await notifee.createTriggerNotification(
{
Expand Down Expand Up @@ -132,7 +132,7 @@ On Android, you have the option to create your trigger notification with Android
```js
const trigger: TimestampTrigger = {
//...
alarmManager: true
alarmManager: true,
};
```

Expand All @@ -143,16 +143,34 @@ const trigger: TimestampTrigger = {
//...
alarmManager: {
allowWhileIdle: true,
}
},
};
```

Please note, for iOS, a repeating trigger does not work the same as Android - the initial trigger cannot be delayed:

- `HOURLY`: the starting date and hour will be ignored, and only the minutes and seconds will be taken into the account. If the timestamp is set to trigger in 3 hours and repeat every 5th minute of the hour, the alert will not fire in 3 hours, but will instead fire immediately on the next 5th minute of the hour.
- `DAILY`: the starting day will be ignored, and only the time will be taken into account. If it is January 1 at 10 AM and you schedule a daily recurring notification for January 2 at 11 AM, it will fire on January 1 at 11 AM and every day thereafter.
- `WEEKLY`: the starting week will be ignored, and only the day and time will be taken into account.

> For more details, please see the discussion [here](https://github.com/notifee/react-native-notifee/issues/241).
> For more details, please see the discussion [here](https://github.com/notifee/react-native-notifee/issues/241).
### Android 12 Limitations

Starting from Android 12, timestamp triggers cannot be created unless user specfically allow the [exact alarm permission](https://developer.android.com/reference/android/Manifest.permission#SCHEDULE_EXACT_ALARM). Before you create a timestamp trigger, check whether `SCHEDULE_EXACT_ALARM` permission is allowed by making a call to `getNotificationSettings`. If ` alarm` is `DISABLED`, you should educate the user on this permission and ask to enable scheduling alarms. You can then use `openAlarmPermissionSettings` function to display the Alarms & Reminder settings of your app.

```js
const settings = Notifee.getNotificationSettings();
if (settings.android.alarm == AndroidNotificationSetting.ENABLED) {
//Create timestamp trigger
} else {
// Show some user information to educate them on what exact alarm permission is,
// and why it is necessary for your app functionality, then send them to system preferences:
await Notifee.openAlarmPermissionSettings();
}
```

Please note that if the user revokes the permission via system preferences, all of the timestamp triggers will be deleted by the system. However, if you check for the permission, notice it is missing, educate the user and they grant permission again, notifee will automatically reschedule the triggers when the user allows the alarm permission again with no need for additional code.

## Interval Trigger

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,13 @@ public void displayNotification(ReadableMap notificationMap, Promise promise) {
(e, aVoid) -> NotifeeReactUtils.promiseResolver(promise, e));
}

@ReactMethod
public void openAlarmPermissionSettings(Promise promise) {
Notifee.getInstance()
.openAlarmPermissionSettings(
getCurrentActivity(), (e, avoid) -> NotifeeReactUtils.promiseResolver(promise, e));
}

@ReactMethod
public void createTriggerNotification(
ReadableMap notificationMap, ReadableMap triggerMap, Promise promise) {
Expand Down
Loading

0 comments on commit f9dbf21

Please sign in to comment.