Skip to content

Commit

Permalink
feat(local-notifications): Adding permissions for using SCHEDULE_EXAC…
Browse files Browse the repository at this point in the history
…T_ALARM in Android 14 (#1840)

Co-authored-by: Mike Summerfeldt <[email protected]>
Co-authored-by: jcesarmobile <[email protected]>
  • Loading branch information
3 people authored Dec 12, 2023
1 parent 4b84fbd commit 55c31e8
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 1 deletion.
53 changes: 52 additions & 1 deletion local-notifications/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ Starting on Android 12, scheduled notifications won't be exact unless this permi
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
```

Note that even if the permission is present, users can still disable exact notifications from the app settings.
Note that even if the permission is present, users can still disable exact notifications from the app settings. Use `checkExactNotificationSetting()` to check the the value of the setting. If a user disables this setting, the app will restart and any notification scheduled with an exact alarm will be deleted. If your application depends on exact alarms, be sure to check this setting on app launch (for example, in [`App.appStateChange`](https://capacitorjs.com/docs/apis/app#addlistenerappstatechange-)) in order to provide fallbacks or alternative behavior.

On Android 14, there is a new permission called `USE_EXACT_ALARM`. Use this permission to use exact alarms without needing to request permission from the user. This should only be used if the use of exact alarms is central to your app's functionality. Read more about the implications of using this permission [here](https://developer.android.com/reference/android/Manifest.permission#USE_EXACT_ALARM).

## Configuration

Expand Down Expand Up @@ -94,6 +96,8 @@ If the device has entered [Doze](https://developer.android.com/training/monitori
* [`listChannels()`](#listchannels)
* [`checkPermissions()`](#checkpermissions)
* [`requestPermissions()`](#requestpermissions)
* [`changeExactNotificationSetting()`](#changeexactnotificationsetting)
* [`checkExactNotificationSetting()`](#checkexactnotificationsetting)
* [`addListener('localNotificationReceived', ...)`](#addlistenerlocalnotificationreceived)
* [`addListener('localNotificationActionPerformed', ...)`](#addlistenerlocalnotificationactionperformed)
* [`removeAllListeners()`](#removealllisteners)
Expand Down Expand Up @@ -321,6 +325,46 @@ Request permission to display local notifications.
--------------------


### changeExactNotificationSetting()

```typescript
changeExactNotificationSetting() => Promise<SettingsPermissionStatus>
```

Direct user to the application settings screen to configure exact alarms.

In the event that a user changes the settings from granted to denied, the application
will restart and any notification scheduled with an exact alarm will be deleted.

On Android &lt; 12, the user will NOT be directed to the application settings screen, instead this function will
return `granted`.

Only available on Android.

**Returns:** <code>Promise&lt;<a href="#settingspermissionstatus">SettingsPermissionStatus</a>&gt;</code>

**Since:** 6.0.0

--------------------


### checkExactNotificationSetting()

```typescript
checkExactNotificationSetting() => Promise<SettingsPermissionStatus>
```

Check application setting for using exact alarms.

Only available on Android.

**Returns:** <code>Promise&lt;<a href="#settingspermissionstatus">SettingsPermissionStatus</a>&gt;</code>

**Since:** 6.0.0

--------------------


### addListener('localNotificationReceived', ...)

```typescript
Expand Down Expand Up @@ -652,6 +696,13 @@ An action that can be taken when a notification is displayed.
| **`display`** | <code><a href="#permissionstate">PermissionState</a></code> | Permission state of displaying notifications. | 1.0.0 |


#### SettingsPermissionStatus

| Prop | Type | Description | Since |
| ----------------- | ----------------------------------------------------------- | --------------------------------------- | ----- |
| **`exact_alarm`** | <code><a href="#permissionstate">PermissionState</a></code> | Permission state of using exact alarms. | 6.0.0 |


#### PluginListenerHandle

| Prop | Type |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,10 @@ private void setExactIfPossible(
PendingIntent pendingIntent
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !alarmManager.canScheduleExactAlarms()) {
Logger.warn(
"Capacitor/LocalNotification",
"Exact alarms not allowed in user settings. Notification scheduled with non-exact alarm."
);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && schedule.allowWhileIdle()) {
alarmManager.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, trigger, pendingIntent);
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package com.capacitorjs.plugins.localnotifications;

import static android.provider.Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM;

import android.Manifest;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.service.notification.StatusBarNotification;
import androidx.activity.result.ActivityResult;
import com.getcapacitor.Bridge;
import com.getcapacitor.JSArray;
import com.getcapacitor.JSObject;
Expand All @@ -15,6 +20,7 @@
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginHandle;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.ActivityCallback;
import com.getcapacitor.annotation.CapacitorPlugin;
import com.getcapacitor.annotation.Permission;
import com.getcapacitor.annotation.PermissionCallback;
Expand Down Expand Up @@ -221,13 +227,40 @@ public void requestPermissions(PluginCall call) {
}
}

@PluginMethod
public void changeExactNotificationSetting(PluginCall call) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
startActivityForResult(
call,
new Intent(ACTION_REQUEST_SCHEDULE_EXACT_ALARM, Uri.parse("package:" + getActivity().getPackageName())),
"alarmPermissionsCallback"
);
} else {
checkExactNotificationSetting(call);
}
}

