expo-notifications 1.0.0-canary-20240927-ab8a962 → 1.0.0-canary-20241008-90b13ad
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/android/src/main/java/expo/modules/notifications/notifications/NotificationManager.java +8 -0
- package/android/src/main/java/expo/modules/notifications/notifications/NotificationSerializer.java +7 -1
- package/android/src/main/java/expo/modules/notifications/notifications/background/BackgroundRemoteNotificationTaskConsumer.java +2 -1
- package/android/src/main/java/expo/modules/notifications/notifications/debug/DebugLogging.kt +1 -1
- package/android/src/main/java/expo/modules/notifications/notifications/handling/NotificationsHandler.kt +3 -1
- package/android/src/main/java/expo/modules/notifications/notifications/handling/SingleNotificationHandlerTask.java +2 -4
- package/android/src/main/java/expo/modules/notifications/notifications/interfaces/INotificationContent.kt +2 -2
- package/android/src/main/java/expo/modules/notifications/notifications/interfaces/NotificationBuilder.kt +1 -9
- package/android/src/main/java/expo/modules/notifications/notifications/interfaces/NotificationListener.java +1 -1
- package/android/src/main/java/expo/modules/notifications/notifications/model/NotificationAction.java +3 -0
- package/android/src/main/java/expo/modules/notifications/notifications/model/NotificationCategory.java +2 -0
- package/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java +1 -1
- package/android/src/main/java/expo/modules/notifications/notifications/model/RemoteNotificationContent.kt +1 -1
- package/android/src/main/java/expo/modules/notifications/notifications/presentation/ExpoNotificationPresentationModule.kt +16 -2
- package/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/BaseNotificationBuilder.kt +148 -0
- package/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.kt +98 -29
- package/android/src/main/java/expo/modules/notifications/notifications/scheduling/NotificationScheduler.kt +18 -0
- package/android/src/main/java/expo/modules/notifications/notifications/triggers/MonthlyTrigger.java +85 -0
- package/android/src/main/java/expo/modules/notifications/service/NotificationsService.kt +11 -6
- package/android/src/main/java/expo/modules/notifications/service/delegates/ExpoHandlingDelegate.kt +11 -0
- package/android/src/main/java/expo/modules/notifications/service/delegates/ExpoPresentationDelegate.kt +6 -5
- package/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt +13 -2
- package/build/NotificationScheduler.types.d.ts +8 -1
- package/build/NotificationScheduler.types.d.ts.map +1 -1
- package/build/NotificationScheduler.types.js.map +1 -1
- package/build/Notifications.types.d.ts +35 -2
- package/build/Notifications.types.d.ts.map +1 -1
- package/build/Notifications.types.js +1 -0
- package/build/Notifications.types.js.map +1 -1
- package/build/NotificationsEmitter.d.ts.map +1 -1
- package/build/NotificationsEmitter.js +8 -3
- package/build/NotificationsEmitter.js.map +1 -1
- package/build/scheduleNotificationAsync.d.ts.map +1 -1
- package/build/scheduleNotificationAsync.js +25 -1
- package/build/scheduleNotificationAsync.js.map +1 -1
- package/ios/EXNotifications/Notifications/Emitter/EXNotificationsEmitter.h +1 -0
- package/ios/EXNotifications/Notifications/Emitter/EXNotificationsEmitter.m +1 -1
- package/ios/EXNotifications/Notifications/Scheduling/EXNotificationSchedulerModule.m +16 -0
- package/package.json +7 -7
- package/plugin/build/withNotificationsIOS.js +3 -1
- package/plugin/src/withNotificationsIOS.ts +3 -1
- package/src/NotificationScheduler.types.ts +9 -0
- package/src/Notifications.types.ts +30 -2
- package/src/NotificationsEmitter.ts +9 -3
- package/src/scheduleNotificationAsync.ts +32 -1
- package/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/BaseNotificationBuilder.java +0 -50
- package/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/CategoryAwareNotificationBuilder.java +0 -80
- package/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ChannelAwareNotificationBuilder.java +0 -134
package/CHANGELOG.md
CHANGED
|
@@ -10,13 +10,18 @@
|
|
|
10
10
|
### 🎉 New features
|
|
11
11
|
|
|
12
12
|
- Add clearLastNotificationResponseAsync to API. ([#31607](https://github.com/expo/expo/pull/31607) by [@douglowder](https://github.com/douglowder))
|
|
13
|
+
- New monthly trigger type for scheduled notifications. ([#31823](https://github.com/expo/expo/pull/31823) by [@douglowder](https://github.com/douglowder))
|
|
13
14
|
|
|
14
15
|
### 🐛 Bug fixes
|
|
15
16
|
|
|
17
|
+
- throw improved error on invalid subscription in removeNotificationSubscription ([#31842](https://github.com/expo/expo/pull/31842) by [@vonovak](https://github.com/vonovak))
|
|
18
|
+
- [android] fix notifications actions not being presented ([#31795](https://github.com/expo/expo/pull/31795) by [@vonovak](https://github.com/vonovak))
|
|
16
19
|
- Add missing `react` and `react-native` peer dependencies for isolated modules. ([#30478](https://github.com/expo/expo/pull/30478) by [@byCedric](https://github.com/byCedric))
|
|
20
|
+
- [iOS] do not overwrite existing aps entitlement. ([#31892](https://github.com/expo/expo/pull/31892) by [@douglowder](https://github.com/douglowder))
|
|
17
21
|
|
|
18
22
|
### 💡 Others
|
|
19
23
|
|
|
24
|
+
- [android] refactor ExpoNotificationBuilder ([#31838](https://github.com/expo/expo/pull/31838) by [@vonovak](https://github.com/vonovak))
|
|
20
25
|
- Warn about limited support in Expo Go ([#31573](https://github.com/expo/expo/pull/31573) by [@vonovak](https://github.com/vonovak))
|
|
21
26
|
- Keep using the legacy event emitter as the module is not fully migrated to Expo Modules API. ([#28946](https://github.com/expo/expo/pull/28946) by [@tsapeta](https://github.com/tsapeta))
|
|
22
27
|
|
package/android/src/main/java/expo/modules/notifications/notifications/NotificationManager.java
CHANGED
|
@@ -30,6 +30,10 @@ public class NotificationManager implements SingletonModule, expo.modules.notifi
|
|
|
30
30
|
public NotificationManager() {
|
|
31
31
|
mListenerReferenceMap = new WeakHashMap<>();
|
|
32
32
|
|
|
33
|
+
// TODO @vonovak there's a chain of listeners:
|
|
34
|
+
// ExpoHandlingDelegate -> NotificationManager -> NotificationsEmitter
|
|
35
|
+
// -> NotificationsHandler
|
|
36
|
+
// it seems it could be shorter?
|
|
33
37
|
// Registers this singleton instance in static ExpoHandlingDelegate listeners collection.
|
|
34
38
|
// Since it doesn't hold strong reference to the object this should be safe.
|
|
35
39
|
ExpoHandlingDelegate.Companion.addListener(this);
|
|
@@ -81,6 +85,10 @@ public class NotificationManager implements SingletonModule, expo.modules.notifi
|
|
|
81
85
|
* Calls {@link NotificationListener#onNotificationReceived(Notification)} on all values
|
|
82
86
|
* of {@link NotificationManager#mListenerReferenceMap}.
|
|
83
87
|
*
|
|
88
|
+
* In practice, that means calling {@link NotificationsEmitter} (just emits an event to JS) and
|
|
89
|
+
* {@link NotificationsHandler} which calls `handleNotification` in JS to determine the behavior.
|
|
90
|
+
* Then `SingleNotificationHandlerTask.processNotificationWithBehavior` may present it.
|
|
91
|
+
*
|
|
84
92
|
* @param notification Notification received
|
|
85
93
|
*/
|
|
86
94
|
public void onNotificationReceived(Notification notification) {
|
package/android/src/main/java/expo/modules/notifications/notifications/NotificationSerializer.java
CHANGED
|
@@ -32,6 +32,7 @@ import expo.modules.notifications.notifications.model.triggers.FirebaseNotificat
|
|
|
32
32
|
|
|
33
33
|
import expo.modules.notifications.notifications.triggers.DailyTrigger;
|
|
34
34
|
import expo.modules.notifications.notifications.triggers.DateTrigger;
|
|
35
|
+
import expo.modules.notifications.notifications.triggers.MonthlyTrigger;
|
|
35
36
|
import expo.modules.notifications.notifications.triggers.TimeIntervalTrigger;
|
|
36
37
|
import expo.modules.notifications.notifications.triggers.WeeklyTrigger;
|
|
37
38
|
import expo.modules.notifications.notifications.triggers.YearlyTrigger;
|
|
@@ -101,7 +102,7 @@ public class NotificationSerializer {
|
|
|
101
102
|
public static Bundle toBundle(INotificationContent content) {
|
|
102
103
|
Bundle serializedContent = new Bundle();
|
|
103
104
|
serializedContent.putString("title", content.getTitle());
|
|
104
|
-
serializedContent.putString("subtitle", content.
|
|
105
|
+
serializedContent.putString("subtitle", content.getSubText());
|
|
105
106
|
serializedContent.putString("body", content.getText());
|
|
106
107
|
if (content.getColor() != null) {
|
|
107
108
|
serializedContent.putString("color", String.format("#%08X", content.getColor().intValue()));
|
|
@@ -211,6 +212,11 @@ public class NotificationSerializer {
|
|
|
211
212
|
bundle.putInt("weekday", ((WeeklyTrigger) trigger).getWeekday());
|
|
212
213
|
bundle.putInt("hour", ((WeeklyTrigger) trigger).getHour());
|
|
213
214
|
bundle.putInt("minute", ((WeeklyTrigger) trigger).getMinute());
|
|
215
|
+
} else if (trigger instanceof MonthlyTrigger) {
|
|
216
|
+
bundle.putString("type", "monthly");
|
|
217
|
+
bundle.putInt("day", ((MonthlyTrigger) trigger).getDay());
|
|
218
|
+
bundle.putInt("hour", ((MonthlyTrigger) trigger).getHour());
|
|
219
|
+
bundle.putInt("minute", ((MonthlyTrigger) trigger).getMinute());
|
|
214
220
|
} else if (trigger instanceof YearlyTrigger) {
|
|
215
221
|
bundle.putString("type", "yearly");
|
|
216
222
|
bundle.putInt("day", ((YearlyTrigger) trigger).getDay());
|
|
@@ -25,9 +25,10 @@ import expo.modules.interfaces.taskManager.TaskManagerUtilsInterface;
|
|
|
25
25
|
/**
|
|
26
26
|
* Represents a task to be run when the app is receives a remote push
|
|
27
27
|
* notification. Map of current tasks is maintained in {@link FirebaseMessagingDelegate}.
|
|
28
|
+
*
|
|
29
|
+
* Instances are instantiated by expo task manager, after being registered in ExpoBackgroundNotificationTasksModule
|
|
28
30
|
*/
|
|
29
31
|
public class BackgroundRemoteNotificationTaskConsumer extends TaskConsumer implements TaskConsumerInterface {
|
|
30
|
-
private static final String TAG = BackgroundRemoteNotificationTaskConsumer.class.getSimpleName();
|
|
31
32
|
private static final String NOTIFICATION_KEY = "notification";
|
|
32
33
|
|
|
33
34
|
private TaskInterface mTask;
|
package/android/src/main/java/expo/modules/notifications/notifications/debug/DebugLogging.kt
CHANGED
|
@@ -70,7 +70,7 @@ object DebugLogging {
|
|
|
70
70
|
"""
|
|
71
71
|
$caller:
|
|
72
72
|
notification.notificationRequest.content.title: ${notification.notificationRequest.content.title}
|
|
73
|
-
notification.notificationRequest.content.
|
|
73
|
+
notification.notificationRequest.content.subText: ${notification.notificationRequest.content.subText}
|
|
74
74
|
notification.notificationRequest.content.text: ${notification.notificationRequest.content.text}
|
|
75
75
|
notification.notificationRequest.content.sound: ${notification.notificationRequest.content.soundName}
|
|
76
76
|
notification.notificationRequest.content.channelID: ${notification.notificationRequest.trigger.notificationChannel}
|
|
@@ -99,7 +99,7 @@ open class NotificationsHandler : Module(), NotificationListener {
|
|
|
99
99
|
?: throw NotificationWasAlreadyHandledException(identifier)
|
|
100
100
|
|
|
101
101
|
with(behavior) {
|
|
102
|
-
task.
|
|
102
|
+
task.processNotificationWithBehavior(
|
|
103
103
|
NotificationBehavior(shouldShowAlert, shouldPlaySound, shouldSetBadge, priority),
|
|
104
104
|
promise
|
|
105
105
|
)
|
|
@@ -110,6 +110,8 @@ open class NotificationsHandler : Module(), NotificationListener {
|
|
|
110
110
|
* Callback called by [NotificationManager] to inform its listeners of new messages.
|
|
111
111
|
* Starts up a new [SingleNotificationHandlerTask] which will take it on from here.
|
|
112
112
|
*
|
|
113
|
+
* SingleNotificationHandlerTask.processNotificationWithBehavior can then present it
|
|
114
|
+
*
|
|
113
115
|
* @param notification Notification received
|
|
114
116
|
*/
|
|
115
117
|
override fun onNotificationReceived(notification: Notification) {
|
|
@@ -34,7 +34,6 @@ public class SingleNotificationHandlerTask {
|
|
|
34
34
|
private Handler mHandler;
|
|
35
35
|
private EventEmitter mEventEmitter;
|
|
36
36
|
private Notification mNotification;
|
|
37
|
-
private NotificationBehavior mBehavior;
|
|
38
37
|
private Context mContext;
|
|
39
38
|
private NotificationsHandler mDelegate;
|
|
40
39
|
|
|
@@ -88,12 +87,11 @@ public class SingleNotificationHandlerTask {
|
|
|
88
87
|
* @param behavior Behavior requested by the app
|
|
89
88
|
* @param promise Promise to fulfill once the behavior is applied to the notification.
|
|
90
89
|
*/
|
|
91
|
-
/* package */ void
|
|
92
|
-
mBehavior = behavior;
|
|
90
|
+
/* package */ void processNotificationWithBehavior(final NotificationBehavior behavior, final Promise promise) {
|
|
93
91
|
mHandler.post(new Runnable() {
|
|
94
92
|
@Override
|
|
95
93
|
public void run() {
|
|
96
|
-
NotificationsService.Companion.present(mContext, mNotification,
|
|
94
|
+
NotificationsService.Companion.present(mContext, mNotification, behavior, new ResultReceiver(mHandler) {
|
|
97
95
|
@Override
|
|
98
96
|
protected void onReceiveResult(int resultCode, Bundle resultData) {
|
|
99
97
|
super.onReceiveResult(resultCode, resultData);
|
|
@@ -17,7 +17,7 @@ import org.json.JSONObject
|
|
|
17
17
|
interface INotificationContent : Parcelable {
|
|
18
18
|
val title: String?
|
|
19
19
|
val text: String?
|
|
20
|
-
val
|
|
20
|
+
val subText: String?
|
|
21
21
|
val badgeCount: Number?
|
|
22
22
|
val shouldPlayDefaultSound: Boolean
|
|
23
23
|
|
|
@@ -27,7 +27,7 @@ interface INotificationContent : Parcelable {
|
|
|
27
27
|
val shouldUseDefaultVibrationPattern: Boolean
|
|
28
28
|
val vibrationPattern: LongArray?
|
|
29
29
|
val body: JSONObject?
|
|
30
|
-
val priority: NotificationPriority
|
|
30
|
+
val priority: NotificationPriority?
|
|
31
31
|
val color: Number?
|
|
32
32
|
val isAutoDismiss: Boolean
|
|
33
33
|
val categoryId: String?
|
|
@@ -8,14 +8,6 @@ import expo.modules.notifications.notifications.model.NotificationBehavior
|
|
|
8
8
|
* on a [NotificationContent] spec.
|
|
9
9
|
*/
|
|
10
10
|
interface NotificationBuilder {
|
|
11
|
-
/**
|
|
12
|
-
* Pass in a [Notification] based on which the Android notification should be based.
|
|
13
|
-
*
|
|
14
|
-
* @param notification [Notification] on which the notification should be based.
|
|
15
|
-
* @return The same instance of [NotificationBuilder] updated with the notification.
|
|
16
|
-
*/
|
|
17
|
-
fun setNotification(notification: Notification?): NotificationBuilder?
|
|
18
|
-
|
|
19
11
|
/**
|
|
20
12
|
* Pass in a [NotificationBehavior] if you want to override the behavior
|
|
21
13
|
* of the notification, i.e. whether it should show a heads-up alert, set badge, etc.
|
|
@@ -23,7 +15,7 @@ interface NotificationBuilder {
|
|
|
23
15
|
* @param behavior [NotificationBehavior] to which the presentation effect should conform.
|
|
24
16
|
* @return The same instance of [NotificationBuilder] updated with the remote message.
|
|
25
17
|
*/
|
|
26
|
-
fun setAllowedBehavior(behavior: NotificationBehavior?): NotificationBuilder
|
|
18
|
+
fun setAllowedBehavior(behavior: NotificationBehavior?): NotificationBuilder
|
|
27
19
|
|
|
28
20
|
/**
|
|
29
21
|
* Builds the Android notification based on passed in data.
|
|
@@ -13,7 +13,7 @@ import expo.modules.notifications.notifications.model.NotificationResponse;
|
|
|
13
13
|
*/
|
|
14
14
|
public interface NotificationListener {
|
|
15
15
|
/**
|
|
16
|
-
* Callback called when new notification is received.
|
|
16
|
+
* Callback called when new notification is received while the app is in foreground.
|
|
17
17
|
*
|
|
18
18
|
* @param notification Notification received
|
|
19
19
|
*/
|
package/android/src/main/java/expo/modules/notifications/notifications/model/NotificationAction.java
CHANGED
|
@@ -7,6 +7,9 @@ import java.io.Serializable;
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* A class representing a single notification action button.
|
|
10
|
+
*
|
|
11
|
+
* TODO vonovak: no need to implement serializable, parcelable is enough for storing
|
|
12
|
+
*
|
|
10
13
|
*/
|
|
11
14
|
public class NotificationAction implements Parcelable, Serializable {
|
|
12
15
|
private final String mIdentifier;
|
|
@@ -9,6 +9,8 @@ import java.util.List;
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* A class representing a collection of notification actions.
|
|
12
|
+
*
|
|
13
|
+
* TODO vonovak: no need to implement serializable, parcelable is enough for storing
|
|
12
14
|
*/
|
|
13
15
|
public class NotificationCategory implements Parcelable, Serializable {
|
|
14
16
|
private final String mIdentifier;
|
|
@@ -75,7 +75,7 @@ class RemoteNotificationContent(private val remoteMessage: RemoteMessage) : INot
|
|
|
75
75
|
override val isSticky: Boolean
|
|
76
76
|
get() = remoteMessage.data["sticky"]?.toBoolean() ?: false
|
|
77
77
|
|
|
78
|
-
override val
|
|
78
|
+
override val subText: String?
|
|
79
79
|
get() = remoteMessage.data["subtitle"]
|
|
80
80
|
|
|
81
81
|
override val badgeCount: Number?
|
|
@@ -11,6 +11,8 @@ import expo.modules.notifications.ResultReceiverBody
|
|
|
11
11
|
import expo.modules.notifications.createDefaultResultReceiver
|
|
12
12
|
import expo.modules.notifications.notifications.ArgumentsNotificationContentBuilder
|
|
13
13
|
import expo.modules.notifications.notifications.NotificationSerializer
|
|
14
|
+
import expo.modules.notifications.notifications.interfaces.INotificationContent
|
|
15
|
+
import expo.modules.notifications.notifications.interfaces.NotificationTrigger
|
|
14
16
|
import expo.modules.notifications.notifications.model.Notification
|
|
15
17
|
import expo.modules.notifications.notifications.model.NotificationRequest
|
|
16
18
|
import expo.modules.notifications.service.NotificationsService
|
|
@@ -31,7 +33,7 @@ open class ExpoNotificationPresentationModule : Module() {
|
|
|
31
33
|
|
|
32
34
|
AsyncFunction("presentNotificationAsync") { identifier: String, payload: ReadableArguments, promise: Promise ->
|
|
33
35
|
val content = ArgumentsNotificationContentBuilder(context).setPayload(payload).build()
|
|
34
|
-
val request =
|
|
36
|
+
val request = createNotificationRequest(identifier, content, null)
|
|
35
37
|
val notification = Notification(request)
|
|
36
38
|
present(
|
|
37
39
|
context,
|
|
@@ -54,7 +56,7 @@ open class ExpoNotificationPresentationModule : Module() {
|
|
|
54
56
|
createResultReceiver { resultCode: Int, resultData: Bundle? ->
|
|
55
57
|
val notifications = resultData?.getParcelableArrayList<Notification>(NotificationsService.NOTIFICATIONS_KEY)
|
|
56
58
|
if (resultCode == NotificationsService.SUCCESS_CODE && notifications != null) {
|
|
57
|
-
promise.resolve(notifications
|
|
59
|
+
promise.resolve(serializeNotifications(notifications))
|
|
58
60
|
} else {
|
|
59
61
|
val e = resultData?.getSerializable(NotificationsService.EXCEPTION_KEY) as? Exception
|
|
60
62
|
promise.reject("ERR_NOTIFICATIONS_FETCH_FAILED", "A list of displayed notifications could not be fetched.", e)
|
|
@@ -96,4 +98,16 @@ open class ExpoNotificationPresentationModule : Module() {
|
|
|
96
98
|
}
|
|
97
99
|
)
|
|
98
100
|
}
|
|
101
|
+
|
|
102
|
+
protected open fun createNotificationRequest(
|
|
103
|
+
identifier: String,
|
|
104
|
+
content: INotificationContent,
|
|
105
|
+
trigger: NotificationTrigger?
|
|
106
|
+
): NotificationRequest {
|
|
107
|
+
return NotificationRequest(identifier, content, null)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
protected open fun serializeNotifications(notifications: Collection<Notification>): List<Bundle> {
|
|
111
|
+
return notifications.map(NotificationSerializer::toBundle)
|
|
112
|
+
}
|
|
99
113
|
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
package expo.modules.notifications.notifications.presentation.builders
|
|
2
|
+
|
|
3
|
+
import android.app.NotificationChannel
|
|
4
|
+
import android.app.NotificationManager
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.os.Build
|
|
7
|
+
import android.util.Log
|
|
8
|
+
import androidx.annotation.RequiresApi
|
|
9
|
+
import androidx.core.app.NotificationCompat
|
|
10
|
+
import expo.modules.notifications.R
|
|
11
|
+
import expo.modules.notifications.notifications.channels.managers.AndroidXNotificationsChannelGroupManager
|
|
12
|
+
import expo.modules.notifications.notifications.channels.managers.AndroidXNotificationsChannelManager
|
|
13
|
+
import expo.modules.notifications.notifications.channels.managers.NotificationsChannelManager
|
|
14
|
+
import expo.modules.notifications.notifications.interfaces.INotificationContent
|
|
15
|
+
import expo.modules.notifications.notifications.interfaces.NotificationBuilder
|
|
16
|
+
import expo.modules.notifications.notifications.model.Notification
|
|
17
|
+
import expo.modules.notifications.notifications.model.NotificationBehavior
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A foundation class for [NotificationBuilder] implementations. Takes care
|
|
21
|
+
* of accepting [.mNotification] and [.mNotificationBehavior].
|
|
22
|
+
*/
|
|
23
|
+
abstract class BaseNotificationBuilder protected constructor(protected val context: Context, protected val notification: Notification) :
|
|
24
|
+
NotificationBuilder {
|
|
25
|
+
|
|
26
|
+
protected var notificationBehavior: NotificationBehavior? = null
|
|
27
|
+
private set
|
|
28
|
+
|
|
29
|
+
override fun setAllowedBehavior(behavior: NotificationBehavior?): NotificationBuilder {
|
|
30
|
+
notificationBehavior = behavior
|
|
31
|
+
return this
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fun createBuilder(): NotificationCompat.Builder {
|
|
35
|
+
val builder = channelId?.let { NotificationCompat.Builder(context, it) } ?: NotificationCompat.Builder(context)
|
|
36
|
+
return builder
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
protected val channelId: String?
|
|
40
|
+
/**
|
|
41
|
+
* @return A [NotificationChannel]'s identifier to use for the notification.
|
|
42
|
+
*/
|
|
43
|
+
get() {
|
|
44
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
|
45
|
+
// Returning null on incompatible platforms won't be an error.
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
val trigger = notification.notificationRequest?.trigger
|
|
50
|
+
if (trigger == null) {
|
|
51
|
+
Log.e(
|
|
52
|
+
"notifications",
|
|
53
|
+
String.format(
|
|
54
|
+
"Couldn't get channel for the notifications - trigger is 'null'. Fallback to '%s' channel",
|
|
55
|
+
FALLBACK_CHANNEL_ID
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
return fallbackNotificationChannel!!.id
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
val requestedChannelId = trigger.notificationChannel
|
|
62
|
+
?: return fallbackNotificationChannel!!.id
|
|
63
|
+
|
|
64
|
+
val channelForRequestedId =
|
|
65
|
+
notificationsChannelManager.getNotificationChannel(requestedChannelId)
|
|
66
|
+
if (channelForRequestedId == null) {
|
|
67
|
+
Log.e(
|
|
68
|
+
"notifications",
|
|
69
|
+
String.format(
|
|
70
|
+
"Channel '%s' doesn't exists. Fallback to '%s' channel",
|
|
71
|
+
requestedChannelId,
|
|
72
|
+
FALLBACK_CHANNEL_ID
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
return fallbackNotificationChannel!!.id
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return channelForRequestedId.id
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
open val notificationsChannelManager: NotificationsChannelManager
|
|
82
|
+
get() = AndroidXNotificationsChannelManager(
|
|
83
|
+
context,
|
|
84
|
+
AndroidXNotificationsChannelGroupManager(context)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
private val fallbackNotificationChannel: NotificationChannel?
|
|
88
|
+
/**
|
|
89
|
+
* Fetches the fallback notification channel, and if it doesn't exist yet - creates it.
|
|
90
|
+
*
|
|
91
|
+
*
|
|
92
|
+
* Returns null on [NotificationChannel]-incompatible platforms.
|
|
93
|
+
*
|
|
94
|
+
* @return Fallback [NotificationChannel] or null.
|
|
95
|
+
*/
|
|
96
|
+
get() {
|
|
97
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
val channel = notificationManager.getNotificationChannel(FALLBACK_CHANNEL_ID)
|
|
102
|
+
return channel ?: createFallbackChannel()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Creates a fallback channel of [.FALLBACK_CHANNEL_ID] ID, name fetched
|
|
107
|
+
* from Android resources ([.getFallbackChannelName]
|
|
108
|
+
* and importance set by [.FALLBACK_CHANNEL_IMPORTANCE].
|
|
109
|
+
*
|
|
110
|
+
* @return Newly created channel.
|
|
111
|
+
*/
|
|
112
|
+
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
113
|
+
protected fun createFallbackChannel(): NotificationChannel {
|
|
114
|
+
val channel = NotificationChannel(
|
|
115
|
+
FALLBACK_CHANNEL_ID,
|
|
116
|
+
fallbackChannelName,
|
|
117
|
+
FALLBACK_CHANNEL_IMPORTANCE
|
|
118
|
+
)
|
|
119
|
+
channel.setShowBadge(true)
|
|
120
|
+
channel.enableVibration(true)
|
|
121
|
+
notificationManager.createNotificationChannel(channel)
|
|
122
|
+
return channel
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private val fallbackChannelName: String
|
|
126
|
+
/**
|
|
127
|
+
* Fetches fallback channel name from Android resources. Overridable by Android resources system
|
|
128
|
+
*
|
|
129
|
+
* @return Name of the fallback channel
|
|
130
|
+
*/
|
|
131
|
+
get() = context.getString(R.string.expo_notifications_fallback_channel_name)
|
|
132
|
+
|
|
133
|
+
private val notificationManager: NotificationManager
|
|
134
|
+
get() = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
135
|
+
|
|
136
|
+
companion object {
|
|
137
|
+
private const val FALLBACK_CHANNEL_ID = "expo_notifications_fallback_notification_channel"
|
|
138
|
+
|
|
139
|
+
// Behaviors we will want to impose on received notifications include
|
|
140
|
+
// being displayed as a heads-up notification. For that we will need
|
|
141
|
+
// a channel of high importance.
|
|
142
|
+
@RequiresApi(api = Build.VERSION_CODES.N)
|
|
143
|
+
private val FALLBACK_CHANNEL_IMPORTANCE = NotificationManager.IMPORTANCE_HIGH
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
protected val notificationContent: INotificationContent
|
|
147
|
+
get() = notification.notificationRequest.content
|
|
148
|
+
}
|
|
@@ -10,21 +10,86 @@ import android.os.Parcel
|
|
|
10
10
|
import android.provider.Settings
|
|
11
11
|
import android.util.Log
|
|
12
12
|
import androidx.core.app.NotificationCompat
|
|
13
|
+
import androidx.core.app.RemoteInput
|
|
13
14
|
import expo.modules.notifications.notifications.SoundResolver
|
|
14
15
|
import expo.modules.notifications.notifications.enums.NotificationPriority
|
|
15
16
|
import expo.modules.notifications.notifications.model.NotificationAction
|
|
17
|
+
import expo.modules.notifications.notifications.model.NotificationCategory
|
|
16
18
|
import expo.modules.notifications.notifications.model.NotificationRequest
|
|
17
19
|
import expo.modules.notifications.notifications.model.NotificationResponse
|
|
20
|
+
import expo.modules.notifications.notifications.model.TextInputNotificationAction
|
|
21
|
+
import expo.modules.notifications.service.NotificationsService
|
|
18
22
|
import expo.modules.notifications.service.NotificationsService.Companion.createNotificationResponseIntent
|
|
23
|
+
import expo.modules.notifications.service.delegates.SharedPreferencesNotificationCategoriesStore
|
|
24
|
+
import java.io.IOException
|
|
19
25
|
import kotlin.math.max
|
|
20
26
|
import kotlin.math.min
|
|
21
27
|
|
|
22
28
|
/**
|
|
23
29
|
* [NotificationBuilder] interpreting a JSON request object.
|
|
24
30
|
*/
|
|
25
|
-
open class ExpoNotificationBuilder(
|
|
31
|
+
open class ExpoNotificationBuilder(
|
|
32
|
+
context: Context,
|
|
33
|
+
notification: expo.modules.notifications.notifications.model.Notification,
|
|
34
|
+
private val store: SharedPreferencesNotificationCategoriesStore
|
|
35
|
+
) : BaseNotificationBuilder(context, notification) {
|
|
36
|
+
|
|
37
|
+
open fun addActionsToBuilder(
|
|
38
|
+
builder: NotificationCompat.Builder,
|
|
39
|
+
categoryIdentifier: String
|
|
40
|
+
) {
|
|
41
|
+
var actions = emptyList<NotificationAction>()
|
|
42
|
+
try {
|
|
43
|
+
val category: NotificationCategory? = store.getNotificationCategory(categoryIdentifier)
|
|
44
|
+
if (category != null) {
|
|
45
|
+
actions = category.actions
|
|
46
|
+
}
|
|
47
|
+
} catch (e: ClassNotFoundException) {
|
|
48
|
+
Log.e(
|
|
49
|
+
"expo-notifications",
|
|
50
|
+
String.format(
|
|
51
|
+
"Could not read category with identifier: %s. %s",
|
|
52
|
+
categoryIdentifier,
|
|
53
|
+
e.message
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
} catch (e: IOException) {
|
|
57
|
+
Log.e(
|
|
58
|
+
"expo-notifications",
|
|
59
|
+
String.format(
|
|
60
|
+
"Could not read category with identifier: %s. %s",
|
|
61
|
+
categoryIdentifier,
|
|
62
|
+
e.message
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
for (action in actions) {
|
|
67
|
+
if (action is TextInputNotificationAction) {
|
|
68
|
+
builder.addAction(buildTextInputAction(action))
|
|
69
|
+
} else {
|
|
70
|
+
builder.addAction(buildButtonAction(action))
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
protected fun buildButtonAction(action: NotificationAction): NotificationCompat.Action {
|
|
76
|
+
val intent = createNotificationResponseIntent(context, notification, action)
|
|
77
|
+
return NotificationCompat.Action.Builder(icon, action.title, intent).build()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
protected fun buildTextInputAction(action: TextInputNotificationAction): NotificationCompat.Action {
|
|
81
|
+
val intent = createNotificationResponseIntent(context, notification, action)
|
|
82
|
+
val remoteInput = RemoteInput.Builder(NotificationsService.USER_TEXT_RESPONSE_KEY)
|
|
83
|
+
.setLabel(action.placeholder)
|
|
84
|
+
.build()
|
|
85
|
+
|
|
86
|
+
return NotificationCompat.Action.Builder(icon, action.title, intent)
|
|
87
|
+
.addRemoteInput(remoteInput).build()
|
|
88
|
+
}
|
|
89
|
+
|
|
26
90
|
override suspend fun build(): Notification {
|
|
27
|
-
val builder =
|
|
91
|
+
val builder = createBuilder()
|
|
92
|
+
|
|
28
93
|
builder.setSmallIcon(icon)
|
|
29
94
|
builder.setPriority(priority)
|
|
30
95
|
|
|
@@ -33,14 +98,17 @@ open class ExpoNotificationBuilder(context: Context?) : ChannelAwareNotification
|
|
|
33
98
|
builder.setAutoCancel(content.isAutoDismiss)
|
|
34
99
|
builder.setOngoing(content.isSticky)
|
|
35
100
|
|
|
101
|
+
// see "Notification anatomy" https://developer.android.com/develop/ui/views/notifications#Templates
|
|
36
102
|
builder.setContentTitle(content.title)
|
|
37
103
|
builder.setContentText(content.text)
|
|
38
|
-
builder.setSubText(content.
|
|
104
|
+
builder.setSubText(content.subText)
|
|
39
105
|
// Sets the text/contentText as the bigText to allow the notification to be expanded and the
|
|
40
106
|
// entire text to be viewed.
|
|
41
107
|
builder.setStyle(NotificationCompat.BigTextStyle().bigText(content.text))
|
|
42
108
|
|
|
43
109
|
color?.let { builder.color = it.toInt() }
|
|
110
|
+
notificationContent.badgeCount?.toInt()?.let { builder.setNumber(it) }
|
|
111
|
+
notificationContent.categoryId?.let { addActionsToBuilder(builder, it) }
|
|
44
112
|
|
|
45
113
|
val shouldPlayDefaultSound = shouldPlaySound() && content.shouldPlayDefaultSound
|
|
46
114
|
if (shouldPlayDefaultSound && shouldVibrate()) {
|
|
@@ -147,10 +215,9 @@ open class ExpoNotificationBuilder(context: Context?) : ChannelAwareNotification
|
|
|
147
215
|
* @return Whether the notification should play a sound.
|
|
148
216
|
*/
|
|
149
217
|
private fun shouldPlaySound(): Boolean {
|
|
150
|
-
val behaviorAllowsSound =
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
val contentAllowsSound = notificationContent.shouldPlayDefaultSound || notificationContent.soundName != null
|
|
218
|
+
val behaviorAllowsSound = notificationBehavior?.shouldPlaySound() ?: true
|
|
219
|
+
val contentAllowsSound =
|
|
220
|
+
notificationContent.shouldPlayDefaultSound || notificationContent.soundName != null
|
|
154
221
|
|
|
155
222
|
return behaviorAllowsSound && contentAllowsSound
|
|
156
223
|
}
|
|
@@ -166,8 +233,7 @@ open class ExpoNotificationBuilder(context: Context?) : ChannelAwareNotification
|
|
|
166
233
|
* @return Whether the notification should vibrate.
|
|
167
234
|
*/
|
|
168
235
|
private fun shouldVibrate(): Boolean {
|
|
169
|
-
val behaviorAllowsVibration =
|
|
170
|
-
notificationBehavior == null || notificationBehavior.shouldPlaySound()
|
|
236
|
+
val behaviorAllowsVibration = notificationBehavior?.shouldPlaySound() ?: true
|
|
171
237
|
|
|
172
238
|
val contentAllowsVibration =
|
|
173
239
|
notificationContent.shouldUseDefaultVibrationPattern || notificationContent.vibrationPattern != null
|
|
@@ -194,6 +260,7 @@ open class ExpoNotificationBuilder(context: Context?) : ChannelAwareNotification
|
|
|
194
260
|
get() {
|
|
195
261
|
val requestPriority = notificationContent.priority
|
|
196
262
|
|
|
263
|
+
val notificationBehavior = notificationBehavior
|
|
197
264
|
// If we know of a behavior guideline, let's honor it...
|
|
198
265
|
if (notificationBehavior != null) {
|
|
199
266
|
// ...by using the priority override...
|
|
@@ -258,7 +325,7 @@ open class ExpoNotificationBuilder(context: Context?) : ChannelAwareNotification
|
|
|
258
325
|
return null
|
|
259
326
|
}
|
|
260
327
|
|
|
261
|
-
protected val icon: Int
|
|
328
|
+
protected open val icon: Int
|
|
262
329
|
/**
|
|
263
330
|
* The method first tries to get the icon from the manifest's meta-data [.META_DATA_DEFAULT_ICON_KEY].
|
|
264
331
|
* If a custom setting is not found, the method falls back to using app icon.
|
|
@@ -280,7 +347,7 @@ open class ExpoNotificationBuilder(context: Context?) : ChannelAwareNotification
|
|
|
280
347
|
return context.applicationInfo.icon
|
|
281
348
|
}
|
|
282
349
|
|
|
283
|
-
protected val color: Number?
|
|
350
|
+
protected open val color: Number?
|
|
284
351
|
/**
|
|
285
352
|
* The method responsible for finding and returning a custom color used to color the notification icon.
|
|
286
353
|
* It first tries to use a custom color defined in notification content, then it tries to fetch color
|
|
@@ -290,27 +357,29 @@ open class ExpoNotificationBuilder(context: Context?) : ChannelAwareNotification
|
|
|
290
357
|
* or null if the default should be used.
|
|
291
358
|
*/
|
|
292
359
|
get() {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
360
|
+
return notificationContent.color ?: run {
|
|
361
|
+
try {
|
|
362
|
+
val ai = context.packageManager.getApplicationInfo(
|
|
363
|
+
context.packageName,
|
|
364
|
+
PackageManager.GET_META_DATA
|
|
365
|
+
)
|
|
366
|
+
if (ai.metaData.containsKey(META_DATA_DEFAULT_COLOR_KEY)) {
|
|
367
|
+
return context.resources.getColor(
|
|
368
|
+
ai.metaData.getInt(META_DATA_DEFAULT_COLOR_KEY),
|
|
369
|
+
null
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
} catch (e: Exception) {
|
|
373
|
+
Log.e(
|
|
374
|
+
"expo-notifications",
|
|
375
|
+
"Could not have fetched default notification color.",
|
|
376
|
+
e
|
|
306
377
|
)
|
|
307
378
|
}
|
|
308
|
-
} catch (e: Exception) {
|
|
309
|
-
Log.e("expo-notifications", "Could not have fetched default notification color.", e)
|
|
310
|
-
}
|
|
311
379
|
|
|
312
|
-
|
|
313
|
-
|
|
380
|
+
// No custom color
|
|
381
|
+
return null
|
|
382
|
+
}
|
|
314
383
|
}
|
|
315
384
|
|
|
316
385
|
companion object {
|
|
@@ -21,6 +21,7 @@ import expo.modules.notifications.notifications.model.NotificationRequest
|
|
|
21
21
|
import expo.modules.notifications.notifications.triggers.ChannelAwareTrigger
|
|
22
22
|
import expo.modules.notifications.notifications.triggers.DailyTrigger
|
|
23
23
|
import expo.modules.notifications.notifications.triggers.DateTrigger
|
|
24
|
+
import expo.modules.notifications.notifications.triggers.MonthlyTrigger
|
|
24
25
|
import expo.modules.notifications.notifications.triggers.TimeIntervalTrigger
|
|
25
26
|
import expo.modules.notifications.notifications.triggers.WeeklyTrigger
|
|
26
27
|
import expo.modules.notifications.notifications.triggers.YearlyTrigger
|
|
@@ -197,6 +198,23 @@ open class NotificationScheduler : Module() {
|
|
|
197
198
|
)
|
|
198
199
|
}
|
|
199
200
|
|
|
201
|
+
"monthly" -> {
|
|
202
|
+
val day = params["day"] as? Number
|
|
203
|
+
val hour = params["hour"] as? Number
|
|
204
|
+
val minute = params["minute"] as? Number
|
|
205
|
+
|
|
206
|
+
if (day == null || hour == null || minute == null) {
|
|
207
|
+
throw InvalidArgumentException("Invalid value(s) provided for yearly trigger.")
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
MonthlyTrigger(
|
|
211
|
+
day.toInt(),
|
|
212
|
+
hour.toInt(),
|
|
213
|
+
minute.toInt(),
|
|
214
|
+
channelId
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
200
218
|
"yearly" -> {
|
|
201
219
|
val day = params["day"] as? Number
|
|
202
220
|
val month = params["month"] as? Number
|