diff --git a/android/build.gradle b/android/build.gradle index ba96df94..fdb05589 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -15,8 +15,8 @@ buildscript { apply plugin: 'com.android.library' android { - compileSdkVersion 26 - buildToolsVersion "25.0.3" + compileSdkVersion 27 + buildToolsVersion "27.0.2" defaultConfig { minSdkVersion 16 targetSdkVersion 26 @@ -82,6 +82,7 @@ rootProject.gradle.buildFinished { buildResult -> dependencies { compile fileTree(include: ['*.jar'], dir: 'libs') compile "com.facebook.react:react-native:+" // From node_modules + compile "com.android.support:support-v4:27.0.2" compile 'me.leolin:ShortcutBadger:1.1.21@aar' compile "com.google.android.gms:play-services-base:$firebaseVersion" compile "com.google.firebase:firebase-core:$firebaseVersion" diff --git a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseLocalMessagingHelper.java b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseLocalMessagingHelper.java index 2bfbd12b..366e1923 100644 --- a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseLocalMessagingHelper.java +++ b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseLocalMessagingHelper.java @@ -42,23 +42,8 @@ public class RNFirebaseLocalMessagingHelper { sharedPreferences = mContext.getSharedPreferences(PREFERENCES_KEY, Context.MODE_PRIVATE); } - public void sendNotification(Bundle bundle) { - - } - public void setApplicationForeground(boolean foreground){ mIsForeground = foreground; } - private Class getMainActivityClass() { - String packageName = mContext.getPackageName(); - Intent launchIntent = mContext.getPackageManager().getLaunchIntentForPackage(packageName); - String className = launchIntent.getComponent().getClassName(); - try { - return Class.forName(className); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - return null; - } - } } diff --git a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java index d87207da..7376e452 100644 --- a/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java +++ b/android/src/main/java/io/invertase/firebase/messaging/RNFirebaseMessaging.java @@ -155,6 +155,9 @@ public class RNFirebaseMessaging extends ReactContextBaseJavaModule implements A FirebaseMessaging.getInstance().unsubscribeFromTopic(topic); } + ////////////////////////////////////////////////////////////////////// + // Start ActivityEventListener methods + ////////////////////////////////////////////////////////////////////// @Override public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { // FCM functionality does not need this function @@ -168,6 +171,9 @@ public class RNFirebaseMessaging extends ReactContextBaseJavaModule implements A Utils.sendEvent(getReactApplicationContext(), "messaging_message_received", messageMap); } } + ////////////////////////////////////////////////////////////////////// + // End ActivityEventListener methods + ////////////////////////////////////////////////////////////////////// private WritableMap parseIntentForMessage(Intent intent) { // Check if FCM data exists diff --git a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java index 606334b7..387727ba 100644 --- a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java +++ b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java @@ -2,15 +2,12 @@ package io.invertase.firebase.notifications; import android.app.AlarmManager; -import android.app.Application; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; -import android.content.pm.ApplicationInfo; -import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; @@ -19,6 +16,7 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.support.v4.app.NotificationCompat; +import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import com.facebook.react.bridge.Arguments; @@ -29,7 +27,6 @@ import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; -import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.ArrayList; @@ -39,9 +36,11 @@ import io.invertase.firebase.messaging.BundleJSONConverter; public class RNFirebaseNotificationManager { private static final String PREFERENCES_KEY = "RNFNotifications"; + public static final String SCHEDULED_NOTIFICATION_EVENT = "notifications-scheduled-notification"; private static final String TAG = "RNFNotificationManager"; private AlarmManager alarmManager; private Context context; + private boolean isForeground = false; private NotificationManager notificationManager; private SharedPreferences preferences; @@ -72,32 +71,100 @@ public class RNFirebaseNotificationManager { preferences.edit().remove(notificationId).apply(); } - public void displayNotification(Bundle notification) { - displayNotification(notification, null); - } - public void displayNotification(ReadableMap notification, Promise promise) { Bundle notificationBundle = Arguments.toBundle(notification); displayNotification(notificationBundle, promise); } + public void displayScheduledNotification(Bundle notification) { + // Broadcast the notification to the RN Application + Intent scheduledNotificationEvent = new Intent(SCHEDULED_NOTIFICATION_EVENT); + scheduledNotificationEvent.putExtra("notification", notification); + LocalBroadcastManager.getInstance(context).sendBroadcast(scheduledNotificationEvent); + + // If this isn't a repeated notification, clear it from the scheduled notifications list + if (!notification.getBundle("schedule").containsKey("repeated") + || !notification.getBundle("schedule").getBoolean("repeated")) { + String notificationId = notification.getString("notificationId"); + preferences.edit().remove(notificationId).apply();; + } + + // If the app isn't in the foreground, then we display it + // Otherwise, it is up to the JS to decide whether to send the notification + if (!isForeground) { + displayNotification(notification, null); + } + } + + public ArrayList getScheduledNotifications(){ + ArrayList array = new ArrayList<>(); + + Map notifications = preferences.getAll(); + + for(String notificationId : notifications.keySet()){ + try { + JSONObject json = new JSONObject((String)notifications.get(notificationId)); + Bundle bundle = BundleJSONConverter.convertToBundle(json); + array.add(bundle); + } catch (JSONException e) { + Log.e(TAG, e.getMessage()); + } + } + return array; + } + + public void removeAllDeliveredNotifications() { + notificationManager.cancelAll(); + } + + public void removeDeliveredNotification(String notificationId) { + notificationManager.cancel(notificationId.hashCode()); + } + + + public void rescheduleNotifications() { + ArrayList bundles = getScheduledNotifications(); + for(Bundle bundle: bundles){ + scheduleNotification(bundle, null); + } + } + + public void scheduleNotification(ReadableMap notification, Promise promise) { + Bundle notificationBundle = Arguments.toBundle(notification); + + scheduleNotification(notificationBundle, promise); + } + + public void setIsForeground(boolean isForeground) { + this.isForeground = isForeground; + } + + private void cancelAlarm(String notificationId) { + Intent notificationIntent = new Intent(context, RNFirebaseNotificationManager.class); + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, notificationId.hashCode(), notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); + alarmManager.cancel(pendingIntent); + } + private void displayNotification(Bundle notification, Promise promise) { try { Class intentClass = getMainActivityClass(); if (intentClass == null) { + if (promise != null) { + promise.reject("notification/display_notification_error", "Could not find main activity class"); + } return; } - if (bundle.getString("body") == null) { - return; - } - - Resources res = mContext.getResources(); - String packageName = mContext.getPackageName(); - String channelId = notification.getString("channelId"); + String notificationId = notification.getString("notificationId"); - NotificationCompat.Builder nb = new NotificationCompat.Builder(context, channelId); + NotificationCompat.Builder nb; + // TODO: Change 27 to 'Build.VERSION_CODES.O_MR1' when using appsupport v27 + if (Build.VERSION.SDK_INT >= 27) { + nb = new NotificationCompat.Builder(context, channelId); + } else { + nb = new NotificationCompat.Builder(context); + } if (notification.containsKey("body")) { nb = nb.setContentText(notification.getString("body")); @@ -106,8 +173,16 @@ public class RNFirebaseNotificationManager { nb = nb.setExtras(notification.getBundle("data")); } if (notification.containsKey("sound")) { - // TODO: Sound URI; - nb = nb.setSound(); + String sound = notification.getString("sound"); + if (sound.equalsIgnoreCase("default")) { + nb = nb.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)); + } else { + int soundResourceId = getResourceId("raw", sound); + if (soundResourceId == 0) { + soundResourceId = getResourceId("raw", sound.substring(0, sound.lastIndexOf('.'))); + } + nb = nb.setSound(Uri.parse("android.resource://" + context.getPackageName() + "/" + soundResourceId)); + } } if (notification.containsKey("subtitle")) { nb = nb.setSubText(notification.getString("subtitle")); @@ -126,7 +201,8 @@ public class RNFirebaseNotificationManager { nb = nb.setCategory(notification.getString("category")); } if (notification.containsKey("color")) { - nb = nb.setColor(notification.getInt("color")); + String color = notification.getString("color"); + nb = nb.setColor(Color.parseColor(color)); } if (notification.containsKey("colorized")) { nb = nb.setColorized(notification.getBoolean("colorized")); @@ -135,8 +211,12 @@ public class RNFirebaseNotificationManager { nb = nb.setContentInfo(notification.getString("contentInfo")); } if (notification.containsKey("defaults")) { - // TODO: Bitwise ? - nb = nb.setDefaults() + int[] defaultsArray = notification.getIntArray("defaults"); + int defaults = 0; + for (int d : defaultsArray) { + defaults |= d; + } + nb = nb.setDefaults(defaults); } if (notification.containsKey("group")) { nb = nb.setGroup(notification.getString("group")); @@ -183,12 +263,11 @@ public class RNFirebaseNotificationManager { Bundle progress = notification.getBundle("lights"); nb = nb.setProgress(progress.getInt("max"), progress.getInt("progress"), progress.getBoolean("indeterminate")); } - if (notification.containsKey("publicVersion")) { - // TODO: Build notification + // TODO: Public version of notification + /* if (notification.containsKey("publicVersion")) { nb = nb.setPublicVersion(); - } + } */ if (notification.containsKey("remoteInputHistory")) { - // TODO: Build notification nb = nb.setRemoteInputHistory(notification.getStringArray("remoteInputHistory")); } if (notification.containsKey("shortcutId")) { @@ -198,7 +277,18 @@ public class RNFirebaseNotificationManager { nb = nb.setShowWhen(notification.getBoolean("showWhen")); } if (notification.containsKey("smallIcon")) { - nb = nb.setSmallIcon(notification.getInt("smallIcon")); + Bundle smallIcon = notification.getBundle("smallIcon"); + int smallIconResourceId = getResourceId("mipmap", smallIcon.getString("icon")); + if (smallIconResourceId == 0) { + smallIconResourceId = getResourceId("drawable", smallIcon.getString("icon")); + } + if (smallIconResourceId != 0) { + if (smallIcon.containsKey("level")) { + nb = nb.setSmallIcon(smallIconResourceId, smallIcon.getInt("level")); + } else { + nb = nb.setSmallIcon(smallIconResourceId); + } + } } if (notification.containsKey("sortKey")) { nb = nb.setSortKey(notification.getString("sortKey")); @@ -221,148 +311,109 @@ public class RNFirebaseNotificationManager { if (notification.containsKey("when")) { nb = nb.setWhen(notification.getLong("when")); } - // TODO actions: Action[]; // icon, title, ??pendingIntent??, allowGeneratedReplies, extender, extras, remoteinput (ugh) - // TODO: style: Style; // Need to figure out if this can work - //icon - String smallIcon = bundle.getString("icon", "ic_launcher"); - int smallIconResId = res.getIdentifier(smallIcon, "mipmap", packageName); - notification.setSmallIcon(smallIconResId); - - //big text - String bigText = bundle.getString("big_text"); + // TODO: Big text / Big picture + /* String bigText = bundle.getString("big_text"); if(bigText != null){ notification.setStyle(new NotificationCompat.BigTextStyle().bigText(bigText)); } + String picture = bundle.getString("picture"); + if(picture!=null){ + NotificationCompat.BigPictureStyle bigPicture = new NotificationCompat.BigPictureStyle(); - //sound - String soundName = bundle.getString("sound", "default"); - if (!soundName.equalsIgnoreCase("default")) { - int soundResourceId = res.getIdentifier(soundName, "raw", packageName); - if(soundResourceId == 0){ - soundName = soundName.substring(0, soundName.lastIndexOf('.')); - soundResourceId = res.getIdentifier(soundName, "raw", packageName); + Bitmap pictureBitmap = getBitmap(picture); + if (pictureBitmap != null) { + bigPicture.bigPicture(pictureBitmap); } - notification.setSound(Uri.parse("android.resource://" + packageName + "/" + soundResourceId)); + bigPicture.setBigContentTitle(title); + bigPicture.setSummaryText(bundle.getString("body")); + + notification.setStyle(bigPicture); + } */ + + // Create the notification intent + Intent intent = new Intent(context, intentClass); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + intent.putExtras(notification); + if (notification.containsKey("clickAction")) { + intent.setAction(notification.getString("clickAction")); } - //color - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - String color = bundle.getString("color"); - if (color != null) { - notification.setColor(Color.parseColor(color)); - } - } + PendingIntent contentIntent = PendingIntent.getActivity(context, notificationId.hashCode(), intent, + PendingIntent.FLAG_UPDATE_CURRENT); + nb = nb.setContentIntent(contentIntent); - //lights - if (bundle.getBoolean("lights")) { - notification.setDefaults(NotificationCompat.DEFAULT_LIGHTS); - } - - Log.d(TAG, "broadcast intent before showing notification"); - Intent i = new Intent("io.invertase.firebase.messaging.ReceiveLocalNotification"); - i.putExtras(bundle); - mContext.sendOrderedBroadcast(i, null); - - if(!mIsForeground || bundle.getBoolean("show_in_foreground")){ - Intent intent = new Intent(mContext, intentClass); - intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); - intent.putExtras(bundle); - intent.setAction(bundle.getString("click_action")); - - int notificationID = bundle.containsKey("id") ? bundle.getString("id", "").hashCode() : (int) System.currentTimeMillis(); - PendingIntent pendingIntent = PendingIntent.getActivity(mContext, notificationID, intent, - PendingIntent.FLAG_UPDATE_CURRENT); - - NotificationManager notificationManager = - (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); - - notification.setContentIntent(pendingIntent); - - Notification info = notification.build(); - - if (bundle.containsKey("tag")) { - String tag = bundle.getString("tag"); - notificationManager.notify(tag, notificationID, info); - } else { - notificationManager.notify(notificationID, info); - } - } - //clear out one time scheduled notification once fired - if(!bundle.containsKey("repeat_interval") && bundle.containsKey("fire_date")) { - SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.remove(bundle.getString("id")); - editor.apply(); - } + // Build the notification and send it + Notification builtNotification = nb.build(); + notificationManager.notify(notificationId.hashCode(), builtNotification); } catch (Exception e) { - Log.e(TAG, "failed to send local notification", e); - } - } - - public ArrayList getScheduledNotifications(){ - ArrayList array = new ArrayList<>(); - - Map notifications = preferences.getAll(); - - for(String notificationId : notifications.keySet()){ - try { - JSONObject json = new JSONObject((String)notifications.get(notificationId)); - Bundle bundle = BundleJSONConverter.convertToBundle(json); - array.add(bundle); - } catch (JSONException e) { - Log.e(TAG, e.getMessage()); + if (promise == null) { + Log.e(TAG, "Failed to send notification", e); + } else { + promise.reject("notification/display_notification_error", "Could not send notification", e); } } - return array; } - public void removeAllDeliveredNotifications() { - notificationManager.cancelAll(); - } - - public void removeDeliveredNotification(String notificationId) { - notificationManager.cancel(notificationId.hashCode()); - } - - - public void rescheduleNotifications() { - ArrayList bundles = getScheduledNotifications(); - for(Bundle bundle: bundles){ - scheduleNotification(bundle, null); + private Bitmap getBitmap(String image) { + if (image.startsWith("http://") || image.startsWith("https://")) { + return getBitmapFromUrl(image); + } else { + int largeIconResId = getResourceId("mipmap", image); + return BitmapFactory.decodeResource(context.getResources(), largeIconResId); } } - public void scheduleNotification(ReadableMap notification, ReadableMap schedule, Promise promise) { - Bundle notificationBundle = Arguments.toBundle(notification); + private Bitmap getBitmapFromUrl(String imageUrl) { + try { + HttpURLConnection connection = (HttpURLConnection) new URL(imageUrl).openConnection(); + connection.setDoInput(true); + connection.connect(); + return BitmapFactory.decodeStream(connection.getInputStream()); + } catch (IOException e) { + Log.e(TAG, "Failed to get bitmap for url: " + imageUrl, e); + return null; + } + } - scheduleNotification(notificationBundle, promise); + private Class getMainActivityClass() { + String packageName = context.getPackageName(); + Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName); + try { + return Class.forName(launchIntent.getComponent().getClassName()); + } catch (ClassNotFoundException e) { + Log.e(TAG, "Failed to get main activity class", e); + return null; + } + } + + private int getResourceId(String type, String image) { + return context.getResources().getIdentifier(image, type, context.getPackageName()); } private void scheduleNotification(Bundle notification, Promise promise) { - // TODO - String notificationId = notification.getString("notificationId"); if (!notification.containsKey("notificationId")) { - if (promise != null) { - promise.reject("notification/schedule_notification_error", "Missing notificationId"); - } else { + if (promise == null) { Log.e(TAG, "Missing notificationId"); + } else { + promise.reject("notification/schedule_notification_error", "Missing notificationId"); } return; } - // TODO: Schedule check - if (!notification.hasKey("schedule")) { - + if (!notification.containsKey("schedule")) { + if (promise == null) { + Log.e(TAG, "Missing schedule information"); + } else { + promise.reject("notification/schedule_notification_error", "Missing schedule information"); + } return; } - /* - Long fireDate = Math.round(bundle.getDouble("fire_date")); - if (fireDate == 0) { - Log.e(TAG, "failed to schedule notification because fire date is missing"); - return; - }*/ + String notificationId = notification.getString("notificationId"); + Bundle schedule = notification.getBundle("schedule"); + long fireDate = schedule.getLong("fireDate"); // Scheduled alarms are cleared on restart // We store them so that they can be re-scheduled when the phone restarts in RNFirebaseNotificationsRebootReceiver @@ -374,61 +425,47 @@ public class RNFirebaseNotificationManager { return; } - Intent notificationIntent = new Intent(context, RNFirebaseNotificationReceiver.class); notificationIntent.putExtras(notification); - PendingIntent pendingIntent = PendingIntent.getBroadcast(context, notificationId.hashCode(), notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, notificationId.hashCode(), + notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); - // TODO: Scheduling - Long interval = null; - switch (notification.getString("repeat_interval", "")) { - case "minute": - interval = (long) 60000; - break; - case "hour": - interval = AlarmManager.INTERVAL_HOUR; - break; - case "day": - interval = AlarmManager.INTERVAL_DAY; - break; - case "week": - interval = AlarmManager.INTERVAL_DAY * 7; - break; - } + if (schedule.containsKey("interval")) { + Long interval = null; + switch (schedule.getString("interval")) { + case "minute": + interval = 60000L; + break; + case "hour": + interval = AlarmManager.INTERVAL_HOUR; + break; + case "day": + interval = AlarmManager.INTERVAL_DAY; + break; + case "week": + interval = AlarmManager.INTERVAL_DAY * 7; + break; + default: + Log.e(TAG, "Invalid interval: " + schedule.getString("interval")); + break; + } + + if (interval == null) { + promise.reject("notification/schedule_notification_error", "Invalid interval"); + return; + } - if(interval != null){ alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, fireDate, interval, pendingIntent); - } else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){ - alarmManager.setExact(AlarmManager.RTC_WAKEUP, fireDate, pendingIntent); - }else { - alarmManager.set(AlarmManager.RTC_WAKEUP, fireDate, pendingIntent); - } - } - - private void cancelAlarm(String notificationId) { - Intent notificationIntent = new Intent(context, RNFirebaseNotificationManager.class); - PendingIntent pendingIntent = PendingIntent.getBroadcast(context, notificationId.hashCode(), notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); - alarmManager.cancel(pendingIntent); - } - - private Bitmap getBitmap(String image) { - if (image.startsWith("http://") || image.startsWith("https://")) { - return getBitmapFromUrl(image); } else { - int largeIconResId = res.getIdentifier(image, "mipmap", packageName); - return BitmapFactory.decodeResource(res, largeIconResId); + if (schedule.containsKey("exact") + && schedule.getBoolean("exact") + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + alarmManager.setExact(AlarmManager.RTC_WAKEUP, fireDate, pendingIntent); + } else { + alarmManager.set(AlarmManager.RTC_WAKEUP, fireDate, pendingIntent); + } } - } - private Bitmap getBitmapFromUrl(String imageUrl) { - try { - HttpURLConnection connection = (HttpURLConnection) new URL(imageUrl).openConnection(); - connection.setDoInput(true); - connection.connect(); - return BitmapFactory.decodeStream(connection.getInputStream()); - } catch (IOException e) { - Log.e(TAG, e.getMessage()); - return null; - } + promise.resolve(null); } } diff --git a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationReceiver.java b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationReceiver.java index 50ff4133..903daf65 100644 --- a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationReceiver.java +++ b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationReceiver.java @@ -10,6 +10,6 @@ import android.content.Intent; public class RNFirebaseNotificationReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - new RNFirebaseNotificationManager(context).displayNotification(intent.getExtras()); + new RNFirebaseNotificationManager(context).displayScheduledNotification(intent.getExtras()); } } diff --git a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java index 230b486a..7e20f365 100644 --- a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java +++ b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java @@ -1,24 +1,41 @@ package io.invertase.firebase.notifications; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.os.Bundle; -import android.support.v4.app.NotificationCompat; +import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; import java.util.ArrayList; -public class RNFirebaseNotifications extends ReactContextBaseJavaModule { +import io.invertase.firebase.Utils; + +public class RNFirebaseNotifications extends ReactContextBaseJavaModule implements LifecycleEventListener { + private static final String TAG = "RNFirebaseNotifications"; + private RNFirebaseNotificationManager notificationManager; public RNFirebaseNotifications(ReactApplicationContext context) { super(context); notificationManager = new RNFirebaseNotificationManager(context.getApplicationContext()); + context.addLifecycleEventListener(this); + + LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(context); + + // Subscribe to scheduled notification events + localBroadcastManager.registerReceiver(new ScheduledNotificationReceiver(), + new IntentFilter(RNFirebaseNotificationManager.SCHEDULED_NOTIFICATION_EVENT)); } @Override @@ -67,7 +84,46 @@ public class RNFirebaseNotifications extends ReactContextBaseJavaModule { } @ReactMethod - public void scheduleNotification(ReadableMap notification, ReadableMap schedule, Promise promise) { - notificationManager.scheduleNotification(notification, schedule, promise); + public void scheduleNotification(ReadableMap notification, Promise promise) { + notificationManager.scheduleNotification(notification, promise); + } + + ////////////////////////////////////////////////////////////////////// + // Start LifecycleEventListener methods + ////////////////////////////////////////////////////////////////////// + @Override + public void onHostResume() { + notificationManager.setIsForeground(true); + } + + @Override + public void onHostPause() { + notificationManager.setIsForeground(false); + } + + @Override + public void onHostDestroy() { + // Do nothing + } + ////////////////////////////////////////////////////////////////////// + // End LifecycleEventListener methods + ////////////////////////////////////////////////////////////////////// + + private WritableMap parseNotificationBundle(Bundle notification) { + return Arguments.makeNativeMap(notification); + } + + private class ScheduledNotificationReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (getReactApplicationContext().hasActiveCatalystInstance()) { + Log.d(TAG, "Received new scheduled notification"); + + Bundle notification = intent.getBundleExtra("notification"); + WritableMap messageMap = parseNotificationBundle(notification); + + Utils.sendEvent(getReactApplicationContext(), "notifications_notification_received", messageMap); + } + } } } diff --git a/ios/RNFirebase/notifications/RNFirebaseNotifications.m b/ios/RNFirebase/notifications/RNFirebaseNotifications.m index 895c559e..12456a63 100644 --- a/ios/RNFirebase/notifications/RNFirebaseNotifications.m +++ b/ios/RNFirebase/notifications/RNFirebaseNotifications.m @@ -27,7 +27,6 @@ RCT_EXPORT_METHOD(cancelNotification:(NSString*) notificationId) { if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { for (UILocalNotification *notification in RCTSharedApplication().scheduledLocalNotifications) { NSDictionary *notificationInfo = notification.userInfo; - // TODO: NotificationId? if ([notificationId isEqualToString:[notificationInfo valueForKey:@"notificationId"]]) { [RCTSharedApplication() cancelLocalNotification:notification]; } @@ -46,12 +45,12 @@ RCT_EXPORT_METHOD(displayNotification:(NSDictionary*) notification resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { - UILocalNotification* notif = [self buildUILocalNotification:notification]; + UILocalNotification* notif = [self buildUILocalNotification:notification withSchedule:false]; [RCTSharedApplication() presentLocalNotificationNow:notif]; resolve(nil); } else { #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 - UNNotificationRequest* request = [self buildUNNotificationRequest:notification]; + UNNotificationRequest* request = [self buildUNNotificationRequest:notification withSchedule:false]; [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) { if (!error) { resolve(nil); @@ -123,18 +122,15 @@ RCT_EXPORT_METHOD(removeDeliveredNotification:(NSString*) notificationId) { } RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification - schedule:(NSDictionary*) schedule resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max) { - UILocalNotification* notif = [self buildUILocalNotification:notification]; - // TODO: Schedule + UILocalNotification* notif = [self buildUILocalNotification:notification withSchedule:true]; [RCTSharedApplication() scheduleLocalNotification:notif]; resolve(nil); } else { #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 - UNNotificationRequest* request = [self buildUNNotificationRequest:notification]; - // TODO: Schedule + UNNotificationRequest* request = [self buildUNNotificationRequest:notification withSchedule:true]; [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) { if (!error) { resolve(nil); @@ -146,7 +142,8 @@ RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification } } -- (UILocalNotification*) buildUILocalNotification:(NSDictionary *) notification { +- (UILocalNotification*) buildUILocalNotification:(NSDictionary *) notification + withSchedule:(BOOL) withSchedule { UILocalNotification *localNotification = [[UILocalNotification alloc] init]; if (notification[@"body"]) { localNotification.alertBody = notification[@"body"]; @@ -179,11 +176,32 @@ RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification localNotification.alertLaunchImage = ios[@"launchImage"]; } } - + if (withSchedule) { + NSDictionary *schedule = notification[@"schedule"]; + NSNumber *fireDateNumber = schedule[@"fireDate"]; + NSDate *fireDate = [NSDate dateWithTimeIntervalSince1970:([fireDateNumber doubleValue] / 1000.0)]; + localNotification.fireDate = fireDate; + + NSString *interval = schedule[@"repeatInterval"]; + if (interval) { + if ([interval isEqualToString:@"minute"]) { + localNotification.repeatInterval = NSCalendarUnitMinute; + } else if ([interval isEqualToString:@"hour"]) { + localNotification.repeatInterval = NSCalendarUnitHour; + } else if ([interval isEqualToString:@"day"]) { + localNotification.repeatInterval = NSCalendarUnitDay; + } else if ([interval isEqualToString:@"week"]) { + localNotification.repeatInterval = NSCalendarUnitWeekday; + } + } + + } + return localNotification; } -- (UNNotificationRequest*) buildUNNotificationRequest:(NSDictionary *) notification { +- (UNNotificationRequest*) buildUNNotificationRequest:(NSDictionary *) notification + withSchedule:(BOOL) withSchedule { UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init]; if (notification[@"body"]) { content.body = notification[@"body"]; @@ -234,8 +252,34 @@ RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification } } - // TODO: Scheduling - return [UNNotificationRequest requestWithIdentifier:notification[@"ios"][@"identifier"] content:content trigger:nil]; + if (withSchedule) { + NSDictionary *schedule = notification[@"schedule"]; + NSNumber *fireDateNumber = schedule[@"fireDate"]; + NSString *interval = schedule[@"repeatInterval"]; + NSDate *fireDate = [NSDate dateWithTimeIntervalSince1970:([fireDateNumber doubleValue] / 1000.0)]; + + NSCalendarUnit calendarUnit; + if (interval) { + if ([interval isEqualToString:@"minute"]) { + calendarUnit = NSCalendarUnitSecond; + } else if ([interval isEqualToString:@"hour"]) { + calendarUnit = NSCalendarUnitMinute | NSCalendarUnitSecond; + } else if ([interval isEqualToString:@"day"]) { + calendarUnit = NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond; + } else if ([interval isEqualToString:@"week"]) { + calendarUnit = NSCalendarUnitWeekday | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond; + } + } else { + // Needs to match exactly to the secpmd + calendarUnit = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond; + } + + NSDateComponents *components = [[NSCalendar currentCalendar] components:calendarUnit fromDate:fireDate]; + UNCalendarNotificationTrigger *trigger = [UNCalendarNotificationTrigger triggerWithDateMatchingComponents:components repeats:interval]; + return [UNNotificationRequest requestWithIdentifier:notification[@"notificationId"] content:content trigger:trigger]; + } else { + return [UNNotificationRequest requestWithIdentifier:notification[@"notificationId"] content:content trigger:nil]; + } } - (NSDictionary*) parseUILocalNotification:(UILocalNotification *) localNotification { @@ -278,7 +322,7 @@ RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification - (NSDictionary*) parseUNNotificationRequest:(UNNotificationRequest *) localNotification { NSMutableDictionary *notification = [[NSMutableDictionary alloc] init]; - notification[@"identifier"] = localNotification.identifier; + notification[@"notificationId"] = localNotification.identifier; if (localNotification.content.body) { notification[@"body"] = localNotification.content.body; diff --git a/lib/modules/instanceid/index.js b/lib/modules/instanceid/index.js index d4f6ad68..3924a0a9 100644 --- a/lib/modules/instanceid/index.js +++ b/lib/modules/instanceid/index.js @@ -5,7 +5,7 @@ import ModuleBase from '../../utils/ModuleBase'; import { getNativeModule } from '../../utils/native'; -import type App from '../core/firebase-app'; +import type App from '../core/app'; export const MODULE_NAME = 'RNFirebaseInstanceId'; export const NAMESPACE = 'instanceid'; diff --git a/lib/modules/notifications/AndroidNotification.js b/lib/modules/notifications/AndroidNotification.js index 104e13da..f2591707 100644 --- a/lib/modules/notifications/AndroidNotification.js +++ b/lib/modules/notifications/AndroidNotification.js @@ -17,17 +17,18 @@ type Progress = { }; type SmallIcon = { - icon: number, + icon: string, level?: number, }; -export type NativeAndroidNotification = { +export type NativeAndroidNotification = {| // TODO actions: Action[], autoCancel: boolean, badgeIconType: BadgeIconTypeType, category: CategoryType, channelId: string, - color: number, + clickAction?: string, + color: string, colorized: boolean, contentInfo: string, defaults: DefaultsType[], @@ -43,7 +44,7 @@ export type NativeAndroidNotification = { people: string[], priority: PriorityType, progress: Progress, - publicVersion: Notification, + // publicVersion: Notification, remoteInputHistory: string[], shortcutId: string, showWhen: boolean, @@ -56,7 +57,7 @@ export type NativeAndroidNotification = { vibrate: number[], visibility: VisibilityType, when: number, -}; +|}; export const BadgeIconType = { Large: 2, @@ -122,7 +123,8 @@ export default class AndroidNotification { _badgeIconType: BadgeIconTypeType; _category: CategoryType; _channelId: string; - _color: number; + _clickAction: string; + _color: string; _colorized: boolean; _contentInfo: string; _defaults: DefaultsType[]; @@ -139,11 +141,13 @@ export default class AndroidNotification { _people: string[]; _priority: PriorityType; _progress: Progress; - _publicVersion: Notification; + // _publicVersion: Notification; _remoteInputHistory: string[]; _shortcutId: string; _showWhen: boolean; - _smallIcon: SmallIcon; + _smallIcon: SmallIcon = { + icon: 'ic_launcher', + }; _sortKey: string; // TODO: style: Style; // Need to figure out if this can work _ticker: string; @@ -170,9 +174,7 @@ export default class AndroidNotification { /** * - * @param identifier - * @param identifier - * @param identifier + * @param person * @returns {Notification} */ addPerson(person: string): Notification { @@ -228,7 +230,7 @@ export default class AndroidNotification { * @param color * @returns {Notification} */ - setColor(color: number): Notification { + setColor(color: string): Notification { this._color = color; return this._notification; } @@ -394,10 +396,10 @@ export default class AndroidNotification { * @param publicVersion * @returns {Notification} */ - setPublicVersion(publicVersion: Notification): Notification { + /* setPublicVersion(publicVersion: Notification): Notification { this._publicVersion = publicVersion; return this._notification; - } + } */ /** * @@ -435,7 +437,7 @@ export default class AndroidNotification { * @param level * @returns {Notification} */ - setSmallIcon(icon: number, level?: number): Notification { + setSmallIcon(icon: string, level?: number): Notification { this._smallIcon = { icon, level, @@ -517,6 +519,7 @@ export default class AndroidNotification { badgeIconType: this._badgeIconType, category: this._category, channelId: this._channelId, + clickAction: this._clickAction, color: this._color, colorized: this._colorized, contentInfo: this._contentInfo, @@ -533,7 +536,7 @@ export default class AndroidNotification { people: this._people, priority: this._priority, progress: this._progress, - publicVersion: this._publicVersion, + // publicVersion: this._publicVersion, remoteInputHistory: this._remoteInputHistory, shortcutId: this._shortcutId, showWhen: this._showWhen, diff --git a/lib/modules/notifications/IOSNotification.js b/lib/modules/notifications/IOSNotification.js index 607518e7..d4ad7e11 100644 --- a/lib/modules/notifications/IOSNotification.js +++ b/lib/modules/notifications/IOSNotification.js @@ -2,7 +2,6 @@ * @flow * IOSNotification representation wrapper */ -import { generatePushID } from '../../utils'; import type Notification from './Notification'; type AttachmentOptions = {| @@ -23,16 +22,15 @@ type Attachment = {| url: string, |}; -export type NativeIOSNotification = { +export type NativeIOSNotification = {| alertAction?: string, attachments: Attachment[], badge?: number, category?: string, hasAction?: boolean, - identifier?: string, launchImage?: string, threadIdentifier?: string, -}; +|}; export default class IOSNotification { _alertAction: string; // alertAction | N/A @@ -40,23 +38,20 @@ export default class IOSNotification { _badge: number; // applicationIconBadgeNumber | badge _category: string; _hasAction: boolean; // hasAction | N/A - _identifier: string; // N/A | identifier _launchImage: string; // alertLaunchImage | launchImageName _notification: Notification; _threadIdentifier: string; // N/A | threadIdentifier constructor(notification: Notification) { this._attachments = []; - // TODO: Is this the best way to generate an ID? - this._identifier = generatePushID(); this._notification = notification; } /** * * @param identifier - * @param identifier - * @param identifier + * @param url + * @param options * @returns {Notification} */ addAttachment( @@ -112,16 +107,6 @@ export default class IOSNotification { return this._notification; } - /** - * - * @param identifier - * @returns {Notification} - */ - setIdentifier(identifier: string): Notification { - this._identifier = identifier; - return this._notification; - } - /** * * @param launchImage @@ -151,7 +136,6 @@ export default class IOSNotification { badge: this._badge, category: this._category, hasAction: this._hasAction, - identifier: this._identifier, launchImage: this._launchImage, threadIdentifier: this._threadIdentifier, }; diff --git a/lib/modules/notifications/Notification.js b/lib/modules/notifications/Notification.js index ab7dbc6c..88389505 100644 --- a/lib/modules/notifications/Notification.js +++ b/lib/modules/notifications/Notification.js @@ -4,16 +4,19 @@ */ import AndroidNotification from './AndroidNotification'; import IOSNotification from './IOSNotification'; -import { isObject } from '../../utils'; +import { generatePushID, isObject } from '../../utils'; import type { NativeAndroidNotification } from './AndroidNotification'; import type { NativeIOSNotification } from './IOSNotification'; +import type { Schedule } from './'; type NativeNotification = {| android: NativeAndroidNotification, body: string, data: { [string]: string }, ios: NativeIOSNotification, + notificationId: string, + schedule?: Schedule, sound?: string, subtitle?: string, title: string, @@ -25,6 +28,7 @@ export default class Notification { _body: string; // alertBody | body | contentText _data: { [string]: string }; // userInfo | userInfo | extras _ios: IOSNotification; + _notificationId: string; _sound: string | void; // soundName | sound | sound _subtitle: string | void; // N/A | subtitle | subText _title: string; // alertTitle | title | contentTitle @@ -33,6 +37,8 @@ export default class Notification { this._android = new AndroidNotification(this); this._data = {}; this._ios = new IOSNotification(this); + // TODO: Is this the best way to generate an ID? + this._notificationId = generatePushID(); } get android(): AndroidNotification { @@ -68,6 +74,16 @@ export default class Notification { return this; } + /** + * + * @param notificationId + * @returns {Notification} + */ + setNotificationId(notificationId: string): Notification { + this._notificationId = notificationId; + return this; + } + /** * * @param sound @@ -101,9 +117,13 @@ export default class Notification { build(): NativeNotification { // Android required fields: body, title, smallicon // iOS required fields: TODO - if (!this.body) { + if (!this._body) { throw new Error('Notification: Missing required `body` property'); - } else if (!this.title) { + } else if (!this._notificationId) { + throw new Error( + 'Notification: Missing required `notificationId` property' + ); + } else if (!this._title) { throw new Error('Notification: Missing required `title` property'); } @@ -112,6 +132,7 @@ export default class Notification { body: this._body, data: this._data, ios: this._ios.build(), + notificationId: this._notificationId, sound: this._sound, subtitle: this._subtitle, title: this._title, diff --git a/lib/modules/notifications/index.js b/lib/modules/notifications/index.js index c0cda1b5..17857fbd 100644 --- a/lib/modules/notifications/index.js +++ b/lib/modules/notifications/index.js @@ -17,7 +17,7 @@ import { Visibility, } from './AndroidNotification'; -import type App from '../core/firebase-app'; +import type App from '../core/app'; // TODO: Received notification type will be different from sent notification type OnNotification = Notification => any; @@ -26,9 +26,10 @@ type OnNotificationObserver = { next: OnNotification, }; -// TODO: Schedule type -type Schedule = { - build: () => Object, +export type Schedule = { + exact?: boolean, + fireDate: number, + repeatInterval?: 'minute' | 'hour' | 'day' | 'week', }; const NATIVE_EVENTS = ['notifications_notification_received']; @@ -174,10 +175,9 @@ export default class Notifications extends ModuleBase { `Notifications:scheduleNotification expects a 'Notification' but got type ${typeof notification}` ); } - return getNativeModule(this).scheduleNotification( - notification.build(), - schedule.build() - ); + const nativeNotification = notification.build(); + nativeNotification.schedule = schedule; + return getNativeModule(this).scheduleNotification(nativeNotification); } } diff --git a/tests/android/app/build.gradle b/tests/android/app/build.gradle index 39b6b646..c7b38a71 100644 --- a/tests/android/app/build.gradle +++ b/tests/android/app/build.gradle @@ -98,7 +98,7 @@ def enableProguardInReleaseBuilds = false android { compileSdkVersion 26 - buildToolsVersion '25.0.3' + buildToolsVersion '26.0.2' defaultConfig { applicationId "com.reactnativefirebasedemo" @@ -163,7 +163,7 @@ dependencies { compile('com.crashlytics.sdk.android:crashlytics:2.7.1@aar') { transitive = true } - compile "com.android.support:appcompat-v7:26.0.1" + compile "com.android.support:appcompat-v7:26.0.2" compile "com.facebook.react:react-native:+" // From node_modules } diff --git a/tests/android/build.gradle b/tests/android/build.gradle index 2043c3f7..7c902675 100644 --- a/tests/android/build.gradle +++ b/tests/android/build.gradle @@ -29,10 +29,10 @@ allprojects { subprojects { ext { - compileSdk = 25 - buildTools = "25.0.2" + compileSdk = 26 + buildTools = "26.0.2" minSdk = 16 - targetSdk = 25 + targetSdk = 26 } afterEvaluate { project -> diff --git a/tests/ios/Podfile.lock b/tests/ios/Podfile.lock index 22babbd1..884f82d5 100644 --- a/tests/ios/Podfile.lock +++ b/tests/ios/Podfile.lock @@ -164,7 +164,7 @@ PODS: - React/Core - React/fishhook - React/RCTBlob - - RNFirebase (3.2.4): + - RNFirebase (3.2.5): - React - yoga (0.52.0.React) @@ -228,7 +228,7 @@ SPEC CHECKSUMS: nanopb: 5601e6bca2dbf1ed831b519092ec110f66982ca3 Protobuf: 8a9838fba8dae3389230e1b7f8c104aa32389c03 React: 61a6bdf17a9ff16875c230e6ff278d9de274e16c - RNFirebase: 011e47909cf54070f72d50b8d61eb7b347774d29 + RNFirebase: e3448c730955d51d06dee59a265011536abdd7c4 yoga: 646606bf554d54a16711f35596178522fbc00480 PODFILE CHECKSUM: 67c98bcb203cb992da590bcab6f690f727653ca5