Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion push-notifications/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ const getDeliveredNotifications = async () => {
* [`createChannel(...)`](#createchannel)
* [`deleteChannel(...)`](#deletechannel)
* [`listChannels()`](#listchannels)
* [`checkAppEnabled()`](#checkappenabled)
* [`checkChannelEnabled(...)`](#checkchannelenabled)
* [`checkPermissions()`](#checkpermissions)
* [`requestPermissions()`](#requestpermissions)
* [`addListener('registration', ...)`](#addlistenerregistration-)
Expand Down Expand Up @@ -342,6 +344,50 @@ Only available on Android O or newer (SDK 26+).
--------------------


### checkAppEnabled()

```typescript
checkAppEnabled() => Promise<EnabledResult>
```

Check whether notifications are enabled for the app in the system settings.

On iOS, authorized, provisional, and ephemeral authorization are considered enabled.
This does not request permission or check push registration, individual channels,
or temporary restrictions such as Do Not Disturb.

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

**Since:** 8.2.0

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


### checkChannelEnabled(...)

```typescript
checkChannelEnabled(args: { id: string; }) => Promise<EnabledResult>
```

Check whether a notification channel exists and its importance is not `0` (disabled).

Returns `false` if the channel does not exist. This does not check app-wide
notification settings or channel group settings. Use `checkAppEnabled()` to
check the app-wide setting separately.

Only available on Android O or newer (SDK 26+).

| Param | Type |
| ---------- | ---------------------------- |
| **`args`** | <code>{ id: string; }</code> |

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

**Since:** 8.2.0

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


### checkPermissions()

```typescript
Expand All @@ -352,7 +398,7 @@ Check permission to receive push notifications.

On Android 12 and below the status is always granted because you can always
receive push notifications. If you need to check if the user allows
to display notifications, use local-notifications plugin.
to display notifications, use `checkAppEnabled()`.

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

Expand Down Expand Up @@ -518,6 +564,7 @@ Remove all native listeners for this plugin.
| **`description`** | <code>string</code> | The description of this channel (presented to the user). | | 1.0.0 |
| **`sound`** | <code>string</code> | The sound that should be played for notifications posted to this channel. Notification channels with an importance of at least `3` should have a sound. The file name of a sound file should be specified relative to the android app `res/raw` directory. | | 1.0.0 |
| **`importance`** | <code><a href="#importance">Importance</a></code> | The level of interruption for notifications posted to this channel. | <code>`3`</code> | 1.0.0 |
| **`enabled`** | <code>boolean</code> | Whether the channel's importance is not `0` (disabled). Returned by `listChannels()` and ignored by `createChannel()`. This does not include app-wide notification settings or channel group settings. Only available on Android O or newer (SDK 26+). | | 8.2.0 |
| **`visibility`** | <code><a href="#visibility">Visibility</a></code> | The visibility of notifications posted to this channel. This setting is for whether notifications posted to this channel appear on the lockscreen or not, and if so, whether they appear in a redacted form. | | 1.0.0 |
| **`lights`** | <code>boolean</code> | Whether notifications posted to this channel should display notification lights, on devices that support it. | | 1.0.0 |
| **`lightColor`** | <code>string</code> | The light color for notifications posted to this channel. Only supported if lights are enabled on this channel and the device supports it. Supported color formats are `#RRGGBB` and `#RRGGBBAA`. | | 1.0.0 |
Expand All @@ -531,6 +578,13 @@ Remove all native listeners for this plugin.
| **`channels`** | <code>Channel[]</code> | List of all the Channels created by your app. | 1.0.0 |


#### EnabledResult

| Prop | Type | Description | Since |
| ----------- | -------------------- | ----------------------------------------------------------------------- | ----- |
| **`value`** | <code>boolean</code> | Whether notifications are enabled for the app or the specified channel. | 8.2.0 |


#### PermissionStatus

| Prop | Type | Description | Since |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package com.capacitorjs.plugins.pushnotifications;

import static org.junit.Assert.*;

import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.os.Build;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
import com.getcapacitor.JSObject;
import com.getcapacitor.PluginCall;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(AndroidJUnit4.class)
public class NotificationSettingsTest {

private Context context;
private NotificationManager notificationManager;
private NotificationChannelManager channelManager;
private final List<String> channelIds = new ArrayList<>();

@Before
public void setUp() {
context = InstrumentationRegistry.getInstrumentation().getTargetContext();
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
channelManager = new NotificationChannelManager(context, notificationManager, null);
}

@After
public void tearDown() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
for (String id : channelIds) {
notificationManager.deleteNotificationChannel(id);
}
}
}

@Test
public void checkAppEnabledUsesSystemSettingsWithoutRegistration() throws Exception {
PushNotificationsPlugin plugin = new PushNotificationsPlugin() {
@Override
public Context getContext() {
return context;
}
};
RecordingCall call = new RecordingCall("checkAppEnabled", new JSObject());

plugin.checkAppEnabled(call);

assertNotNull(call.result);
assertEquals(notificationManager.areNotificationsEnabled(), call.result.getBoolean("value"));
}

@Test
@SdkSuppress(minSdkVersion = 26)
public void channelChecksAndListingAgreeForEveryImportance() throws Exception {
int[] importanceLevels = {
NotificationManager.IMPORTANCE_NONE,
NotificationManager.IMPORTANCE_MIN,
NotificationManager.IMPORTANCE_LOW,
NotificationManager.IMPORTANCE_DEFAULT,
NotificationManager.IMPORTANCE_HIGH
};
for (int importance : importanceLevels) {
String id = UUID.randomUUID().toString();
channelIds.add(id);
notificationManager.createNotificationChannel(new NotificationChannel(id, "Test channel", importance));
RecordingCall check = new RecordingCall("checkChannelEnabled", new JSObject().put("id", id));
channelManager.checkChannelEnabled(check);
assertNotNull(check.result);
assertEquals(importance != NotificationManager.IMPORTANCE_NONE, check.result.getBoolean("value"));
}

RecordingCall list = new RecordingCall("listChannels", new JSObject());
channelManager.listChannels(list);
assertNotNull(list.result);
JSONArray channels = list.result.getJSONArray("channels");
int found = 0;
for (int index = 0; index < channels.length(); index++) {
JSONObject channel = channels.getJSONObject(index);
if (channelIds.contains(channel.getString("id"))) {
assertEquals(channel.getInt("importance") != NotificationManager.IMPORTANCE_NONE, channel.getBoolean("enabled"));
found++;
}
}
assertEquals(importanceLevels.length, found);
}

@Test
@SdkSuppress(minSdkVersion = 26)
public void missingChannelIsDisabled() throws Exception {
RecordingCall call = new RecordingCall("checkChannelEnabled", new JSObject().put("id", UUID.randomUUID().toString()));
channelManager.checkChannelEnabled(call);
assertNotNull(call.result);
assertFalse(call.result.getBoolean("value"));
}

@Test
@SdkSuppress(minSdkVersion = 26)
public void missingOrEmptyIdentifierIsRejected() {
for (JSObject options : new JSObject[] { new JSObject(), new JSObject().put("id", "") }) {
RecordingCall call = new RecordingCall("checkChannelEnabled", options);
channelManager.checkChannelEnabled(call);
assertNull(call.result);
assertEquals("Channel missing identifier", call.errorMessage);
}
}

@Test
@SdkSuppress(maxSdkVersion = 25)
public void channelChecksAreUnavailableBeforeAndroidO() {
RecordingCall call = new RecordingCall("checkChannelEnabled", new JSObject().put("id", "general"));
channelManager.checkChannelEnabled(call);
assertNull(call.result);
assertEquals("UNAVAILABLE", call.errorCode);
}

private static class RecordingCall extends PluginCall {

JSObject result;
String errorMessage;
String errorCode;

RecordingCall(String method, JSObject options) {
super(null, "PushNotifications", "test", method, options);
}

@Override
public void resolve(JSObject data) {
result = data;
}

@Override
public void reject(String message, String code, Exception exception, JSObject data) {
errorMessage = message;
errorCode = code;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public NotificationChannelManager(Context context, NotificationManager manager,
private static String CHANNEL_NAME = "name";
private static String CHANNEL_DESCRIPTION = "description";
private static String CHANNEL_IMPORTANCE = "importance";
private static String CHANNEL_ENABLED = "enabled";
private static String CHANNEL_VISIBILITY = "visibility";
private static String CHANNEL_SOUND = "sound";
private static String CHANNEL_VIBRATE = "vibration";
Expand Down Expand Up @@ -111,6 +112,22 @@ public void deleteChannel(PluginCall call) {
}
}

public void checkChannelEnabled(PluginCall call) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
String channelId = call.getString(CHANNEL_ID);
if (channelId == null || channelId.isEmpty()) {
call.reject("Channel missing identifier");
return;
}
NotificationChannel channel = notificationManager.getNotificationChannel(channelId);
JSObject result = new JSObject();
result.put("value", channel != null && channel.getImportance() != NotificationManager.IMPORTANCE_NONE);
call.resolve(result);
} else {
call.unavailable();
}
}

public void listChannels(PluginCall call) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
List<NotificationChannel> notificationChannels = notificationManager.getNotificationChannels();
Expand All @@ -121,6 +138,7 @@ public void listChannels(PluginCall call) {
channel.put(CHANNEL_NAME, notificationChannel.getName());
channel.put(CHANNEL_DESCRIPTION, notificationChannel.getDescription());
channel.put(CHANNEL_IMPORTANCE, notificationChannel.getImportance());
channel.put(CHANNEL_ENABLED, notificationChannel.getImportance() != NotificationManager.IMPORTANCE_NONE);
channel.put(CHANNEL_VISIBILITY, notificationChannel.getLockscreenVisibility());
channel.put(CHANNEL_SOUND, notificationChannel.getSound());
channel.put(CHANNEL_VIBRATE, notificationChannel.shouldVibrate());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import android.os.Looper;
import android.service.notification.StatusBarNotification;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import com.getcapacitor.*;
import com.getcapacitor.annotation.CapacitorPlugin;
import com.getcapacitor.annotation.Permission;
Expand Down Expand Up @@ -87,6 +88,13 @@ protected void handleOnNewIntent(Intent data) {
}
}

@PluginMethod
public void checkAppEnabled(PluginCall call) {
JSObject result = new JSObject();
result.put("value", NotificationManagerCompat.from(getContext()).areNotificationsEnabled());
call.resolve(result);
}

@PluginMethod
public void checkPermissions(PluginCall call) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
Expand Down Expand Up @@ -214,6 +222,11 @@ public void listChannels(PluginCall call) {
notificationChannelManager.listChannels(call);
}

@PluginMethod
public void checkChannelEnabled(PluginCall call) {
notificationChannelManager.checkChannelEnabled(call);
}

public void sendToken(String token) {
JSObject data = new JSObject();
data.put("value", token);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ public class PushNotificationsHandler: NSObject, NotificationHandlerProtocol {
}
}

public func checkAppEnabled(with completion: ((Bool) -> Void)? = nil) {
checkPermissions { status in
switch status {
case .authorized, .provisional, .ephemeral:
completion?(true)
case .notDetermined, .denied:
completion?(false)
@unknown default:
completion?(false)
}
}
}

public func willPresent(notification: UNNotification) -> UNNotificationPresentationOptions {
let notificationData = makeNotificationRequestJSObject(notification.request)
self.plugin?.notifyListeners("pushNotificationReceived", data: notificationData)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public class PushNotificationsPlugin: CAPPlugin, CAPBridgedPlugin {
public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "register", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "unregister", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "checkAppEnabled", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "checkChannelEnabled", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "checkPermissions", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "requestPermissions", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getDeliveredNotifications", returnType: CAPPluginReturnPromise),
Expand Down Expand Up @@ -96,6 +98,12 @@ public class PushNotificationsPlugin: CAPPlugin, CAPBridgedPlugin {
}
}

@objc func checkAppEnabled(_ call: CAPPluginCall) {
self.notificationDelegateHandler.checkAppEnabled { enabled in
call.resolve(["value": enabled])
}
}

/**
* Check notification permission
*/
Expand Down Expand Up @@ -181,6 +189,10 @@ public class PushNotificationsPlugin: CAPPlugin, CAPBridgedPlugin {
call.unimplemented("Not available on iOS")
}

@objc func checkChannelEnabled(_ call: CAPPluginCall) {
call.unimplemented("Not available on iOS")
}

@objc public func didRegisterForRemoteNotificationsWithDeviceToken(notification: NSNotification) {
appDelegateRegistrationCalled = true
if let deviceToken = notification.object as? Data {
Expand Down
Loading