package com.dieam.reactnativepushnotification.modules; 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; import android.media.RingtoneManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.support.v4.app.NotificationCompat; import android.util.Log; import com.facebook.react.bridge.ReadableMap; import org.json.JSONArray; import org.json.JSONException; import java.util.Arrays; import static com.dieam.reactnativepushnotification.modules.RNPushNotification.LOG_TAG; import static com.dieam.reactnativepushnotification.modules.RNPushNotificationAttributes.fromJson; public class RNPushNotificationHelper { public static final String PREFERENCES_KEY = "rn_push_notification"; private static final long DEFAULT_VIBRATION = 300L; private Context context; private final SharedPreferences scheduledNotificationsPersistence; private static final int ONE_MINUTE = 60 * 1000; private static final long ONE_HOUR = 60 * ONE_MINUTE; private static final long ONE_DAY = 24 * ONE_HOUR; public RNPushNotificationHelper(Application context) { this.context = context; this.scheduledNotificationsPersistence = context.getSharedPreferences(RNPushNotificationHelper.PREFERENCES_KEY, Context.MODE_PRIVATE); } public Class getMainActivityClass() { String packageName = context.getPackageName(); Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName); String className = launchIntent.getComponent().getClassName(); try { return Class.forName(className); } catch (ClassNotFoundException e) { e.printStackTrace(); return null; } } private AlarmManager getAlarmManager() { return (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); } private PendingIntent toScheduleNotificationIntent(Bundle bundle) { int notificationID = Integer.parseInt(bundle.getString("id")); Intent notificationIntent = new Intent(context, RNPushNotificationPublisher.class); notificationIntent.putExtra(RNPushNotificationPublisher.NOTIFICATION_ID, notificationID); notificationIntent.putExtras(bundle); return PendingIntent.getBroadcast(context, notificationID, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); } public void sendNotificationScheduled(Bundle bundle) { Class intentClass = getMainActivityClass(); if (intentClass == null) { Log.e(LOG_TAG, "No activity class found for the scheduled notification"); return; } if (bundle.getString("message") == null) { Log.e(LOG_TAG, "No message specified for the scheduled notification"); return; } if (bundle.getString("id") == null) { Log.e(LOG_TAG, "No notification ID specified for the scheduled notification"); return; } double fireDate = bundle.getDouble("fireDate"); if (fireDate == 0) { Log.e(LOG_TAG, "No date specified for the scheduled notification"); return; } RNPushNotificationAttributes notificationAttributes = new RNPushNotificationAttributes(bundle); String id = notificationAttributes.getId(); Log.d(LOG_TAG, "Storing push notification with id " + id); SharedPreferences.Editor editor = scheduledNotificationsPersistence.edit(); editor.putString(id, notificationAttributes.toJson().toString()); commit(editor); boolean isSaved = scheduledNotificationsPersistence.contains(id); if (!isSaved) { Log.e(LOG_TAG, "Failed to save " + id); } sendNotificationScheduledCore(bundle); } public void sendNotificationScheduledCore(Bundle bundle) { long fireDate = (long) bundle.getDouble("fireDate"); // If the fireDate is in past, this will fire immediately and show the // notification to the user PendingIntent pendingIntent = toScheduleNotificationIntent(bundle); Log.d(LOG_TAG, String.format("Setting a notification with id %s at time %s", bundle.getString("id"), Long.toString(fireDate))); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { getAlarmManager().setExact(AlarmManager.RTC_WAKEUP, fireDate, pendingIntent); } else { getAlarmManager().set(AlarmManager.RTC_WAKEUP, fireDate, pendingIntent); } } public void sendToNotificationCentre(Bundle bundle) { try { Class intentClass = getMainActivityClass(); if (intentClass == null) { Log.e(LOG_TAG, "No activity class found for the notification"); return; } if (bundle.getString("message") == null) { // this happens when a 'data' notification is received - we do not synthesize a local notification in this case Log.d(LOG_TAG, "Cannot send to notification centre because there is no 'message' field in: " + bundle); return; } String notificationIdString = bundle.getString("id"); if (notificationIdString == null) { Log.e(LOG_TAG, "No notification ID specified for the notification"); return; } Resources res = context.getResources(); String packageName = context.getPackageName(); String title = bundle.getString("title"); if (title == null) { ApplicationInfo appInfo = context.getApplicationInfo(); title = context.getPackageManager().getApplicationLabel(appInfo).toString(); } NotificationCompat.Builder notification = new NotificationCompat.Builder(context) .setContentTitle(title) .setTicker(bundle.getString("ticker")) .setVisibility(NotificationCompat.VISIBILITY_PRIVATE) .setPriority(NotificationCompat.PRIORITY_HIGH) .setAutoCancel(bundle.getBoolean("autoCancel", true)); String group = bundle.getString("group"); if (group != null) { notification.setGroup(group); } notification.setContentText(bundle.getString("message")); String largeIcon = bundle.getString("largeIcon"); String subText = bundle.getString("subText"); if (subText != null) { notification.setSubText(subText); } String numberString = bundle.getString("number"); if (numberString != null) { notification.setNumber(Integer.parseInt(numberString)); } int smallIconResId; int largeIconResId; String smallIcon = bundle.getString("smallIcon"); if (smallIcon != null) { smallIconResId = res.getIdentifier(smallIcon, "mipmap", packageName); } else { smallIconResId = res.getIdentifier("ic_notification", "mipmap", packageName); } if (smallIconResId == 0) { smallIconResId = res.getIdentifier("ic_launcher", "mipmap", packageName); if (smallIconResId == 0) { smallIconResId = android.R.drawable.ic_dialog_info; } } if (largeIcon != null) { largeIconResId = res.getIdentifier(largeIcon, "mipmap", packageName); } else { largeIconResId = res.getIdentifier("ic_launcher", "mipmap", packageName); } Bitmap largeIconBitmap = BitmapFactory.decodeResource(res, largeIconResId); if (largeIconResId != 0 && (largeIcon != null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)) { notification.setLargeIcon(largeIconBitmap); } notification.setSmallIcon(smallIconResId); String bigText = bundle.getString("bigText"); if (bigText == null) { bigText = bundle.getString("message"); } notification.setStyle(new NotificationCompat.BigTextStyle().bigText(bigText)); Intent intent = new Intent(context, intentClass); intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); bundle.putBoolean("userInteraction", true); intent.putExtra("notification", bundle); if (!bundle.containsKey("playSound") || bundle.getBoolean("playSound")) { Uri soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); String soundName = bundle.getString("soundName"); if (soundName != null) { if (!"default".equalsIgnoreCase(soundName)) { // sound name can be full filename, or just the resource name. // So the strings 'my_sound.mp3' AND 'my_sound' are accepted // The reason is to make the iOS and android javascript interfaces compatible int resId; if (context.getResources().getIdentifier(soundName, "raw", context.getPackageName()) != 0) { resId = context.getResources().getIdentifier(soundName, "raw", context.getPackageName()); } else { soundName = soundName.substring(0, soundName.lastIndexOf('.')); resId = context.getResources().getIdentifier(soundName, "raw", context.getPackageName()); } soundUri = Uri.parse("android.resource://" + context.getPackageName() + "/" + resId); } } notification.setSound(soundUri); } if (bundle.containsKey("ongoing") || bundle.getBoolean("ongoing")) { notification.setOngoing(bundle.getBoolean("ongoing")); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { notification.setCategory(NotificationCompat.CATEGORY_CALL); String color = bundle.getString("color"); if (color != null) { notification.setColor(Color.parseColor(color)); } } int notificationID = Integer.parseInt(notificationIdString); PendingIntent pendingIntent = PendingIntent.getActivity(context, notificationID, intent, PendingIntent.FLAG_UPDATE_CURRENT); NotificationManager notificationManager = notificationManager(); notification.setContentIntent(pendingIntent); if (!bundle.containsKey("vibrate") || bundle.getBoolean("vibrate")) { long vibration = bundle.containsKey("vibration") ? (long) bundle.getDouble("vibration") : DEFAULT_VIBRATION; if (vibration == 0) vibration = DEFAULT_VIBRATION; notification.setVibrate(new long[]{0, vibration}); } JSONArray actionsArray = null; try { actionsArray = bundle.getString("actions") != null ? new JSONArray(bundle.getString("actions")) : null; } catch (JSONException e) { Log.e(LOG_TAG, "Exception while converting actions to JSON object.", e); } if (actionsArray != null) { // No icon for now. The icon value of 0 shows no icon. int icon = 0; // Add button for each actions. for (int i = 0; i < actionsArray.length(); i++) { String action; try { action = actionsArray.getString(i); } catch (JSONException e) { Log.e(LOG_TAG, "Exception while getting action from actionsArray.", e); continue; } Intent actionIntent = new Intent(); actionIntent.setAction(context.getPackageName() + "." + action); // Add "action" for later identifying which button gets pressed. bundle.putString("action", action); actionIntent.putExtra("notification", bundle); PendingIntent pendingActionIntent = PendingIntent.getBroadcast(context, notificationID, actionIntent, PendingIntent.FLAG_UPDATE_CURRENT); notification.addAction(icon, action, pendingActionIntent); } } // Remove the notification from the shared preferences once it has been shown // to avoid showing the notification again when the phone is rebooted. If the // notification is not removed, then every time the phone is rebooted, we will // try to reschedule all the notifications stored in shared preferences and since // these notifications will be in the past time, they will be shown immediately // to the user which we shouldn't do. So, remove the notification from the shared // preferences once it has been shown to the user. If it is a repeating notification // it will be scheduled again. if (scheduledNotificationsPersistence.getString(notificationIdString, null) != null) { SharedPreferences.Editor editor = scheduledNotificationsPersistence.edit(); editor.remove(notificationIdString); commit(editor); } Notification info = notification.build(); info.defaults |= Notification.DEFAULT_LIGHTS; if (bundle.containsKey("tag")) { String tag = bundle.getString("tag"); notificationManager.notify(tag, notificationID, info); } else { notificationManager.notify(notificationID, info); } // Can't use setRepeating for recurring notifications because setRepeating // is inexact by default starting API 19 and the notifications are not fired // at the exact time. During testing, it was found that notifications could // late by many minutes. this.scheduleNextNotificationIfRepeating(bundle); } catch (Exception e) { Log.e(LOG_TAG, "failed to send push notification", e); } } private void scheduleNextNotificationIfRepeating(Bundle bundle) { String repeatType = bundle.getString("repeatType"); long repeatTime = (long) bundle.getDouble("repeatTime"); if (repeatType != null) { long fireDate = (long) bundle.getDouble("fireDate"); boolean validRepeatType = Arrays.asList("time", "week", "day", "hour", "minute").contains(repeatType); // Sanity checks if (!validRepeatType) { Log.w(LOG_TAG, String.format("Invalid repeatType specified as %s", repeatType)); return; } if ("time".equals(repeatType) && repeatTime <= 0) { Log.w(LOG_TAG, "repeatType specified as time but no repeatTime " + "has been mentioned"); return; } long newFireDate = 0; switch (repeatType) { case "time": newFireDate = fireDate + repeatTime; break; case "week": newFireDate = fireDate + 7 * ONE_DAY; break; case "day": newFireDate = fireDate + ONE_DAY; break; case "hour": newFireDate = fireDate + ONE_HOUR; break; case "minute": newFireDate = fireDate + ONE_MINUTE; break; } // Sanity check, should never happen if (newFireDate != 0) { Log.d(LOG_TAG, String.format("Repeating notification with id %s at time %s", bundle.getString("id"), Long.toString(newFireDate))); bundle.putDouble("fireDate", newFireDate); this.sendNotificationScheduled(bundle); } } } public void clearNotifications() { Log.i(LOG_TAG, "Clearing alerts from the notification centre"); NotificationManager notificationManager = notificationManager(); notificationManager.cancelAll(); } public void cancelAllScheduledNotifications() { Log.i(LOG_TAG, "Cancelling all notifications"); for (String id : scheduledNotificationsPersistence.getAll().keySet()) { cancelScheduledNotification(id); } } public void cancelScheduledNotification(ReadableMap userInfo) { for (String id : scheduledNotificationsPersistence.getAll().keySet()) { try { String notificationAttributesJson = scheduledNotificationsPersistence.getString(id, null); if (notificationAttributesJson != null) { RNPushNotificationAttributes notificationAttributes = fromJson(notificationAttributesJson); if (notificationAttributes.matches(userInfo)) { cancelScheduledNotification(id); } } } catch (JSONException e) { Log.w(LOG_TAG, "Problem dealing with scheduled notification " + id, e); } } } private void cancelScheduledNotification(String notificationIDString) { Log.i(LOG_TAG, "Cancelling notification: " + notificationIDString); // remove it from the alarm manger schedule Bundle b = new Bundle(); b.putString("id", notificationIDString); getAlarmManager().cancel(toScheduleNotificationIntent(b)); if (scheduledNotificationsPersistence.contains(notificationIDString)) { // remove it from local storage SharedPreferences.Editor editor = scheduledNotificationsPersistence.edit(); editor.remove(notificationIDString); commit(editor); } else { Log.w(LOG_TAG, "Unable to find notification " + notificationIDString); } // removed it from the notification center NotificationManager notificationManager = notificationManager(); notificationManager.cancel(Integer.parseInt(notificationIDString)); } private NotificationManager notificationManager() { return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); } private static void commit(SharedPreferences.Editor editor) { if (Build.VERSION.SDK_INT < 9) { editor.commit(); } else { editor.apply(); } } }