Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge expo-notifications FCMv1 fixes to SDK 50 #29714

Merged
merged 9 commits into from
Jun 13, 2024
Merged
6 changes: 6 additions & 0 deletions docs/pages/versions/unversioned/sdk/notifications.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,11 @@ To configure `expo-notifications`, use the built-in [config plugin](/config-plug
description:
'Tint color for the push notification image when it appears in the notification tray.',
},
{
name: 'defaultChannel',
platform: 'android',
description: 'Default channel for FCMv1 notifications.',
},
{
name: 'sounds',
description:
Expand All @@ -368,6 +373,7 @@ Here is an example of using the config plugin in the app config file:
{
"icon": "./local/assets/notification-icon.png",
"color": "#ffffff",
"defaultChannel": "default",
"sounds": [
"./local/assets/notification-sound.wav",
"./local/assets/notification-sound-other.wav"
Expand Down
12 changes: 12 additions & 0 deletions packages/expo-notifications/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@

### 🐛 Bug fixes

- [Android] adjust logic in handling responses when app in background or killed ([#29659](https://github.com/expo/expo/pull/29659) by [@douglowder](https://github.com/douglowder)))
- [Android] Fix setBadgeCount() when badge count is 0. ([#29657](https://github.com/expo/expo/pull/29657) by [@douglowder](https://github.com/douglowder))
- [Android] Add default channel plugin prop, restore legacy icon and color. ([#29491](https://github.com/expo/expo/pull/29491) by [@douglowder](https://github.com/douglowder))
- Remove console.log line. ([#29443](https://github.com/expo/expo/pull/29443) by [@douglowder](https://github.com/douglowder))
- [Android] Remove unneeded logging. ([#29370](https://github.com/expo/expo/pull/29370) by [@douglowder](https://github.com/douglowder))
- [Android] Fix FCMv1 icons and NPE. ([#29204](https://github.com/expo/expo/pull/29204) by [@douglowder](https://github.com/douglowder))
- [Android] Correctly map response in useLastNotificationResponse hook. ([#28938](https://github.com/expo/expo/pull/28938) by [@douglowder](https://github.com/douglowder))
- [Android] fix response handling when app in background or not running. ([#28883](https://github.com/expo/expo/pull/28883) by [@douglowder](https://github.com/douglowder))
- [Android] Fix notifications events were using an incorrect event emitter. ([#28207](https://github.com/expo/expo/pull/28207) by [@lukmccall](https://github.com/lukmccall))

### 💡 Others

## 0.27.7 — 2024-04-15
Expand Down Expand Up @@ -357,9 +367,11 @@ _This version does not introduce any user-facing changes._
- Changed class responsible for handling Firebase events from `FirebaseMessagingService` to `.service.NotificationsService` on Android. ([#10558](https://github.com/expo/expo/pull/10558) by [@sjchmiela](https://github.com/sjchmiela))

> Note that this change most probably will not affect you — it only affects projects that override `FirebaseMessagingService` to implement some custom handling logic.

- Changed how you can override ways in which a notification is reinterpreted from a [`StatusBarNotification`](https://developer.android.com/reference/android/service/notification/StatusBarNotification) and in which a [`Notification`](https://developer.android.com/reference/android/app/Notification.html?hl=en) is built from defining an `expo.modules.notifications#NotificationsScoper` meta-data value in `AndroidManifest.xml` to implementing a `BroadcastReceiver` subclassing `NotificationsService` delegating those responsibilities to your custom `PresentationDelegate` instance. ([#10558](https://github.com/expo/expo/pull/10558) by [@sjchmiela](https://github.com/sjchmiela))

> Note that this change most probably will not affect you — it only affects projects that override those methods to implement some custom handling logic.

- Removed `removeAllNotificationListeners` method. You can (and should) still remove listeners using `remove` method on `Subscription` objects returned by `addNotification…Listener`. ([#10883](https://github.com/expo/expo/pull/10883) by [@sjchmiela](https://github.com/sjchmiela))
- Fixed device identifier being used to fetch Expo push token being backed up on Android which resulted in multiple devices having the same `deviceId` (and eventually, Expo push token). ([#11005](https://github.com/expo/expo/pull/11005) by [@sjchmiela](https://github.com/sjchmiela))
- Fixed device identifier used when fetching Expo push token being different than `Constants.installationId` in managed workflow apps which resulted in different Expo push tokens returned for the same experience across old and new Expo API and the device push token not being automatically updated on Expo push servers which lead to Expo push tokens corresponding to outdated Firebase tokens. ([#11005](https://github.com/expo/expo/pull/11005) by [@sjchmiela](https://github.com/sjchmiela))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,34 @@
import android.content.Context;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import expo.modules.core.BasePackage;
import expo.modules.core.ExportedModule;
import expo.modules.core.interfaces.InternalModule;
import expo.modules.core.interfaces.ReactActivityLifecycleListener;
import expo.modules.core.interfaces.SingletonModule;
import expo.modules.notifications.notifications.NotificationManager;
import expo.modules.notifications.notifications.categories.ExpoNotificationCategoriesModule;
import expo.modules.notifications.notifications.categories.serializers.ExpoNotificationsCategoriesSerializer;
import expo.modules.notifications.notifications.channels.AndroidXNotificationsChannelsProvider;
import expo.modules.notifications.service.delegates.ExpoNotificationLifecycleListener;
import expo.modules.notifications.tokens.PushTokenManager;

public class NotificationsPackage extends BasePackage {

private NotificationManager mNotificationManager;

public NotificationsPackage() {
mNotificationManager = new NotificationManager();
}

@Override
public List<SingletonModule> createSingletonModules(Context context) {
return Arrays.asList(
new PushTokenManager(),
new NotificationManager()
mNotificationManager
);
}

Expand All @@ -31,4 +41,10 @@ public List<InternalModule> createInternalModules(Context context) {
new ExpoNotificationsCategoriesSerializer()
);
}

@Override
public List<ReactActivityLifecycleListener> createReactActivityLifecycleListeners(Context activityContext) {
return Collections.singletonList(new ExpoNotificationLifecycleListener(activityContext, mNotificationManager));
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ object BadgeHelper {

fun setBadgeCount(context: Context, badgeCount: Int): Boolean {
return try {
ShortcutBadger.applyCountOrThrow(context.applicationContext, badgeCount)
if (badgeCount == 0) {
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
notificationManager.cancelAll()
} else {
ShortcutBadger.applyCountOrThrow(context.applicationContext, badgeCount)
}
BadgeHelper.badgeCount = badgeCount
true
} catch (e: ShortcutBadgeException) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package expo.modules.notifications.notifications;

import android.os.Bundle;
import android.util.Log;

import expo.modules.core.interfaces.SingletonModule;

import java.lang.ref.WeakReference;
Expand All @@ -22,6 +25,7 @@ public class NotificationManager implements SingletonModule, expo.modules.notifi
*/
private WeakHashMap<NotificationListener, WeakReference<NotificationListener>> mListenerReferenceMap;
private Collection<NotificationResponse> mPendingNotificationResponses = new ArrayList<>();
private Collection<Bundle> mPendingNotificationResponsesFromExtras = new ArrayList<>();

public NotificationManager() {
mListenerReferenceMap = new WeakHashMap<>();
Expand Down Expand Up @@ -53,6 +57,11 @@ public void addListener(NotificationListener listener) {
listener.onNotificationResponseReceived(pendingResponse);
}
}
if (!mPendingNotificationResponsesFromExtras.isEmpty()) {
for (Bundle extras : mPendingNotificationResponsesFromExtras) {
listener.onNotificationResponseIntentReceived(extras);
}
}
}
}

Expand Down Expand Up @@ -116,4 +125,28 @@ public void onNotificationsDropped() {
}
}
}

public void onNotificationResponseFromExtras(Bundle extras) {
// We're going to be passed in extras from either
// a killed state (ExpoNotificationLifecycleListener::onCreate)
// OR a background state (ExpoNotificationLifecycleListener::onNewIntent)

// If we've just come from a background state, we'll have listeners set up
// pass on the notification to them
if (!mListenerReferenceMap.isEmpty()) {
for (WeakReference<NotificationListener> listenerReference : mListenerReferenceMap.values()) {
NotificationListener listener = listenerReference.get();
if (listener != null) {
listener.onNotificationResponseIntentReceived(extras);
}
}
} else {
// Otherwise, the app has been launched from a killed state, and our listeners
// haven't yet been setup. We'll add this to a list of pending notifications
// for them to process once they've been initialized.
if (mPendingNotificationResponsesFromExtras.isEmpty()) {
mPendingNotificationResponsesFromExtras.add(extras);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import android.os.Build;
import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import org.jetbrains.annotations.NotNull;
import org.json.JSONArray;
import org.json.JSONObject;
import expo.modules.core.arguments.MapArguments;
Expand All @@ -19,16 +19,12 @@

import expo.modules.notifications.notifications.interfaces.NotificationTrigger;
import expo.modules.notifications.notifications.model.Notification;
import expo.modules.notifications.notifications.model.NotificationAction;
import expo.modules.notifications.notifications.model.NotificationCategory;
import expo.modules.notifications.notifications.model.NotificationContent;
import expo.modules.notifications.notifications.model.NotificationRequest;
import expo.modules.notifications.notifications.model.NotificationResponse;
import expo.modules.notifications.notifications.model.TextInputNotificationAction;
import expo.modules.notifications.notifications.model.TextInputNotificationResponse;
import expo.modules.notifications.notifications.model.triggers.FirebaseNotificationTrigger;

import expo.modules.notifications.notifications.triggers.ChannelAwareTrigger;
import expo.modules.notifications.notifications.triggers.DailyTrigger;
import expo.modules.notifications.notifications.triggers.DateTrigger;
import expo.modules.notifications.notifications.triggers.TimeIntervalTrigger;
Expand Down Expand Up @@ -199,4 +195,31 @@ private static String getChannelId(NotificationTrigger trigger) {
}
return null;
}

@NotNull
public static Bundle toResponseBundleFromExtras(Bundle extras) {
Bundle serializedContent = new Bundle();
serializedContent.putString("title", extras.getString("title"));
serializedContent.putString("body", extras.getString("message"));
serializedContent.putString("dataString", extras.getString("body"));

Bundle serializedTrigger = new Bundle();
serializedTrigger.putString("type", "push");
serializedTrigger.putString("channelId", extras.getString("channelId"));

Bundle serializedRequest = new Bundle();
serializedRequest.putString("identifier", extras.getString("google.message_id"));
serializedRequest.putBundle("trigger", serializedTrigger);
serializedRequest.putBundle("content", serializedContent);

Bundle serializedNotification = new Bundle();
serializedNotification.putLong("date", extras.getLong("google.sent_time"));
serializedNotification.putBundle("request", serializedRequest);

Bundle serializedResponse = new Bundle();
serializedResponse.putString("actionIdentifier", "expo.modules.notifications.actions.DEFAULT");
serializedResponse.putBundle("notification", serializedNotification);

return serializedResponse;
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package expo.modules.notifications.notifications.emitting

import android.os.Bundle
import expo.modules.core.interfaces.services.EventEmitter
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import expo.modules.notifications.ModuleNotFoundException
import expo.modules.notifications.notifications.NotificationSerializer
import expo.modules.notifications.notifications.interfaces.NotificationListener
import expo.modules.notifications.notifications.interfaces.NotificationManager
Expand All @@ -17,8 +15,7 @@ private const val MESSAGES_DELETED_EVENT_NAME = "onNotificationsDeleted"

open class NotificationsEmitter : Module(), NotificationListener {
private lateinit var notificationManager: NotificationManager
private var lastNotificationResponse: NotificationResponse? = null
private var eventEmitter: EventEmitter? = null
private var lastNotificationResponseBundle: Bundle? = null

override fun definition() = ModuleDefinition {
Name("ExpoNotificationsEmitter")
Expand All @@ -30,9 +27,6 @@ open class NotificationsEmitter : Module(), NotificationListener {
)

OnCreate {
eventEmitter = appContext.legacyModule<EventEmitter>()
?: throw ModuleNotFoundException(EventEmitter::class)

// Register the module as a listener in NotificationManager singleton module.
// Deregistration happens in onDestroy callback.
notificationManager = requireNotNull(appContext.legacyModuleRegistry.getSingletonModule("NotificationManager", NotificationManager::class.java))
Expand All @@ -44,7 +38,7 @@ open class NotificationsEmitter : Module(), NotificationListener {
}

AsyncFunction("getLastNotificationResponseAsync") {
lastNotificationResponse?.let(NotificationSerializer::toBundle)
lastNotificationResponseBundle
}
}

Expand All @@ -55,7 +49,7 @@ open class NotificationsEmitter : Module(), NotificationListener {
* @param notification Notification received
*/
override fun onNotificationReceived(notification: Notification) {
eventEmitter?.emit(NEW_MESSAGE_EVENT_NAME, NotificationSerializer.toBundle(notification))
sendEvent(NEW_MESSAGE_EVENT_NAME, NotificationSerializer.toBundle(notification))
}

/**
Expand All @@ -66,19 +60,21 @@ open class NotificationsEmitter : Module(), NotificationListener {
* @return Whether notification has been handled
*/
override fun onNotificationResponseReceived(response: NotificationResponse): Boolean {
lastNotificationResponse = response
eventEmitter?.let {
it.emit(NEW_RESPONSE_EVENT_NAME, NotificationSerializer.toBundle(response))
return true
}
return false
lastNotificationResponseBundle = NotificationSerializer.toBundle(response)
sendEvent(NEW_RESPONSE_EVENT_NAME, lastNotificationResponseBundle)
return true
}

override fun onNotificationResponseIntentReceived(extras: Bundle?) {
lastNotificationResponseBundle = NotificationSerializer.toResponseBundleFromExtras(extras)
sendEvent(NEW_RESPONSE_EVENT_NAME, lastNotificationResponseBundle)
}

/**
* Callback called when [NotificationManager] gets informed of the fact of message dropping.
* Emits a [MESSAGES_DELETED_EVENT_NAME] event.
*/
override fun onNotificationsDropped() {
eventEmitter?.emit(MESSAGES_DELETED_EVENT_NAME, Bundle.EMPTY)
sendEvent(MESSAGES_DELETED_EVENT_NAME, Bundle.EMPTY)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,13 @@ open class NotificationsHandler : Module(), NotificationListener {
*/
override fun onNotificationReceived(notification: Notification) {
val context = appContext.reactContext ?: return
val task = SingleNotificationHandlerTask(context, handler, moduleRegistry, notification, this)
val task = SingleNotificationHandlerTask(
context,
appContext.eventEmitter(this),
handler,
notification,
this
)
tasksMap[task.identifier] = task
task.start()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@
import android.os.Handler;
import android.os.ResultReceiver;

import expo.modules.core.ModuleRegistry;
import expo.modules.core.Promise;
import expo.modules.core.interfaces.services.EventEmitter;

import expo.modules.notifications.notifications.NotificationSerializer;
import expo.modules.notifications.notifications.model.Notification;
import expo.modules.notifications.notifications.model.NotificationBehavior;
Expand Down Expand Up @@ -42,10 +40,16 @@ public class SingleNotificationHandlerTask {

private Runnable mTimeoutRunnable = SingleNotificationHandlerTask.this::handleTimeout;

/* package */ SingleNotificationHandlerTask(Context context, Handler handler, ModuleRegistry moduleRegistry, Notification notification, NotificationsHandler delegate) {
/* package */ SingleNotificationHandlerTask(
Context context,
EventEmitter eventEmitter,
Handler handler,
Notification notification,
NotificationsHandler delegate
) {
mContext = context;
mHandler = handler;
mEventEmitter = moduleRegistry.getModule(EventEmitter.class);
mEventEmitter = eventEmitter;
mNotification = notification;
mDelegate = delegate;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package expo.modules.notifications.notifications.interfaces;

import android.os.Bundle;

import com.google.firebase.messaging.FirebaseMessagingService;

import expo.modules.notifications.notifications.model.Notification;
Expand Down Expand Up @@ -28,6 +30,14 @@ default boolean onNotificationResponseReceived(NotificationResponse response) {
return false;
}

/**
* Callback called when notification response is received through package lifecycle listeners
*
* @param extras Bundle of extras from the lifecycle method
*/
default void onNotificationResponseIntentReceived(Bundle extras) {
}

/**
* Callback called when some notifications are dropped.
* See {@link FirebaseMessagingService#onDeletedMessages()}
Expand Down
Loading
Loading