@PluginMethod
public void checkExactNotificationSetting(PluginCall call) {
JSObject permissionsResultJSON = new JSObject();
permissionsResultJSON.put("exact_alarm", getExactAlarmPermissionText());

call.resolve(permissionsResultJSON);
}

@PermissionCallback
private void permissionsCallback(PluginCall call) {
JSObject permissionsResultJSON = new JSObject();
permissionsResultJSON.put("display", getNotificationPermissionText());

call.resolve(permissionsResultJSON);
}

@ActivityCallback
private void alarmPermissionsCallback(PluginCall call, ActivityResult result) {
checkExactNotificationSetting(call);
}

private String getNotificationPermissionText() {
if (manager.areNotificationsEnabled()) {
return "granted";
Expand All @@ -236,6 +269,19 @@ private String getNotificationPermissionText() {
}
}

private String getExactAlarmPermissionText() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
AlarmManager alarmManager = (AlarmManager) getActivity().getSystemService(Context.ALARM_SERVICE);
if (alarmManager.canScheduleExactAlarms()) {
return "granted";
} else {
return "denied";
}
}

return "granted";
}

public static void fireReceived(JSObject notification) {
LocalNotificationsPlugin localNotificationsPlugin = LocalNotificationsPlugin.getLocalNotificationsInstance();
if (localNotificationsPlugin != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,11 @@ private Notification getParcelableExtraLegacy(Intent intent, String string) {

private boolean rescheduleNotificationIfNeeded(Context context, Intent intent, int id) {
String dateString = intent.getStringExtra(CRON_KEY);

if (dateString != null) {
DateMatch date = DateMatch.fromMatchString(dateString);
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);

long trigger = date.nextTrigger(new Date());
Intent clone = (Intent) intent.clone();
int flags = PendingIntent.FLAG_CANCEL_CURRENT;
Expand All @@ -70,6 +72,10 @@ private boolean rescheduleNotificationIfNeeded(Context context, Intent intent, i
}
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, id, clone, flags);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !alarmManager.canScheduleExactAlarms()) {
Logger.warn(
"Capacitor/LocalNotification",
"Exact alarms not allowed in user settings. Notification scheduled with non-exact alarm."
);
alarmManager.set(AlarmManager.RTC, trigger, pendingIntent);
} else {
alarmManager.setExact(AlarmManager.RTC, trigger, pendingIntent);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ public class LocalNotificationsPlugin: CAPPlugin, CAPBridgedPlugin {
CAPPluginMethod(name: "schedule", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "requestPermissions", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "checkPermissions", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "checkExactNotificationSetting", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "changeExactNotificationSetting", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "cancel", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getPending", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "registerActionTypes", returnType: CAPPluginReturnPromise),
Expand Down Expand Up @@ -147,6 +149,14 @@ public class LocalNotificationsPlugin: CAPPlugin, CAPBridgedPlugin {
}
}

@objc public func checkExactNotificationSetting(_ call: CAPPluginCall) {
call.unimplemented()
}

@objc public func changeExactNotificationSetting(_ call: CAPPluginCall) {
call.unimplemented()
}

/**
* Cancel notifications by id
*/
Expand Down
33 changes: 33 additions & 0 deletions local-notifications/src/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,30 @@ export interface LocalNotificationsPlugin {
*/
requestPermissions(): Promise<PermissionStatus>;

/**
* Direct user to the application settings screen to configure exact alarms.
*
* In the event that a user changes the settings from granted to denied, the application
* will restart and any notification scheduled with an exact alarm will be deleted.
*
* On Android < 12, the user will NOT be directed to the application settings screen, instead this function will
* return `granted`.
*
* Only available on Android.
*
* @since 6.0.0
*/
changeExactNotificationSetting(): Promise<SettingsPermissionStatus>;

/**
* Check application setting for using exact alarms.
*
* Only available on Android.
*
* @since 6.0.0
*/
checkExactNotificationSetting(): Promise<SettingsPermissionStatus>;

/**
* Listen for when notifications are displayed.
*
Expand Down Expand Up @@ -844,6 +868,15 @@ export interface PermissionStatus {
display: PermissionState;
}

export interface SettingsPermissionStatus {
/**
* Permission state of using exact alarms.
*
* @since 6.0.0
*/
exact_alarm: PermissionState;
}

export interface ActionPerformed {
/**
* The identifier of the performed action.
Expand Down
9 changes: 9 additions & 0 deletions local-notifications/src/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
PermissionStatus,
ScheduleOptions,
ScheduleResult,
SettingsPermissionStatus,
} from './definitions';

export class LocalNotificationsWeb
Expand Down Expand Up @@ -106,6 +107,14 @@ export class LocalNotificationsWeb
};
}

async changeExactNotificationSetting(): Promise<SettingsPermissionStatus> {
throw this.unimplemented('Not implemented on web.');
}

async checkExactNotificationSetting(): Promise<SettingsPermissionStatus> {
throw this.unimplemented('Not implemented on web.');
}

async requestPermissions(): Promise<PermissionStatus> {
if (!this.hasNotificationSupport()) {
throw this.unavailable('Notifications not supported in this browser.');
Expand Down

0 comments on commit 55c31e8

Please sign in to comment.