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();
}
}
}