android: Kotlinify push notifications module (#21881)
This commit is contained in:
-109
@@ -1,109 +0,0 @@
|
||||
package im.status.ethereum.pushnotifications;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.app.Service;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import android.os.IBinder;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.provider.Settings;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
import android.os.Build;
|
||||
import im.status.ethereum.module.R;
|
||||
|
||||
public class ForegroundService extends Service {
|
||||
private static final String CHANNEL_ID = "status-service";
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent i, int flags, int startId) {
|
||||
// NOTE: recent versions of Android require the service to display
|
||||
// a sticky notification to inform the user that the service is running
|
||||
Context context = getApplicationContext();
|
||||
// Create the NotificationChannel, but only on API 26+ because
|
||||
// the NotificationChannel class is new and not in the support library
|
||||
|
||||
Intent intent = null;
|
||||
String notificationContentText = null;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationManager notificationManager =
|
||||
context.getSystemService(NotificationManager.class);
|
||||
NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
|
||||
context.getResources().getString(R.string.status_service),
|
||||
NotificationManager.IMPORTANCE_HIGH);
|
||||
channel.setShowBadge(false);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
|
||||
// Create intent that takes the user to the notification channel settings so they can hide it
|
||||
|
||||
intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
|
||||
intent.putExtra(Settings.EXTRA_CHANNEL_ID, CHANNEL_ID);
|
||||
|
||||
notificationContentText = context.getResources().getString(R.string.tap_to_hide_notification);
|
||||
|
||||
} else {
|
||||
|
||||
// For older versions of android intent takes the user to the Status app
|
||||
|
||||
Class intentClass;
|
||||
String packageName = context.getPackageName();
|
||||
Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
|
||||
String className = launchIntent.getComponent().getClassName();
|
||||
try {
|
||||
intentClass = Class.forName(className);
|
||||
} catch (ClassNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
return 0;
|
||||
}
|
||||
|
||||
intent = new Intent(context, intentClass);
|
||||
intent.addCategory(Intent.CATEGORY_BROWSABLE);
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
|
||||
notificationContentText = context.getResources().getString(R.string.keep_status_running);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE);
|
||||
Intent stopIntent = new Intent(PushNotificationHelper.ACTION_TAP_STOP);
|
||||
PendingIntent stopPendingIntent = PendingIntent.getBroadcast(context, 0, stopIntent,
|
||||
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
Notification notification = new NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_stat_notify_status)
|
||||
.setContentTitle(context.getResources().getString(R.string.background_service_opened))
|
||||
.setContentText(notificationContentText)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setNumber(0)
|
||||
.addAction(R.drawable.ic_stat_notify_status,
|
||||
context.getResources().getString(R.string.stop),
|
||||
stopPendingIntent)
|
||||
.build();
|
||||
|
||||
// the id of the foreground notification MUST NOT be 0
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE);
|
||||
} else {
|
||||
startForeground(1, notification);
|
||||
}
|
||||
|
||||
return START_STICKY;
|
||||
}
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package im.status.ethereum.pushnotifications
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.app.Service
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.IBinder
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.provider.Settings
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import android.os.Build
|
||||
import im.status.ethereum.module.R
|
||||
|
||||
class ForegroundService : Service() {
|
||||
companion object {
|
||||
private const val CHANNEL_ID = "status-service"
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onStartCommand(i: Intent?, flags: Int, startId: Int): Int {
|
||||
// NOTE: recent versions of Android require the service to display
|
||||
// a sticky notification to inform the user that the service is running
|
||||
val context = applicationContext
|
||||
// Create the NotificationChannel, but only on API 26+ because
|
||||
// the NotificationChannel class is new and not in the support library
|
||||
|
||||
// Create notification channel for Android O and above
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val notificationManager = context.getSystemService(NotificationManager::class.java)
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
context.resources.getString(R.string.status_service),
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
).apply {
|
||||
setShowBadge(false)
|
||||
}
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
// Initialize intent and notification text based on Android version
|
||||
val notificationIntent = createNotificationIntent(context)
|
||||
val notificationText = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.resources.getString(R.string.tap_to_hide_notification)
|
||||
} else {
|
||||
context.resources.getString(R.string.keep_status_running)
|
||||
}
|
||||
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
notificationIntent,
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val stopIntent = Intent(PushNotificationHelper.ACTION_TAP_STOP)
|
||||
val stopPendingIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
0,
|
||||
stopIntent,
|
||||
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_stat_notify_status)
|
||||
.setContentTitle(context.resources.getString(R.string.background_service_opened))
|
||||
.setContentText(notificationText)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setNumber(0)
|
||||
.addAction(
|
||||
R.drawable.ic_stat_notify_status,
|
||||
context.resources.getString(R.string.stop),
|
||||
stopPendingIntent
|
||||
)
|
||||
.build()
|
||||
|
||||
// the id of the foreground notification MUST NOT be 0
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||
} else {
|
||||
startForeground(1, notification)
|
||||
}
|
||||
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
private fun createNotificationIntent(context: Context): Intent {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// Create intent that takes the user to the notification channel settings
|
||||
Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply {
|
||||
putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
|
||||
putExtra(Settings.EXTRA_CHANNEL_ID, CHANNEL_ID)
|
||||
}
|
||||
} else {
|
||||
// For older versions of android, intent takes the user to the Status app
|
||||
val packageName = context.packageName
|
||||
val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
|
||||
val className = launchIntent?.component?.className
|
||||
|
||||
try {
|
||||
val intentClass = Class.forName(className!!)
|
||||
Intent(context, intentClass).apply {
|
||||
addCategory(Intent.CATEGORY_BROWSABLE)
|
||||
action = Intent.ACTION_VIEW
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
}
|
||||
} catch (e: ClassNotFoundException) {
|
||||
e.printStackTrace()
|
||||
// Return an empty intent as fallback
|
||||
Intent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-146
@@ -1,146 +0,0 @@
|
||||
package im.status.ethereum.pushnotifications;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import com.facebook.react.bridge.ActivityEventListener;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.Callback;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import android.util.Log;
|
||||
|
||||
import im.status.ethereum.pushnotifications.PushNotificationJsDelivery;
|
||||
|
||||
public class PushNotification extends ReactContextBaseJavaModule implements ActivityEventListener {
|
||||
public static final String LOG_TAG = "PushNotification";
|
||||
|
||||
private final SecureRandom mRandomNumberGenerator = new SecureRandom();
|
||||
private PushNotificationHelper pushNotificationHelper;
|
||||
private PushNotificationJsDelivery delivery;
|
||||
private ReactApplicationContext reactContext;
|
||||
private boolean started;
|
||||
|
||||
public PushNotification(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
this.reactContext = reactContext;
|
||||
reactContext.addActivityEventListener(this);
|
||||
Application applicationContext = (Application) reactContext.getApplicationContext();
|
||||
|
||||
IntentFilter intentFilter = new IntentFilter();
|
||||
pushNotificationHelper = new PushNotificationHelper(applicationContext, intentFilter);
|
||||
|
||||
delivery = new PushNotificationJsDelivery(reactContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "PushNotification";
|
||||
}
|
||||
|
||||
// removed @Override temporarily just to get it working on different versions of RN
|
||||
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
|
||||
onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
// removed @Override temporarily just to get it working on different versions of RN
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
// Ignored, required to implement ActivityEventListener for RN 0.33
|
||||
}
|
||||
|
||||
private Bundle getBundleFromIntent(Intent intent) {
|
||||
Bundle bundle = null;
|
||||
if (intent.hasExtra("notification")) {
|
||||
bundle = intent.getBundleExtra("notification");
|
||||
} else if (intent.hasExtra("google.message_id")) {
|
||||
bundle = new Bundle();
|
||||
|
||||
bundle.putBundle("data", intent.getExtras());
|
||||
}
|
||||
|
||||
if(null != bundle && !bundle.getBoolean("foreground", false) && !bundle.containsKey("userInteraction")) {
|
||||
bundle.putBoolean("userInteraction", true);
|
||||
}
|
||||
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewIntent(Intent intent) {
|
||||
Bundle bundle = this.getBundleFromIntent(intent);
|
||||
if (bundle != null) {
|
||||
delivery.notifyNotification(bundle);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
/**
|
||||
* Creates a channel if it does not already exist. Returns whether the channel was created.
|
||||
*/
|
||||
public void createChannel(ReadableMap channelInfo, Callback callback) {
|
||||
boolean created = pushNotificationHelper.createChannel(channelInfo);
|
||||
|
||||
if(callback != null) {
|
||||
callback.invoke(created);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void presentLocalNotification(ReadableMap details) {
|
||||
if (!this.started) {
|
||||
return;
|
||||
}
|
||||
|
||||
Bundle bundle = Arguments.toBundle(details);
|
||||
// If notification ID is not provided by the user, generate one at random
|
||||
if (bundle.getString("id") == null) {
|
||||
bundle.putString("id", String.valueOf(mRandomNumberGenerator.nextInt()));
|
||||
}
|
||||
|
||||
pushNotificationHelper.sendToNotificationCentre(bundle);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void clearMessageNotifications(String conversationId) {
|
||||
if (this.started) {
|
||||
pushNotificationHelper.clearMessageNotifications(conversationId);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void clearAllMessageNotifications() {
|
||||
pushNotificationHelper.clearAllMessageNotifications();
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void enableNotifications() {
|
||||
this.started = true;
|
||||
this.pushNotificationHelper.start();
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void disableNotifications() {
|
||||
if (this.started) {
|
||||
this.started = false;
|
||||
this.pushNotificationHelper.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package im.status.ethereum.pushnotifications
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.app.NotificationManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.annotation.NonNull
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.facebook.react.bridge.*
|
||||
import im.status.ethereum.pushnotifications.PushNotificationJsDelivery
|
||||
import java.security.SecureRandom
|
||||
|
||||
class PushNotification(reactContext: ReactApplicationContext) :
|
||||
ReactContextBaseJavaModule(reactContext), ActivityEventListener {
|
||||
|
||||
companion object {
|
||||
const val LOG_TAG = "PushNotification"
|
||||
}
|
||||
|
||||
private val mRandomNumberGenerator = SecureRandom()
|
||||
private val pushNotificationHelper: PushNotificationHelper
|
||||
private val delivery: PushNotificationJsDelivery
|
||||
private val reactContext: ReactApplicationContext = reactContext
|
||||
private var started: Boolean = false
|
||||
|
||||
init {
|
||||
reactContext.addActivityEventListener(this)
|
||||
val applicationContext = reactContext.applicationContext as Application
|
||||
|
||||
val intentFilter = IntentFilter()
|
||||
pushNotificationHelper = PushNotificationHelper(applicationContext, intentFilter)
|
||||
|
||||
delivery = PushNotificationJsDelivery(reactContext)
|
||||
}
|
||||
|
||||
override fun getName(): String = "PushNotification"
|
||||
|
||||
// Primary implementation of ActivityEventListener interface
|
||||
override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
// Call the helper method with the correct parameter order
|
||||
handleActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
// Private helper method for handling activity results
|
||||
private fun handleActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
// Ignored, required to implement ActivityEventListener for RN 0.33
|
||||
}
|
||||
|
||||
private fun getBundleFromIntent(intent: Intent): Bundle? {
|
||||
return when {
|
||||
intent.hasExtra("notification") -> intent.getBundleExtra("notification")
|
||||
intent.hasExtra("google.message_id") -> Bundle().apply {
|
||||
putBundle("data", intent.extras)
|
||||
}
|
||||
else -> null
|
||||
}?.apply {
|
||||
if (!getBoolean("foreground", false) && !containsKey("userInteraction")) {
|
||||
putBoolean("userInteraction", true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
getBundleFromIntent(intent)?.let { bundle ->
|
||||
delivery.notifyNotification(bundle)
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun createChannel(channelInfo: ReadableMap, callback: Callback?) {
|
||||
val created = pushNotificationHelper.createChannel(channelInfo)
|
||||
callback?.invoke(created)
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun presentLocalNotification(details: ReadableMap) {
|
||||
if (!started) return
|
||||
|
||||
Arguments.toBundle(details)?.apply {
|
||||
// If notification ID is not provided by the user, generate one at random
|
||||
if (getString("id") == null) {
|
||||
putString("id", mRandomNumberGenerator.nextInt().toString())
|
||||
}
|
||||
pushNotificationHelper.sendToNotificationCentre(this)
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun clearMessageNotifications(conversationId: String) {
|
||||
if (started) {
|
||||
pushNotificationHelper.clearMessageNotifications(conversationId)
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun clearAllMessageNotifications() {
|
||||
pushNotificationHelper.clearAllMessageNotifications()
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun enableNotifications() {
|
||||
started = true
|
||||
pushNotificationHelper.start()
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun disableNotifications() {
|
||||
if (started) {
|
||||
started = false
|
||||
pushNotificationHelper.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
-93
@@ -1,93 +0,0 @@
|
||||
package im.status.ethereum.pushnotifications;
|
||||
|
||||
import android.os.Build;
|
||||
import android.app.Application;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import com.facebook.react.ReactApplication;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
|
||||
|
||||
import im.status.ethereum.pushnotifications.PushNotificationJsDelivery;
|
||||
import static im.status.ethereum.pushnotifications.PushNotification.LOG_TAG;
|
||||
|
||||
public class PushNotificationActions extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(final Context context, Intent intent) {
|
||||
String intentActionPrefix = context.getPackageName() + ".ACTION_";
|
||||
|
||||
Log.i(LOG_TAG, "PushNotificationBootEventReceiver loading scheduled notifications");
|
||||
|
||||
if (null == intent.getAction() || !intent.getAction().startsWith(intentActionPrefix)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Bundle bundle = intent.getBundleExtra("notification");
|
||||
|
||||
// Dismiss the notification popup.
|
||||
NotificationManager manager = (NotificationManager) context.getSystemService(context.NOTIFICATION_SERVICE);
|
||||
int notificationID = Integer.parseInt(bundle.getString("id"));
|
||||
|
||||
boolean autoCancel = bundle.getBoolean("autoCancel", true);
|
||||
|
||||
if(autoCancel) {
|
||||
if (bundle.containsKey("tag")) {
|
||||
String tag = bundle.getString("tag");
|
||||
manager.cancel(tag, notificationID);
|
||||
} else {
|
||||
manager.cancel(notificationID);
|
||||
}
|
||||
}
|
||||
|
||||
boolean invokeApp = bundle.getBoolean("invokeApp", true);
|
||||
|
||||
// Notify the action.
|
||||
if(invokeApp) {
|
||||
|
||||
IntentFilter intentFilter = new IntentFilter();
|
||||
PushNotificationHelper helper = new PushNotificationHelper((Application) context.getApplicationContext(), intentFilter);
|
||||
|
||||
helper.invokeApp(bundle);
|
||||
} else {
|
||||
|
||||
// We need to run this on the main thread, as the React code assumes that is true.
|
||||
// Namely, DevServerHelper constructs a Handler() without a Looper, which triggers:
|
||||
// "Can't create handler inside thread that has not called Looper.prepare()"
|
||||
Handler handler = new Handler(Looper.getMainLooper());
|
||||
handler.post(new Runnable() {
|
||||
public void run() {
|
||||
// Construct and load our normal React JS code bundle
|
||||
final ReactInstanceManager mReactInstanceManager = ((ReactApplication) context.getApplicationContext()).getReactNativeHost().getReactInstanceManager();
|
||||
ReactContext context = mReactInstanceManager.getCurrentReactContext();
|
||||
PushNotificationJsDelivery delivery = new PushNotificationJsDelivery(context);
|
||||
// If it's constructed, send a notification
|
||||
if (context != null) {
|
||||
delivery.notifyNotificationAction(bundle);
|
||||
} else {
|
||||
// Otherwise wait for construction, then send the notification
|
||||
mReactInstanceManager.addReactInstanceEventListener(new ReactInstanceManager.ReactInstanceEventListener() {
|
||||
public void onReactContextInitialized(ReactContext context) {
|
||||
PushNotificationJsDelivery delivery = new PushNotificationJsDelivery(context);
|
||||
delivery.notifyNotificationAction(bundle);
|
||||
mReactInstanceManager.removeReactInstanceEventListener(this);
|
||||
}
|
||||
});
|
||||
if (!mReactInstanceManager.hasStartedCreatingInitialContext()) {
|
||||
// Construct it in the background
|
||||
mReactInstanceManager.createReactContextInBackground();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package im.status.ethereum.pushnotifications
|
||||
|
||||
import android.os.Build
|
||||
import android.app.Application
|
||||
import android.app.NotificationManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import com.facebook.react.ReactApplication
|
||||
import com.facebook.react.ReactInstanceManager
|
||||
import com.facebook.react.bridge.ReactContext
|
||||
import im.status.ethereum.pushnotifications.PushNotificationJsDelivery
|
||||
import im.status.ethereum.pushnotifications.PushNotification.Companion.LOG_TAG
|
||||
|
||||
/**
|
||||
* BroadcastReceiver responsible for handling push notification actions.
|
||||
* This class processes notification interactions and manages their lifecycle.
|
||||
*/
|
||||
class PushNotificationActions : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val intentActionPrefix = "${context.packageName}.ACTION_"
|
||||
|
||||
Log.i(LOG_TAG, "PushNotificationBootEventReceiver loading scheduled notifications")
|
||||
|
||||
if (intent.action == null || !intent.action!!.startsWith(intentActionPrefix)) {
|
||||
return
|
||||
}
|
||||
|
||||
val bundle = intent.getBundleExtra("notification") ?: return
|
||||
|
||||
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
val notificationID = bundle.getString("id")?.toInt() ?: return
|
||||
|
||||
// Dismiss the notification popup.
|
||||
val autoCancel = bundle.getBoolean("autoCancel", true)
|
||||
if (autoCancel) {
|
||||
bundle.getString("tag")?.let { tag ->
|
||||
manager.cancel(tag, notificationID)
|
||||
} ?: run {
|
||||
manager.cancel(notificationID)
|
||||
}
|
||||
}
|
||||
|
||||
// Notify the action.
|
||||
val invokeApp = bundle.getBoolean("invokeApp", true)
|
||||
|
||||
if (invokeApp) {
|
||||
val intentFilter = IntentFilter()
|
||||
val helper = PushNotificationHelper(
|
||||
context.applicationContext as Application,
|
||||
intentFilter
|
||||
)
|
||||
helper.invokeApp(bundle)
|
||||
} else {
|
||||
// We need to run this on the main thread, as the React code assumes that is true.
|
||||
// Namely, DevServerHelper constructs a Handler() without a Looper, which triggers:
|
||||
// "Can't create handler inside thread that has not called Looper.prepare()"
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
// Construct and load our normal React JS code bundle
|
||||
val reactInstanceManager = (context.applicationContext as ReactApplication)
|
||||
.reactNativeHost
|
||||
.reactInstanceManager
|
||||
|
||||
val currentContext = reactInstanceManager.currentReactContext
|
||||
|
||||
when {
|
||||
// If context exists, deliver notification immediately
|
||||
currentContext != null -> {
|
||||
PushNotificationJsDelivery(currentContext)
|
||||
.notifyNotificationAction(bundle)
|
||||
}
|
||||
// Otherwise wait for construction, then send the notification
|
||||
else -> {
|
||||
val listener = object : ReactInstanceManager.ReactInstanceEventListener {
|
||||
override fun onReactContextInitialized(reactContext: ReactContext) {
|
||||
PushNotificationJsDelivery(reactContext)
|
||||
.notifyNotificationAction(bundle)
|
||||
reactInstanceManager.removeReactInstanceEventListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
// Add the listener and create context if needed
|
||||
reactInstanceManager.addReactInstanceEventListener(listener)
|
||||
if (!reactInstanceManager.hasStartedCreatingInitialContext()) {
|
||||
// Construct it in the background
|
||||
reactInstanceManager.createReactContextInBackground()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-890
@@ -1,890 +0,0 @@
|
||||
// https://github.com/zo0r/react-native-push-notification/blob/bedc8f646aab67d594f291449fbfa24e07b64fe8/android/src/main/java/com/dieam/reactnativepushnotification/modules/RNPushNotificationHelper.java Copy-Paste with removed firebase
|
||||
package im.status.ethereum.pushnotifications;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.app.ActivityManager.RunningAppProcessInfo;
|
||||
import android.app.AlarmManager;
|
||||
import android.app.Application;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.BroadcastReceiver;
|
||||
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.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffXfermode;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.RingtoneManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
import android.util.Log;
|
||||
import android.util.Base64;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.Person;
|
||||
import androidx.core.graphics.drawable.IconCompat;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.List;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import im.status.ethereum.module.R;
|
||||
import static im.status.ethereum.pushnotifications.PushNotification.LOG_TAG;
|
||||
|
||||
public class PushNotificationHelper {
|
||||
|
||||
private Context context;
|
||||
|
||||
private static final long DEFAULT_VIBRATION = 300L;
|
||||
private static final String CHANNEL_ID = "status-im-notifications";
|
||||
public static final String ACTION_DELETE_NOTIFICATION = "im.status.ethereum.module.DELETE_NOTIFICATION";
|
||||
public static final String ACTION_TAP_STOP = "im.status.ethereum.module.TAP_STOP";
|
||||
final int flag = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE : PendingIntent.FLAG_CANCEL_CURRENT;
|
||||
|
||||
private NotificationManager notificationManager;
|
||||
|
||||
|
||||
private HashMap<String, Person> persons;
|
||||
private HashMap<String, StatusMessageGroup> messageGroups;
|
||||
|
||||
private IntentFilter intentFilter;
|
||||
|
||||
public PushNotificationHelper(Application context, IntentFilter intentFilter) {
|
||||
this.context = context;
|
||||
this.intentFilter = intentFilter;
|
||||
this.persons = new HashMap<String, Person>();
|
||||
this.messageGroups = new HashMap<String, StatusMessageGroup>();
|
||||
this.notificationManager = context.getSystemService(NotificationManager.class);
|
||||
this.registerBroadcastReceiver();
|
||||
}
|
||||
|
||||
public Intent getOpenAppIntent(String deepLink) {
|
||||
Class intentClass;
|
||||
String packageName = context.getPackageName();
|
||||
Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
|
||||
String className = launchIntent.getComponent().getClassName();
|
||||
try {
|
||||
intentClass = Class.forName(className);
|
||||
} catch (ClassNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
Intent intent = new Intent(context, intentClass);
|
||||
intent.addCategory(Intent.CATEGORY_BROWSABLE);
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
//NOTE: you might wonder, why the heck did he decide to set these flags in particular. Well,
|
||||
//the answer is a simple as it can get in the Android native development world. I noticed
|
||||
//that my initial setup was opening the app but wasn't triggering any events on the js side, like
|
||||
//the links do from the browser. So I compared both intents and noticed that the link from
|
||||
//the browser produces an intent with the flag 0x14000000. I found out that it was the following
|
||||
//flags in this link:
|
||||
//https://stackoverflow.com/questions/52390129/android-intent-setflags-issue
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
if (deepLink != null) {
|
||||
intent.setData(Uri.parse(deepLink));
|
||||
}
|
||||
return intent;
|
||||
}
|
||||
|
||||
//NOTE: we use a dynamically created BroadcastReceiver here so that we can capture
|
||||
//intents from notifications and act on them. For instance when tapping/dismissing
|
||||
//a chat notification we want to clear the chat so that next messages don't show
|
||||
//the messages that we have seen again
|
||||
private final BroadcastReceiver notificationActionReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent.getAction() == ACTION_DELETE_NOTIFICATION) {
|
||||
String groupId = intent.getExtras().getString("im.status.ethereum.groupId");
|
||||
if (groupId != null) {
|
||||
cleanGroup(groupId);
|
||||
}
|
||||
}
|
||||
if (intent.getAction() == ACTION_TAP_STOP) {
|
||||
stop();
|
||||
System.exit(0);
|
||||
}
|
||||
Log.e(LOG_TAG, "intent received: " + intent.getAction());
|
||||
}
|
||||
};
|
||||
|
||||
public void registerBroadcastReceiver() {
|
||||
this.intentFilter.addAction(ACTION_DELETE_NOTIFICATION);
|
||||
this.intentFilter.addAction(ACTION_TAP_STOP);
|
||||
context.registerReceiver(notificationActionReceiver, this.intentFilter, Context.RECEIVER_EXPORTED);
|
||||
Log.e(LOG_TAG, "Broadcast Receiver registered");
|
||||
}
|
||||
|
||||
|
||||
|
||||
private NotificationManager notificationManager() {
|
||||
return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
}
|
||||
|
||||
public void invokeApp(Bundle bundle) {
|
||||
String packageName = context.getPackageName();
|
||||
Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
|
||||
String className = launchIntent.getComponent().getClassName();
|
||||
|
||||
try {
|
||||
Class<?> activityClass = Class.forName(className);
|
||||
Intent activityIntent = new Intent(context, activityClass);
|
||||
|
||||
if(bundle != null) {
|
||||
activityIntent.putExtra("notification", bundle);
|
||||
}
|
||||
|
||||
activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
||||
context.startActivity(activityIntent);
|
||||
} catch(Exception e) {
|
||||
Log.e(LOG_TAG, "Class not found", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void sendToNotificationCentre(final Bundle bundle) {
|
||||
PushNotificationPicturesAggregator aggregator = new PushNotificationPicturesAggregator(new PushNotificationPicturesAggregator.Callback() {
|
||||
public void call(Bitmap largeIconImage, Bitmap bigPictureImage) {
|
||||
sendToNotificationCentreWithPicture(bundle, largeIconImage, bigPictureImage);
|
||||
}
|
||||
});
|
||||
|
||||
aggregator.setLargeIconUrl(context, bundle.getString("largeIconUrl"));
|
||||
aggregator.setBigPictureUrl(context, bundle.getString("bigPictureUrl"));
|
||||
}
|
||||
|
||||
public void handleConversation(final Bundle bundle) {
|
||||
if (bundle.getBoolean("deleted")){
|
||||
this.removeStatusMessage(bundle);
|
||||
} else {
|
||||
this.addStatusMessage(bundle);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearMessageNotifications(String conversationId) {
|
||||
notificationManager.cancel(conversationId.hashCode());
|
||||
cleanGroup(conversationId);
|
||||
}
|
||||
|
||||
public void clearAllMessageNotifications() {
|
||||
notificationManager.cancelAll();
|
||||
}
|
||||
|
||||
public void sendToNotificationCentreWithPicture(final Bundle bundle, Bitmap largeIconBitmap, Bitmap bigPictureBitmap) {
|
||||
|
||||
try {
|
||||
Class intentClass = getMainActivityClass();
|
||||
if (intentClass == null) {
|
||||
Log.e(LOG_TAG, "No activity class found for the notification");
|
||||
return;
|
||||
}
|
||||
|
||||
if (bundle.getBoolean("isConversation")) {
|
||||
this.handleConversation(bundle);
|
||||
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, "Ignore this message if you sent data-only notification. 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();
|
||||
}
|
||||
|
||||
int priority = NotificationCompat.PRIORITY_HIGH;
|
||||
final String priorityString = bundle.getString("priority");
|
||||
|
||||
if (priorityString != null) {
|
||||
switch (priorityString.toLowerCase()) {
|
||||
case "max":
|
||||
priority = NotificationCompat.PRIORITY_MAX;
|
||||
break;
|
||||
case "high":
|
||||
priority = NotificationCompat.PRIORITY_HIGH;
|
||||
break;
|
||||
case "low":
|
||||
priority = NotificationCompat.PRIORITY_LOW;
|
||||
break;
|
||||
case "min":
|
||||
priority = NotificationCompat.PRIORITY_MIN;
|
||||
break;
|
||||
case "default":
|
||||
priority = NotificationCompat.PRIORITY_DEFAULT;
|
||||
break;
|
||||
default:
|
||||
priority = NotificationCompat.PRIORITY_HIGH;
|
||||
}
|
||||
}
|
||||
|
||||
int visibility = NotificationCompat.VISIBILITY_PRIVATE;
|
||||
final String visibilityString = bundle.getString("visibility");
|
||||
|
||||
if (visibilityString != null) {
|
||||
switch (visibilityString.toLowerCase()) {
|
||||
case "private":
|
||||
visibility = NotificationCompat.VISIBILITY_PRIVATE;
|
||||
break;
|
||||
case "public":
|
||||
visibility = NotificationCompat.VISIBILITY_PUBLIC;
|
||||
break;
|
||||
case "secret":
|
||||
visibility = NotificationCompat.VISIBILITY_SECRET;
|
||||
break;
|
||||
default:
|
||||
visibility = NotificationCompat.VISIBILITY_PRIVATE;
|
||||
}
|
||||
}
|
||||
|
||||
String channel_id = bundle.getString("channelId");
|
||||
|
||||
if(channel_id == null) {
|
||||
channel_id = this.getNotificationDefaultChannelId();
|
||||
}
|
||||
|
||||
NotificationCompat.Builder notification = new NotificationCompat.Builder(context, channel_id)
|
||||
.setContentTitle(title)
|
||||
.setTicker(bundle.getString("ticker"))
|
||||
.setVisibility(visibility)
|
||||
.setPriority(priority)
|
||||
.setAutoCancel(bundle.getBoolean("autoCancel", true))
|
||||
.setOnlyAlertOnce(bundle.getBoolean("onlyAlertOnce", false));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // API 24 and higher
|
||||
// Restore showing timestamp on Android 7+
|
||||
// Source: https://developer.android.com/reference/android/app/Notification.Builder.html#setShowWhen(boolean)
|
||||
boolean showWhen = bundle.getBoolean("showWhen", true);
|
||||
|
||||
notification.setShowWhen(showWhen);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // API 26 and higher
|
||||
// Changing Default mode of notification
|
||||
notification.setDefaults(Notification.DEFAULT_LIGHTS);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { // API 20 and higher
|
||||
String group = bundle.getString("group");
|
||||
|
||||
if (group != null) {
|
||||
notification.setGroup(group);
|
||||
}
|
||||
|
||||
if (bundle.containsKey("groupSummary") || bundle.getBoolean("groupSummary")) {
|
||||
notification.setGroupSummary(bundle.getBoolean("groupSummary"));
|
||||
}
|
||||
}
|
||||
|
||||
String numberString = bundle.getString("number");
|
||||
|
||||
if (numberString != null) {
|
||||
notification.setNumber(Integer.parseInt(numberString));
|
||||
}
|
||||
|
||||
// Small icon
|
||||
int smallIconResId = 0;
|
||||
|
||||
String smallIcon = bundle.getString("smallIcon");
|
||||
|
||||
if (smallIcon != null && !smallIcon.isEmpty()) {
|
||||
smallIconResId = res.getIdentifier(smallIcon, "mipmap", packageName);
|
||||
} else if(smallIcon == null) {
|
||||
smallIconResId = res.getIdentifier("ic_stat_notify_status", "drawable", packageName);
|
||||
}
|
||||
|
||||
if (smallIconResId == 0) {
|
||||
smallIconResId = res.getIdentifier("ic_launcher", "mipmap", packageName);
|
||||
|
||||
if (smallIconResId == 0) {
|
||||
smallIconResId = android.R.drawable.ic_dialog_info;
|
||||
}
|
||||
}
|
||||
|
||||
notification.setSmallIcon(smallIconResId);
|
||||
|
||||
// Large icon
|
||||
if(largeIconBitmap == null) {
|
||||
int largeIconResId = 0;
|
||||
|
||||
String largeIcon = bundle.getString("largeIcon");
|
||||
|
||||
if (largeIcon != null && !largeIcon.isEmpty()) {
|
||||
largeIconResId = res.getIdentifier(largeIcon, "mipmap", packageName);
|
||||
} else if(largeIcon == null) {
|
||||
largeIconResId = res.getIdentifier("ic_launcher", "mipmap", packageName);
|
||||
}
|
||||
|
||||
// Before Lolipop there was no large icon for notifications.
|
||||
if (largeIconResId != 0 && (largeIcon != null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)) {
|
||||
largeIconBitmap = BitmapFactory.decodeResource(res, largeIconResId);
|
||||
}
|
||||
}
|
||||
|
||||
Bundle author = bundle.getBundle("notificationAuthor");
|
||||
|
||||
if (largeIconBitmap == null && author != null) {
|
||||
String base64Image = author.getString("icon").split(",")[1];
|
||||
byte[] decodedString = Base64.decode(base64Image, Base64.DEFAULT);
|
||||
Bitmap decodedByte = BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length);
|
||||
notification.setLargeIcon(getCircleBitmap(decodedByte));
|
||||
} else if (largeIconBitmap != null){
|
||||
notification.setLargeIcon(largeIconBitmap);
|
||||
}
|
||||
|
||||
String message = bundle.getString("message");
|
||||
|
||||
notification.setContentText(message);
|
||||
|
||||
String subText = bundle.getString("subText");
|
||||
|
||||
if (subText != null) {
|
||||
notification.setSubText(subText);
|
||||
}
|
||||
|
||||
String bigText = bundle.getString("bigText");
|
||||
|
||||
if (bigText == null) {
|
||||
bigText = message;
|
||||
}
|
||||
|
||||
NotificationCompat.Style style;
|
||||
|
||||
if(bigPictureBitmap != null) {
|
||||
style = new NotificationCompat.BigPictureStyle()
|
||||
.bigPicture(bigPictureBitmap)
|
||||
.setBigContentTitle(title)
|
||||
.setSummaryText(message);
|
||||
} else {
|
||||
style = new NotificationCompat.BigTextStyle().bigText(bigText);
|
||||
}
|
||||
|
||||
notification.setStyle(style);
|
||||
|
||||
Intent intent = new Intent(context, intentClass);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
bundle.putBoolean("foreground", this.isApplicationInForeground());
|
||||
bundle.putBoolean("userInteraction", true);
|
||||
intent.putExtra("notification", bundle);
|
||||
|
||||
Uri soundUri = null;
|
||||
|
||||
if (!bundle.containsKey("playSound") || bundle.getBoolean("playSound")) {
|
||||
String soundName = bundle.getString("soundName");
|
||||
|
||||
if (soundName == null) {
|
||||
soundName = "default";
|
||||
}
|
||||
|
||||
soundUri = getSoundUri(soundName);
|
||||
|
||||
notification.setSound(soundUri);
|
||||
}
|
||||
|
||||
if (soundUri == null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
notification.setSound(null);
|
||||
}
|
||||
|
||||
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");
|
||||
int defaultColor = -1;
|
||||
if (color != null) {
|
||||
notification.setColor(Color.parseColor(color));
|
||||
} else if (defaultColor != -1) {
|
||||
notification.setColor(defaultColor);
|
||||
}
|
||||
}
|
||||
|
||||
int notificationID = notificationIdString.hashCode();
|
||||
|
||||
notification.setContentIntent(createOnTapIntent(context, notificationID, bundle.getString("deepLink")))
|
||||
.setDeleteIntent(createOnDismissedIntent(context, notificationID, bundle.getString("deepLink")));
|
||||
|
||||
NotificationManager notificationManager = notificationManager();
|
||||
|
||||
long[] vibratePattern = new long[]{0};
|
||||
|
||||
if (!bundle.containsKey("vibrate") || bundle.getBoolean("vibrate")) {
|
||||
long vibration = bundle.containsKey("vibration") ? (long) bundle.getDouble("vibration") : DEFAULT_VIBRATION;
|
||||
if (vibration == 0)
|
||||
vibration = DEFAULT_VIBRATION;
|
||||
|
||||
vibratePattern = new long[]{0, vibration};
|
||||
|
||||
notification.setVibrate(vibratePattern);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// Define the shortcutId
|
||||
String shortcutId = bundle.getString("shortcutId");
|
||||
|
||||
if (shortcutId != null) {
|
||||
notification.setShortcutId(shortcutId);
|
||||
}
|
||||
|
||||
Long timeoutAfter = (long) bundle.getDouble("timeoutAfter");
|
||||
|
||||
if (timeoutAfter != null && timeoutAfter >= 0) {
|
||||
notification.setTimeoutAfter(timeoutAfter);
|
||||
}
|
||||
}
|
||||
|
||||
Long when = (long) bundle.getDouble("when");
|
||||
|
||||
if (when != null && when >= 0) {
|
||||
notification.setWhen(when);
|
||||
}
|
||||
|
||||
notification.setUsesChronometer(bundle.getBoolean("usesChronometer", false));
|
||||
notification.setChannelId(channel_id);
|
||||
|
||||
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(context, PushNotificationActions.class);
|
||||
actionIntent.setAction(packageName + ".ACTION_" + i);
|
||||
|
||||
actionIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
|
||||
// Add "action" for later identifying which button gets pressed.
|
||||
bundle.putString("action", action);
|
||||
actionIntent.putExtra("notification", bundle);
|
||||
actionIntent.setPackage(packageName);
|
||||
|
||||
PendingIntent pendingActionIntent = PendingIntent.getBroadcast(context, notificationID, actionIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
notification.addAction(new NotificationCompat.Action.Builder(icon, action, pendingActionIntent).build());
|
||||
} else {
|
||||
notification.addAction(icon, action, pendingActionIntent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!(this.isApplicationInForeground() && bundle.getBoolean("ignoreInForeground"))) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "failed to send push notification", e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkOrCreateChannel(NotificationManager manager, String channel_id, String channel_name, String channel_description, Uri soundUri, int importance, long[] vibratePattern, boolean showBadge) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
|
||||
return false;
|
||||
if (manager == null)
|
||||
return false;
|
||||
|
||||
NotificationChannel channel = manager.getNotificationChannel(channel_id);
|
||||
|
||||
if (
|
||||
channel == null && channel_name != null && channel_description != null ||
|
||||
channel != null &&
|
||||
(
|
||||
channel_name != null && !channel.getName().equals(channel_name) ||
|
||||
channel_description != null && !channel.getDescription().equals(channel_description)
|
||||
)
|
||||
) {
|
||||
// If channel doesn't exist create a new one.
|
||||
// If channel name or description is updated then update the existing channel.
|
||||
channel = new NotificationChannel(channel_id, channel_name, importance);
|
||||
|
||||
channel.setDescription(channel_description);
|
||||
channel.enableLights(true);
|
||||
channel.enableVibration(vibratePattern != null);
|
||||
channel.setVibrationPattern(vibratePattern);
|
||||
channel.setShowBadge(showBadge);
|
||||
|
||||
if (soundUri != null) {
|
||||
AudioAttributes audioAttributes = new AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
|
||||
.build();
|
||||
|
||||
channel.setSound(soundUri, audioAttributes);
|
||||
} else {
|
||||
channel.setSound(null, null);
|
||||
}
|
||||
|
||||
manager.createNotificationChannel(channel);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean createChannel(ReadableMap channelInfo) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
|
||||
return false;
|
||||
|
||||
String channelId = channelInfo.getString("channelId");
|
||||
String channelName = channelInfo.getString("channelName");
|
||||
String channelDescription = channelInfo.hasKey("channelDescription") ? channelInfo.getString("channelDescription") : "";
|
||||
String soundName = channelInfo.hasKey("soundName") ? channelInfo.getString("soundName") : "default";
|
||||
int importance = channelInfo.hasKey("importance") ? channelInfo.getInt("importance") : 4;
|
||||
boolean vibrate = channelInfo.hasKey("vibrate") && channelInfo.getBoolean("vibrate");
|
||||
long[] vibratePattern = vibrate ? new long[] { DEFAULT_VIBRATION } : null;
|
||||
boolean showBadge = channelInfo.hasKey("showBadge") && channelInfo.getBoolean("showBadge");
|
||||
|
||||
NotificationManager manager = notificationManager();
|
||||
|
||||
Uri soundUri = getSoundUri(soundName);
|
||||
|
||||
return checkOrCreateChannel(manager, channelId, channelName, channelDescription, soundUri, importance, vibratePattern, showBadge);
|
||||
}
|
||||
|
||||
public String getNotificationDefaultChannelId() {
|
||||
return this.CHANNEL_ID;
|
||||
}
|
||||
|
||||
private Uri getSoundUri(String soundName) {
|
||||
if (soundName == null || "default".equalsIgnoreCase(soundName)) {
|
||||
return RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
|
||||
} else {
|
||||
|
||||
// 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());
|
||||
}
|
||||
|
||||
return Uri.parse("android.resource://" + context.getPackageName() + "/" + resId);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isApplicationInForeground() {
|
||||
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||||
List<RunningAppProcessInfo> processInfos = activityManager.getRunningAppProcesses();
|
||||
if (processInfos != null) {
|
||||
for (RunningAppProcessInfo processInfo : processInfos) {
|
||||
if (processInfo.processName.equals(context.getPackageName())
|
||||
&& processInfo.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND
|
||||
&& processInfo.pkgList.length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Bitmap getCircleBitmap(Bitmap bitmap) {
|
||||
final Bitmap output = Bitmap.createBitmap(bitmap.getWidth(),
|
||||
bitmap.getHeight(), Bitmap.Config.ARGB_8888);
|
||||
final Canvas canvas = new Canvas(output);
|
||||
|
||||
final int color = Color.RED;
|
||||
final Paint paint = new Paint();
|
||||
final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
|
||||
final RectF rectF = new RectF(rect);
|
||||
|
||||
paint.setAntiAlias(true);
|
||||
canvas.drawARGB(0, 0, 0, 0);
|
||||
paint.setColor(color);
|
||||
canvas.drawOval(rectF, paint);
|
||||
|
||||
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
|
||||
canvas.drawBitmap(bitmap, rect, rect, paint);
|
||||
|
||||
bitmap.recycle();
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private Person getPerson(Bundle bundle) {
|
||||
String name = bundle.getString("name");
|
||||
|
||||
return new Person.Builder().setName(name).build();
|
||||
}
|
||||
|
||||
private StatusMessage createMessage(Bundle data) {
|
||||
Person author = getPerson(data.getBundle("notificationAuthor"));
|
||||
long timeStampLongValue = (long) data.getDouble("timestamp");
|
||||
return new StatusMessage(data.getString("id"), author, timeStampLongValue, data.getString("message"));
|
||||
}
|
||||
|
||||
private PendingIntent createGroupOnDismissedIntent(Context context, int notificationId, String groupId, String deepLink) {
|
||||
Intent intent = new Intent(ACTION_DELETE_NOTIFICATION);
|
||||
intent.putExtra("im.status.ethereum.deepLink", deepLink);
|
||||
intent.putExtra("im.status.ethereum.groupId", groupId);
|
||||
return PendingIntent.getBroadcast(context.getApplicationContext(), notificationId, intent, flag);
|
||||
}
|
||||
|
||||
private PendingIntent createGroupOnTapIntent(Context context, int notificationId, String groupId, String deepLink) {
|
||||
Intent intent = getOpenAppIntent(deepLink);
|
||||
return PendingIntent.getActivity(context.getApplicationContext(), notificationId, intent, flag);
|
||||
}
|
||||
|
||||
private PendingIntent createOnTapIntent(Context context, int notificationId, String deepLink) {
|
||||
Intent intent = getOpenAppIntent(deepLink);
|
||||
return PendingIntent.getActivity(context.getApplicationContext(), notificationId, intent, flag);
|
||||
}
|
||||
|
||||
private PendingIntent createOnDismissedIntent(Context context, int notificationId, String deepLink) {
|
||||
Intent intent = new Intent(ACTION_DELETE_NOTIFICATION);
|
||||
intent.putExtra("im.status.ethereum.deepLink", deepLink);
|
||||
return PendingIntent.getBroadcast(context.getApplicationContext(), notificationId, intent, flag);
|
||||
}
|
||||
|
||||
public void removeStatusMessage(Bundle bundle) {
|
||||
String conversationId = bundle.getString("conversationId");
|
||||
StatusMessageGroup group = this.messageGroups.get(conversationId);
|
||||
NotificationManager notificationManager = notificationManager();
|
||||
|
||||
if (group == null) {
|
||||
group = new StatusMessageGroup(conversationId);
|
||||
}
|
||||
|
||||
this.messageGroups.put(conversationId, group);
|
||||
|
||||
String id = bundle.getString("id");
|
||||
group.removeMessage(id);
|
||||
|
||||
this.showMessages(bundle);
|
||||
}
|
||||
|
||||
public StatusMessageGroup getMessageGroup(String conversationId) {
|
||||
return this.messageGroups.get(conversationId);
|
||||
}
|
||||
|
||||
public void addStatusMessage(Bundle bundle) {
|
||||
String conversationId = bundle.getString("conversationId");
|
||||
StatusMessageGroup group = this.messageGroups.get(conversationId);
|
||||
NotificationManager notificationManager = notificationManager();
|
||||
|
||||
if (group == null) {
|
||||
group = new StatusMessageGroup(conversationId);
|
||||
}
|
||||
|
||||
this.messageGroups.put(conversationId, group);
|
||||
|
||||
group.addMessage(createMessage(bundle));
|
||||
|
||||
this.showMessages(bundle);
|
||||
}
|
||||
|
||||
public void showMessages(Bundle bundle) {
|
||||
String conversationId = bundle.getString("conversationId");
|
||||
StatusMessageGroup group = this.messageGroups.get(conversationId);
|
||||
NotificationManager notificationManager = notificationManager();
|
||||
|
||||
NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle("Me");
|
||||
ArrayList<StatusMessage> messages = group.getMessages();
|
||||
|
||||
if (messages.size() == 0) {
|
||||
notificationManager.cancel(conversationId.hashCode());
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < messages.size(); i++) {
|
||||
StatusMessage message = messages.get(i);
|
||||
messagingStyle.addMessage(message.getText(),
|
||||
message.getTimestamp(),
|
||||
message.getAuthor());
|
||||
}
|
||||
|
||||
if (bundle.getString("title") != null) {
|
||||
messagingStyle.setConversationTitle(bundle.getString("title"));
|
||||
}
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_stat_notify_status)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
.setStyle(messagingStyle)
|
||||
.setGroup(conversationId)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setGroupSummary(true)
|
||||
.setContentIntent(createGroupOnTapIntent(context, conversationId.hashCode(), conversationId, bundle.getString("deepLink")))
|
||||
.setDeleteIntent(createGroupOnDismissedIntent(context, conversationId.hashCode(), conversationId, bundle.getString("deepLink")))
|
||||
.setNumber(messages.size() + 1)
|
||||
.setAutoCancel(true);
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
builder.setVibrate(new long[0]);
|
||||
}
|
||||
notificationManager.notify(conversationId.hashCode(), builder.build());
|
||||
}
|
||||
|
||||
class StatusMessageGroup {
|
||||
private ArrayList<StatusMessage> messages;
|
||||
private String id;
|
||||
|
||||
StatusMessageGroup(String id) {
|
||||
this.id = id;
|
||||
this.messages = new ArrayList<StatusMessage>();
|
||||
}
|
||||
public ArrayList<StatusMessage> getMessages() {
|
||||
return messages;
|
||||
}
|
||||
|
||||
public void addMessage(StatusMessage message) {
|
||||
this.messages.add(message);
|
||||
}
|
||||
|
||||
public void removeMessage(String id) {
|
||||
ArrayList<StatusMessage> newMessages = new ArrayList<StatusMessage>();
|
||||
for(StatusMessage message: this.messages) {
|
||||
if(!message.id.equals(id)) {
|
||||
newMessages.add(message);
|
||||
}
|
||||
}
|
||||
this.messages = newMessages;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
}
|
||||
|
||||
class StatusMessage {
|
||||
public Person getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
private String id;
|
||||
private Person author;
|
||||
private long timestamp;
|
||||
private String text;
|
||||
|
||||
StatusMessage(String id, Person author, long timestamp, String text) {
|
||||
this.id = id;
|
||||
this.author = author;
|
||||
this.timestamp = timestamp;
|
||||
this.text = text;
|
||||
}
|
||||
}
|
||||
|
||||
private void removeGroup(String groupId) {
|
||||
this.messageGroups.remove(groupId);
|
||||
}
|
||||
|
||||
private void cleanGroup(String groupId) {
|
||||
removeGroup(groupId);
|
||||
if (messageGroups.size() == 0) {
|
||||
notificationManager.cancelAll();
|
||||
}
|
||||
}
|
||||
|
||||
public void start() {
|
||||
Log.e(LOG_TAG, "Starting Foreground Service");
|
||||
Intent serviceIntent = new Intent(context, ForegroundService.class);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(serviceIntent);
|
||||
} else {
|
||||
context.startService(serviceIntent);
|
||||
}
|
||||
this.registerBroadcastReceiver();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
Log.e(LOG_TAG, "Stopping Foreground Service");
|
||||
//NOTE: we cancel all the current notifications, because the intents can't be used anymore
|
||||
//since the broadcast receiver will be killed as well and won't be able to handle any intent
|
||||
notificationManager.cancelAll();
|
||||
Intent serviceIntent = new Intent(context, ForegroundService.class);
|
||||
context.stopService(serviceIntent);
|
||||
context.unregisterReceiver(notificationActionReceiver);
|
||||
}
|
||||
}
|
||||
+750
@@ -0,0 +1,750 @@
|
||||
// https://github.com/zo0r/react-native-push-notification/blob/bedc8f646aab67d594f291449fbfa24e07b64fe8/android/src/main/
|
||||
// java/com/dieam/reactnativepushnotification/modules/RNPushNotificationHelper.java Copy-Paste with removed firebase
|
||||
package im.status.ethereum.pushnotifications
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.app.ActivityManager.RunningAppProcessInfo
|
||||
import android.app.AlarmManager
|
||||
import android.app.Application
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.BroadcastReceiver
|
||||
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.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffXfermode
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.media.AudioAttributes
|
||||
import android.media.RingtoneManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.service.notification.StatusBarNotification
|
||||
import android.util.Log
|
||||
import android.util.Base64
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.Person
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.facebook.react.bridge.WritableArray
|
||||
import com.facebook.react.bridge.WritableMap
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import im.status.ethereum.module.R
|
||||
import im.status.ethereum.pushnotifications.PushNotification.Companion.LOG_TAG
|
||||
|
||||
class PushNotificationHelper(private val context: Application, private val intentFilter: IntentFilter) {
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_VIBRATION: Long = 300L
|
||||
private const val CHANNEL_ID = "status-im-notifications"
|
||||
const val ACTION_DELETE_NOTIFICATION = "im.status.ethereum.module.DELETE_NOTIFICATION"
|
||||
const val ACTION_TAP_STOP = "im.status.ethereum.module.TAP_STOP"
|
||||
}
|
||||
|
||||
private val flag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
PendingIntent.FLAG_CANCEL_CURRENT
|
||||
}
|
||||
|
||||
private val notificationManager: NotificationManager by lazy {
|
||||
context.getSystemService(NotificationManager::class.java)
|
||||
}
|
||||
|
||||
private val persons: MutableMap<String, Person> = mutableMapOf()
|
||||
private val messageGroups: MutableMap<String, StatusMessageGroup> = mutableMapOf()
|
||||
|
||||
init {
|
||||
registerBroadcastReceiver()
|
||||
}
|
||||
|
||||
private val notificationActionReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (intent.action) {
|
||||
ACTION_DELETE_NOTIFICATION -> {
|
||||
intent.extras?.getString("im.status.ethereum.groupId")?.let { groupId ->
|
||||
cleanGroup(groupId)
|
||||
}
|
||||
}
|
||||
ACTION_TAP_STOP -> {
|
||||
stop()
|
||||
System.exit(0)
|
||||
}
|
||||
}
|
||||
Log.e(LOG_TAG, "intent received: ${intent.action}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerBroadcastReceiver() {
|
||||
intentFilter.apply {
|
||||
addAction(ACTION_DELETE_NOTIFICATION)
|
||||
addAction(ACTION_TAP_STOP)
|
||||
}
|
||||
context.registerReceiver(notificationActionReceiver, intentFilter, Context.RECEIVER_EXPORTED)
|
||||
Log.e(LOG_TAG, "Broadcast Receiver registered")
|
||||
}
|
||||
|
||||
fun getOpenAppIntent(deepLink: String?): Intent? {
|
||||
val packageName = context.packageName
|
||||
val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
|
||||
val className = launchIntent?.component?.className ?: return null
|
||||
|
||||
return try {
|
||||
val intentClass = Class.forName(className)
|
||||
Intent(context, intentClass).apply {
|
||||
addCategory(Intent.CATEGORY_BROWSABLE)
|
||||
action = Intent.ACTION_VIEW
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
deepLink?.let { data = Uri.parse(it) }
|
||||
}
|
||||
} catch (e: ClassNotFoundException) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun invokeApp(bundle: Bundle?) {
|
||||
val packageName = context.packageName
|
||||
val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
|
||||
val className = launchIntent?.component?.className ?: return
|
||||
|
||||
try {
|
||||
val activityClass = Class.forName(className)
|
||||
Intent(context, activityClass).apply {
|
||||
bundle?.let { putExtra("notification", it) }
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context.startActivity(this)
|
||||
}
|
||||
} catch (e: ClassNotFoundException) {
|
||||
Log.e(LOG_TAG, "Class not found", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun getMainActivityClass(): Class<*>? {
|
||||
val packageName = context.packageName
|
||||
val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
|
||||
val className = launchIntent?.component?.className ?: return null
|
||||
|
||||
return try {
|
||||
Class.forName(className)
|
||||
} catch (e: ClassNotFoundException) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun isApplicationInForeground(): Boolean {
|
||||
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||
val processInfos = activityManager.runningAppProcesses ?: return false
|
||||
|
||||
return processInfos.any { processInfo ->
|
||||
processInfo.processName == context.packageName &&
|
||||
processInfo.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND &&
|
||||
processInfo.pkgList.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCircleBitmap(bitmap: Bitmap): Bitmap {
|
||||
val output = Bitmap.createBitmap(bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(output)
|
||||
val paint = Paint().apply {
|
||||
isAntiAlias = true
|
||||
color = Color.RED
|
||||
}
|
||||
val rect = Rect(0, 0, bitmap.width, bitmap.height)
|
||||
val rectF = RectF(rect)
|
||||
|
||||
canvas.drawARGB(0, 0, 0, 0)
|
||||
canvas.drawOval(rectF, paint)
|
||||
|
||||
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
|
||||
canvas.drawBitmap(bitmap, rect, rect, paint)
|
||||
|
||||
bitmap.recycle()
|
||||
return output
|
||||
}
|
||||
|
||||
// Represents a message in a Status conversation
|
||||
data class StatusMessage(
|
||||
val id: String,
|
||||
val author: Person,
|
||||
val timestamp: Long,
|
||||
val text: String
|
||||
)
|
||||
|
||||
// Represents a group of messages in a Status conversation
|
||||
inner class StatusMessageGroup(val id: String) {
|
||||
private val _messages = mutableListOf<StatusMessage>()
|
||||
val messages: List<StatusMessage> get() = _messages.toList()
|
||||
|
||||
fun addMessage(message: StatusMessage) {
|
||||
_messages.add(message)
|
||||
}
|
||||
|
||||
fun removeMessage(id: String) {
|
||||
_messages.removeAll { it.id == id }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPerson(bundle: Bundle): Person {
|
||||
return Person.Builder()
|
||||
.setName(bundle.getString("name"))
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun createMessage(data: Bundle): StatusMessage {
|
||||
val author = getPerson(data.getBundle("notificationAuthor") ?: Bundle())
|
||||
val timeStampLongValue = data.getDouble("timestamp").toLong()
|
||||
return StatusMessage(
|
||||
id = data.getString("id") ?: "",
|
||||
author = author,
|
||||
timestamp = timeStampLongValue,
|
||||
text = data.getString("message") ?: ""
|
||||
)
|
||||
}
|
||||
|
||||
private fun createGroupOnDismissedIntent(
|
||||
context: Context,
|
||||
notificationId: Int,
|
||||
groupId: String,
|
||||
deepLink: String?
|
||||
): PendingIntent {
|
||||
return Intent(ACTION_DELETE_NOTIFICATION).apply {
|
||||
putExtra("im.status.ethereum.deepLink", deepLink)
|
||||
putExtra("im.status.ethereum.groupId", groupId)
|
||||
}.let { intent ->
|
||||
PendingIntent.getBroadcast(
|
||||
context.applicationContext,
|
||||
notificationId,
|
||||
intent,
|
||||
flag
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createGroupOnTapIntent(
|
||||
context: Context,
|
||||
notificationId: Int,
|
||||
groupId: String,
|
||||
deepLink: String?
|
||||
): PendingIntent {
|
||||
return getOpenAppIntent(deepLink)?.let { intent ->
|
||||
PendingIntent.getActivity(
|
||||
context.applicationContext,
|
||||
notificationId,
|
||||
intent,
|
||||
flag
|
||||
)
|
||||
} ?: throw IllegalStateException("Could not create open app intent")
|
||||
}
|
||||
|
||||
private fun createOnTapIntent(
|
||||
context: Context,
|
||||
notificationId: Int,
|
||||
deepLink: String?
|
||||
): PendingIntent {
|
||||
return getOpenAppIntent(deepLink)?.let { intent ->
|
||||
PendingIntent.getActivity(
|
||||
context.applicationContext,
|
||||
notificationId,
|
||||
intent,
|
||||
flag
|
||||
)
|
||||
} ?: throw IllegalStateException("Could not create open app intent")
|
||||
}
|
||||
|
||||
private fun createOnDismissedIntent(
|
||||
context: Context,
|
||||
notificationId: Int,
|
||||
deepLink: String?
|
||||
): PendingIntent {
|
||||
return Intent(ACTION_DELETE_NOTIFICATION).apply {
|
||||
putExtra("im.status.ethereum.deepLink", deepLink)
|
||||
}.let { intent ->
|
||||
PendingIntent.getBroadcast(
|
||||
context.applicationContext,
|
||||
notificationId,
|
||||
intent,
|
||||
flag
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeStatusMessage(bundle: Bundle) {
|
||||
val conversationId = bundle.getString("conversationId") ?: return
|
||||
val group = messageGroups.getOrPut(conversationId) {
|
||||
StatusMessageGroup(conversationId)
|
||||
}
|
||||
|
||||
bundle.getString("id")?.let { id ->
|
||||
group.removeMessage(id)
|
||||
}
|
||||
|
||||
showMessages(bundle)
|
||||
}
|
||||
|
||||
fun getMessageGroup(conversationId: String): StatusMessageGroup? {
|
||||
return messageGroups[conversationId]
|
||||
}
|
||||
|
||||
fun addStatusMessage(bundle: Bundle) {
|
||||
val conversationId = bundle.getString("conversationId") ?: return
|
||||
val group = messageGroups.getOrPut(conversationId) {
|
||||
StatusMessageGroup(conversationId)
|
||||
}
|
||||
|
||||
group.addMessage(createMessage(bundle))
|
||||
showMessages(bundle)
|
||||
}
|
||||
|
||||
fun showMessages(bundle: Bundle) {
|
||||
val conversationId = bundle.getString("conversationId") ?: return
|
||||
val group = messageGroups[conversationId] ?: return
|
||||
|
||||
val messagingStyle = NotificationCompat.MessagingStyle("Me")
|
||||
|
||||
// If there are no messages, cancel the notification and return
|
||||
if (group.messages.isEmpty()) {
|
||||
notificationManager.cancel(conversationId.hashCode())
|
||||
return
|
||||
}
|
||||
|
||||
group.messages.forEach { message ->
|
||||
messagingStyle.addMessage(
|
||||
message.text,
|
||||
message.timestamp,
|
||||
message.author
|
||||
)
|
||||
}
|
||||
|
||||
// Set conversation title if available
|
||||
bundle.getString("title")?.let { title ->
|
||||
messagingStyle.conversationTitle = title
|
||||
}
|
||||
|
||||
val builder = NotificationCompat.Builder(context, CHANNEL_ID).apply {
|
||||
setSmallIcon(R.drawable.ic_stat_notify_status)
|
||||
setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
setStyle(messagingStyle)
|
||||
setGroup(conversationId)
|
||||
setOnlyAlertOnce(true)
|
||||
setGroupSummary(true)
|
||||
setContentIntent(createGroupOnTapIntent(context, conversationId.hashCode(), conversationId, bundle.getString("deepLink")))
|
||||
setDeleteIntent(createGroupOnDismissedIntent(context, conversationId.hashCode(), conversationId, bundle.getString("deepLink")))
|
||||
setNumber(group.messages.size + 1)
|
||||
setAutoCancel(true)
|
||||
|
||||
// Set empty vibration pattern for Android 5.0 and above
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
setVibrate(LongArray(0))
|
||||
}
|
||||
}
|
||||
|
||||
// Show the notification
|
||||
notificationManager.notify(conversationId.hashCode(), builder.build())
|
||||
}
|
||||
|
||||
fun checkOrCreateChannel(
|
||||
manager: NotificationManager?,
|
||||
channelId: String,
|
||||
channelName: String?,
|
||||
channelDescription: String?,
|
||||
soundUri: Uri?,
|
||||
importance: Int,
|
||||
vibratePattern: LongArray?,
|
||||
showBadge: Boolean
|
||||
): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || manager == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
val existingChannel = manager.getNotificationChannel(channelId)
|
||||
val shouldCreateOrUpdate = when {
|
||||
// Channel doesn't exist and we have required info
|
||||
existingChannel == null && channelName != null && channelDescription != null -> true
|
||||
|
||||
// Channel exists but needs update
|
||||
existingChannel != null && (
|
||||
(channelName != null && existingChannel.name != channelName) ||
|
||||
(channelDescription != null && existingChannel.description != channelDescription)
|
||||
) -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
if (shouldCreateOrUpdate) {
|
||||
NotificationChannel(channelId, channelName, importance).apply {
|
||||
description = channelDescription
|
||||
enableLights(true)
|
||||
enableVibration(vibratePattern != null)
|
||||
vibrationPattern = vibratePattern
|
||||
setShowBadge(showBadge)
|
||||
|
||||
// Configure sound
|
||||
if (soundUri != null) {
|
||||
val audioAttributes = AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
|
||||
.build()
|
||||
setSound(soundUri, audioAttributes)
|
||||
} else {
|
||||
setSound(null, null)
|
||||
}
|
||||
|
||||
manager.createNotificationChannel(this)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
fun createChannel(channelInfo: ReadableMap): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
return false
|
||||
}
|
||||
|
||||
return checkOrCreateChannel(
|
||||
manager = notificationManager,
|
||||
channelId = channelInfo.getString("channelId") ?: return false,
|
||||
channelName = channelInfo.getString("channelName"),
|
||||
channelDescription = if (channelInfo.hasKey("channelDescription")) {
|
||||
channelInfo.getString("channelDescription")
|
||||
} else "",
|
||||
soundUri = getSoundUri(
|
||||
if (channelInfo.hasKey("soundName")) {
|
||||
channelInfo.getString("soundName")
|
||||
} else "default"
|
||||
),
|
||||
importance = if (channelInfo.hasKey("importance")) {
|
||||
channelInfo.getInt("importance")
|
||||
} else 4,
|
||||
vibratePattern = if (channelInfo.hasKey("vibrate") && channelInfo.getBoolean("vibrate")) {
|
||||
longArrayOf(DEFAULT_VIBRATION)
|
||||
} else null,
|
||||
showBadge = channelInfo.hasKey("showBadge") && channelInfo.getBoolean("showBadge")
|
||||
)
|
||||
}
|
||||
|
||||
fun getNotificationDefaultChannelId(): String = CHANNEL_ID
|
||||
|
||||
private fun getSoundUri(soundName: String?): Uri {
|
||||
return when {
|
||||
soundName == null || soundName.equals("default", ignoreCase = true) -> {
|
||||
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
|
||||
}
|
||||
else -> {
|
||||
val resources = context.resources
|
||||
val packageName = context.packageName
|
||||
val resId = when {
|
||||
// Try to find the resource as is
|
||||
resources.getIdentifier(soundName, "raw", packageName) != 0 -> {
|
||||
resources.getIdentifier(soundName, "raw", packageName)
|
||||
}
|
||||
// Try without file extension
|
||||
else -> {
|
||||
val nameWithoutExtension = soundName.substringBeforeLast('.')
|
||||
resources.getIdentifier(nameWithoutExtension, "raw", packageName)
|
||||
}
|
||||
}
|
||||
Uri.parse("android.resource://$packageName/$resId")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeGroup(groupId: String) {
|
||||
messageGroups.remove(groupId)
|
||||
}
|
||||
|
||||
private fun cleanGroup(groupId: String) {
|
||||
removeGroup(groupId)
|
||||
if (messageGroups.isEmpty()) {
|
||||
notificationManager.cancelAll()
|
||||
}
|
||||
}
|
||||
|
||||
fun start() {
|
||||
Log.e(LOG_TAG, "Starting Foreground Service")
|
||||
Intent(context, ForegroundService::class.java).also { serviceIntent ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(serviceIntent)
|
||||
} else {
|
||||
context.startService(serviceIntent)
|
||||
}
|
||||
}
|
||||
registerBroadcastReceiver()
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
Log.e(LOG_TAG, "Stopping Foreground Service")
|
||||
// Cancel all current notifications since their intents won't be valid
|
||||
// after the broadcast receiver is killed
|
||||
notificationManager.cancelAll()
|
||||
|
||||
Intent(context, ForegroundService::class.java).also { serviceIntent ->
|
||||
context.stopService(serviceIntent)
|
||||
}
|
||||
context.unregisterReceiver(notificationActionReceiver)
|
||||
}
|
||||
|
||||
fun sendToNotificationCentre(bundle: Bundle) {
|
||||
val aggregator = PushNotificationPicturesAggregator { largeIconImage, bigPictureImage ->
|
||||
sendToNotificationCentreWithPicture(bundle, largeIconImage, bigPictureImage)
|
||||
}
|
||||
|
||||
aggregator.apply {
|
||||
setLargeIconUrl(context, bundle.getString("largeIconUrl"))
|
||||
setBigPictureUrl(context, bundle.getString("bigPictureUrl"))
|
||||
}
|
||||
}
|
||||
|
||||
fun handleConversation(bundle: Bundle) {
|
||||
if (bundle.getBoolean("deleted")) {
|
||||
removeStatusMessage(bundle)
|
||||
} else {
|
||||
addStatusMessage(bundle)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearMessageNotifications(conversationId: String) {
|
||||
notificationManager.cancel(conversationId.hashCode())
|
||||
cleanGroup(conversationId)
|
||||
}
|
||||
|
||||
fun clearAllMessageNotifications() {
|
||||
notificationManager.cancelAll()
|
||||
}
|
||||
|
||||
fun sendToNotificationCentreWithPicture(
|
||||
bundle: Bundle,
|
||||
largeIconBitmap: Bitmap?,
|
||||
bigPictureBitmap: Bitmap?
|
||||
) {
|
||||
try {
|
||||
val intentClass = getMainActivityClass() ?: run {
|
||||
Log.e(LOG_TAG, "No activity class found for the notification")
|
||||
return
|
||||
}
|
||||
|
||||
if (bundle.getBoolean("isConversation")) {
|
||||
handleConversation(bundle)
|
||||
return
|
||||
}
|
||||
|
||||
if (bundle.getString("message") == null) {
|
||||
Log.d(LOG_TAG, "Ignore this message if you sent data-only notification. " +
|
||||
"Cannot send to notification centre because there is no 'message' field in: $bundle")
|
||||
return
|
||||
}
|
||||
|
||||
val notificationIdString = bundle.getString("id") ?: run {
|
||||
Log.e(LOG_TAG, "No notification ID specified for the notification")
|
||||
return
|
||||
}
|
||||
|
||||
val resources = context.resources
|
||||
val packageName = context.packageName
|
||||
|
||||
val title = bundle.getString("title") ?: run {
|
||||
val appInfo = context.applicationInfo
|
||||
context.packageManager.getApplicationLabel(appInfo).toString()
|
||||
}
|
||||
|
||||
val priority = when (bundle.getString("priority")?.toLowerCase()) {
|
||||
"max" -> NotificationCompat.PRIORITY_MAX
|
||||
"high" -> NotificationCompat.PRIORITY_HIGH
|
||||
"low" -> NotificationCompat.PRIORITY_LOW
|
||||
"min" -> NotificationCompat.PRIORITY_MIN
|
||||
"default" -> NotificationCompat.PRIORITY_DEFAULT
|
||||
else -> NotificationCompat.PRIORITY_HIGH
|
||||
}
|
||||
|
||||
val visibility = when (bundle.getString("visibility")?.toLowerCase()) {
|
||||
"private" -> NotificationCompat.VISIBILITY_PRIVATE
|
||||
"public" -> NotificationCompat.VISIBILITY_PUBLIC
|
||||
"secret" -> NotificationCompat.VISIBILITY_SECRET
|
||||
else -> NotificationCompat.VISIBILITY_PRIVATE
|
||||
}
|
||||
|
||||
val channelId = bundle.getString("channelId") ?: getNotificationDefaultChannelId()
|
||||
|
||||
NotificationCompat.Builder(context, channelId).apply {
|
||||
setContentTitle(title)
|
||||
setTicker(bundle.getString("ticker"))
|
||||
setVisibility(visibility)
|
||||
setPriority(priority)
|
||||
setAutoCancel(bundle.getBoolean("autoCancel", true))
|
||||
setOnlyAlertOnce(bundle.getBoolean("onlyAlertOnce", false))
|
||||
|
||||
// Configure notification based on Android version
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
setShowWhen(bundle.getBoolean("showWhen", true))
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
setDefaults(Notification.DEFAULT_LIGHTS)
|
||||
}
|
||||
|
||||
// Handle group settings for Android KitKat Watch and above
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
|
||||
bundle.getString("group")?.let { group ->
|
||||
setGroup(group)
|
||||
}
|
||||
|
||||
if (bundle.containsKey("groupSummary") || bundle.getBoolean("groupSummary")) {
|
||||
setGroupSummary(bundle.getBoolean("groupSummary"))
|
||||
}
|
||||
}
|
||||
|
||||
// Set notification number if provided
|
||||
bundle.getString("number")?.toIntOrNull()?.let { number ->
|
||||
setNumber(number)
|
||||
}
|
||||
|
||||
val smallIconResId = findSmallIconResourceId(resources, packageName, bundle)
|
||||
setSmallIcon(smallIconResId)
|
||||
|
||||
configureLargeIcon(this, largeIconBitmap, bundle, resources, packageName)
|
||||
|
||||
val message = bundle.getString("message") ?: ""
|
||||
setContentText(message)
|
||||
|
||||
bundle.getString("subText")?.let { subText ->
|
||||
setSubText(subText)
|
||||
}
|
||||
|
||||
val bigText = bundle.getString("bigText") ?: message
|
||||
val style = when {
|
||||
bigPictureBitmap != null -> NotificationCompat.BigPictureStyle()
|
||||
.bigPicture(bigPictureBitmap)
|
||||
.setBigContentTitle(title)
|
||||
.setSummaryText(message)
|
||||
else -> NotificationCompat.BigTextStyle().bigText(bigText)
|
||||
}
|
||||
setStyle(style)
|
||||
|
||||
val notificationId = notificationIdString.hashCode()
|
||||
setContentIntent(createOnTapIntent(context, notificationId, bundle.getString("deepLink")))
|
||||
setDeleteIntent(createOnDismissedIntent(context, notificationId, bundle.getString("deepLink")))
|
||||
|
||||
configureSoundAndVibration(this, bundle)
|
||||
|
||||
setOngoing(bundle.getBoolean("ongoing", false))
|
||||
|
||||
// Handle color for Lollipop and above
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
setCategory(NotificationCompat.CATEGORY_CALL)
|
||||
bundle.getString("color")?.let { color ->
|
||||
setColor(Color.parseColor(color))
|
||||
}
|
||||
}
|
||||
|
||||
// Finally show the notification if conditions are met
|
||||
if (!(isApplicationInForeground() && bundle.getBoolean("ignoreInForeground"))) {
|
||||
val notification = build()
|
||||
notification.defaults = notification.defaults or Notification.DEFAULT_LIGHTS
|
||||
|
||||
if (bundle.containsKey("tag")) {
|
||||
notificationManager.notify(bundle.getString("tag"), notificationId, notification)
|
||||
} else {
|
||||
notificationManager.notify(notificationId, notification)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "Failed to send push notification", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findSmallIconResourceId(resources: Resources, packageName: String, bundle: Bundle): Int {
|
||||
val smallIcon = bundle.getString("smallIcon")
|
||||
return when {
|
||||
!smallIcon.isNullOrEmpty() ->
|
||||
resources.getIdentifier(smallIcon, "mipmap", packageName)
|
||||
smallIcon == null ->
|
||||
resources.getIdentifier("ic_stat_notify_status", "drawable", packageName)
|
||||
else -> {
|
||||
val defaultIcon = resources.getIdentifier("ic_launcher", "mipmap", packageName)
|
||||
defaultIcon.takeIf { it != 0 } ?: android.R.drawable.ic_dialog_info
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun configureLargeIcon(
|
||||
builder: NotificationCompat.Builder,
|
||||
largeIconBitmap: Bitmap?,
|
||||
bundle: Bundle,
|
||||
resources: Resources,
|
||||
packageName: String
|
||||
) {
|
||||
var finalLargeIcon = largeIconBitmap
|
||||
|
||||
if (finalLargeIcon == null) {
|
||||
val largeIcon = bundle.getString("largeIcon")
|
||||
val largeIconResId = when {
|
||||
!largeIcon.isNullOrEmpty() ->
|
||||
resources.getIdentifier(largeIcon, "mipmap", packageName)
|
||||
largeIcon == null ->
|
||||
resources.getIdentifier("ic_launcher", "mipmap", packageName)
|
||||
else -> 0
|
||||
}
|
||||
|
||||
if (largeIconResId != 0 && (largeIcon != null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)) {
|
||||
finalLargeIcon = BitmapFactory.decodeResource(resources, largeIconResId)
|
||||
}
|
||||
}
|
||||
|
||||
bundle.getBundle("notificationAuthor")?.getString("icon")?.let { icon ->
|
||||
val base64Image = icon.split(",")[1]
|
||||
val decodedString = Base64.decode(base64Image, Base64.DEFAULT)
|
||||
val decodedByte = BitmapFactory.decodeByteArray(decodedString, 0, decodedString.size)
|
||||
builder.setLargeIcon(getCircleBitmap(decodedByte))
|
||||
} ?: finalLargeIcon?.let { icon ->
|
||||
builder.setLargeIcon(icon)
|
||||
}
|
||||
}
|
||||
|
||||
private fun configureSoundAndVibration(
|
||||
builder: NotificationCompat.Builder,
|
||||
bundle: Bundle
|
||||
) {
|
||||
var soundUri: Uri? = null
|
||||
|
||||
if (!bundle.containsKey("playSound") || bundle.getBoolean("playSound")) {
|
||||
val soundName = bundle.getString("soundName") ?: "default"
|
||||
soundUri = getSoundUri(soundName)
|
||||
builder.setSound(soundUri)
|
||||
}
|
||||
|
||||
if (soundUri == null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
builder.setSound(null)
|
||||
}
|
||||
|
||||
if (!bundle.containsKey("vibrate") || bundle.getBoolean("vibrate")) {
|
||||
val vibration = if (bundle.containsKey("vibration")) {
|
||||
bundle.getDouble("vibration").toLong()
|
||||
} else DEFAULT_VIBRATION
|
||||
|
||||
val finalVibration = if (vibration == 0L) DEFAULT_VIBRATION else vibration
|
||||
builder.setVibrate(longArrayOf(0, finalVibration))
|
||||
}
|
||||
}
|
||||
}
|
||||
-86
@@ -1,86 +0,0 @@
|
||||
package im.status.ethereum.pushnotifications;
|
||||
|
||||
import android.os.Build;
|
||||
import android.app.Application;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static im.status.ethereum.pushnotifications.PushNotification.LOG_TAG;
|
||||
|
||||
class PushNotificationJsDelivery {
|
||||
|
||||
private ReactContext reactContext;
|
||||
|
||||
PushNotificationJsDelivery(ReactContext context){
|
||||
reactContext = context;
|
||||
}
|
||||
|
||||
String convertJSON(Bundle bundle) {
|
||||
try {
|
||||
JSONObject json = convertJSONObject(bundle);
|
||||
return json.toString();
|
||||
} catch (JSONException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// a Bundle is not a map, so we have to convert it explicitly
|
||||
private JSONObject convertJSONObject(Bundle bundle) throws JSONException {
|
||||
JSONObject json = new JSONObject();
|
||||
Set<String> keys = bundle.keySet();
|
||||
for (String key : keys) {
|
||||
Object value = bundle.get(key);
|
||||
if (value instanceof Bundle) {
|
||||
json.put(key, convertJSONObject((Bundle)value));
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
json.put(key, JSONObject.wrap(value));
|
||||
} else {
|
||||
json.put(key, value);
|
||||
}
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
void notifyNotification(Bundle bundle) {
|
||||
String bundleString = convertJSON(bundle);
|
||||
|
||||
WritableMap params = Arguments.createMap();
|
||||
params.putString("dataJSON", bundleString);
|
||||
|
||||
sendEvent("remoteNotificationReceived", params);
|
||||
}
|
||||
|
||||
void notifyNotificationAction(Bundle bundle) {
|
||||
String bundleString = convertJSON(bundle);
|
||||
|
||||
WritableMap params = Arguments.createMap();
|
||||
params.putString("dataJSON", bundleString);
|
||||
|
||||
sendEvent("notificationActionReceived", params);
|
||||
}
|
||||
|
||||
void sendEvent(String eventName, Object params) {
|
||||
if (reactContext.hasActiveCatalystInstance()) {
|
||||
reactContext
|
||||
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||
.emit(eventName, params);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package im.status.ethereum.pushnotifications
|
||||
|
||||
import android.os.Build
|
||||
import android.app.Application
|
||||
import android.app.NotificationManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import com.facebook.react.bridge.ReactContext
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.WritableMap
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import im.status.ethereum.pushnotifications.PushNotification.Companion.LOG_TAG
|
||||
|
||||
class PushNotificationJsDelivery(
|
||||
private val reactContext: ReactContext
|
||||
) {
|
||||
/**
|
||||
* Converts a Bundle to its JSON string representation.
|
||||
* Returns null if conversion fails.
|
||||
*/
|
||||
fun convertJSON(bundle: Bundle): String? {
|
||||
return try {
|
||||
convertJSONObject(bundle).toString()
|
||||
} catch (e: JSONException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Bundle to a JSONObject, handling nested bundles and different value types.
|
||||
* This explicit conversion is necessary because Bundle is not a standard map.
|
||||
*/
|
||||
@Throws(JSONException::class)
|
||||
private fun convertJSONObject(bundle: Bundle): JSONObject {
|
||||
return JSONObject().apply {
|
||||
// Using Kotlin's for loop syntax and 'apply' scope function
|
||||
bundle.keySet().forEach { key ->
|
||||
bundle.get(key)?.let { value ->
|
||||
when (value) {
|
||||
is Bundle -> put(key, convertJSONObject(value))
|
||||
else -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
put(key, JSONObject.wrap(value))
|
||||
} else {
|
||||
put(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies about a received remote notification by sending an event to JavaScript
|
||||
*/
|
||||
fun notifyNotification(bundle: Bundle) {
|
||||
convertJSON(bundle)?.let { bundleString ->
|
||||
Arguments.createMap().apply {
|
||||
putString("dataJSON", bundleString)
|
||||
sendEvent("remoteNotificationReceived", this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies about a notification action by sending an event to JavaScript
|
||||
*/
|
||||
fun notifyNotificationAction(bundle: Bundle) {
|
||||
convertJSON(bundle)?.let { bundleString ->
|
||||
Arguments.createMap().apply {
|
||||
putString("dataJSON", bundleString)
|
||||
sendEvent("notificationActionReceived", this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an event to JavaScript if there's an active Catalyst instance
|
||||
*/
|
||||
fun sendEvent(eventName: String, params: Any) {
|
||||
if (reactContext.hasActiveCatalystInstance()) {
|
||||
reactContext
|
||||
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
||||
.emit(eventName, params)
|
||||
}
|
||||
}
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
package im.status.ethereum.pushnotifications;
|
||||
|
||||
import im.status.ethereum.pushnotifications.PushNotification;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.JavaScriptModule;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class PushNotificationPackage implements ReactPackage {
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
return Collections.<NativeModule>singletonList(new PushNotification(reactContext));
|
||||
}
|
||||
|
||||
public List<Class<? extends JavaScriptModule>> createJSModules() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package im.status.ethereum.pushnotifications
|
||||
|
||||
import im.status.ethereum.pushnotifications.PushNotification
|
||||
import com.facebook.react.ReactPackage
|
||||
import com.facebook.react.bridge.JavaScriptModule
|
||||
import com.facebook.react.bridge.NativeModule
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.uimanager.ViewManager
|
||||
|
||||
class PushNotificationPackage : ReactPackage {
|
||||
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
||||
return listOf(PushNotification(reactContext))
|
||||
}
|
||||
|
||||
// Note: This function is deprecated in newer versions of React Native
|
||||
// but kept for backwards compatibility
|
||||
fun createJSModules(): List<Class<out JavaScriptModule>> {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
-136
@@ -1,136 +0,0 @@
|
||||
package im.status.ethereum.pushnotifications;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.facebook.common.executors.CallerThreadExecutor;
|
||||
import com.facebook.common.references.CloseableReference;
|
||||
import com.facebook.datasource.DataSource;
|
||||
import com.facebook.drawee.backends.pipeline.Fresco;
|
||||
import com.facebook.imagepipeline.common.Priority;
|
||||
import com.facebook.imagepipeline.core.ImagePipeline;
|
||||
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber;
|
||||
import com.facebook.imagepipeline.image.CloseableImage;
|
||||
import com.facebook.imagepipeline.request.ImageRequest;
|
||||
import com.facebook.imagepipeline.request.ImageRequestBuilder;
|
||||
|
||||
import android.util.Log;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static im.status.ethereum.pushnotifications.PushNotification.LOG_TAG;
|
||||
|
||||
public class PushNotificationPicturesAggregator {
|
||||
interface Callback {
|
||||
public void call(Bitmap largeIconImage, Bitmap bigPictureImage);
|
||||
}
|
||||
|
||||
private AtomicInteger count = new AtomicInteger(0);
|
||||
|
||||
private Bitmap largeIconImage;
|
||||
private Bitmap bigPictureImage;
|
||||
|
||||
private Callback callback;
|
||||
|
||||
public PushNotificationPicturesAggregator(Callback callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
public void setBigPicture(Bitmap bitmap) {
|
||||
this.bigPictureImage = bitmap;
|
||||
this.finished();
|
||||
}
|
||||
|
||||
public void setBigPictureUrl(Context context, String url) {
|
||||
if(null == url) {
|
||||
this.setBigPicture(null);
|
||||
return;
|
||||
}
|
||||
|
||||
Uri uri = null;
|
||||
|
||||
try {
|
||||
uri = Uri.parse(url);
|
||||
} catch(Exception ex) {
|
||||
Log.e(LOG_TAG, "Failed to parse bigPictureUrl", ex);
|
||||
this.setBigPicture(null);
|
||||
return;
|
||||
}
|
||||
|
||||
final PushNotificationPicturesAggregator aggregator = this;
|
||||
|
||||
this.downloadRequest(context, uri, new BaseBitmapDataSubscriber() {
|
||||
@Override
|
||||
public void onNewResultImpl(@Nullable Bitmap bitmap) {
|
||||
aggregator.setBigPicture(bitmap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailureImpl(DataSource dataSource) {
|
||||
aggregator.setBigPicture(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setLargeIcon(Bitmap bitmap) {
|
||||
this.largeIconImage = bitmap;
|
||||
this.finished();
|
||||
}
|
||||
|
||||
public void setLargeIconUrl(Context context, String url) {
|
||||
if(null == url) {
|
||||
this.setLargeIcon(null);
|
||||
return;
|
||||
}
|
||||
|
||||
Uri uri = null;
|
||||
|
||||
try {
|
||||
uri = Uri.parse(url);
|
||||
} catch(Exception ex) {
|
||||
Log.e(LOG_TAG, "Failed to parse largeIconUrl", ex);
|
||||
this.setLargeIcon(null);
|
||||
return;
|
||||
}
|
||||
|
||||
final PushNotificationPicturesAggregator aggregator = this;
|
||||
|
||||
this.downloadRequest(context, uri, new BaseBitmapDataSubscriber() {
|
||||
@Override
|
||||
public void onNewResultImpl(@Nullable Bitmap bitmap) {
|
||||
aggregator.setLargeIcon(bitmap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailureImpl(DataSource dataSource) {
|
||||
aggregator.setLargeIcon(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void downloadRequest(Context context, Uri uri, BaseBitmapDataSubscriber subscriber) {
|
||||
ImageRequest imageRequest = ImageRequestBuilder
|
||||
.newBuilderWithSource(uri)
|
||||
.setRequestPriority(Priority.HIGH)
|
||||
.setLowestPermittedRequestLevel(ImageRequest.RequestLevel.FULL_FETCH)
|
||||
.build();
|
||||
|
||||
if(!Fresco.hasBeenInitialized()) {
|
||||
Fresco.initialize(context);
|
||||
}
|
||||
|
||||
DataSource<CloseableReference<CloseableImage>> dataSource = Fresco.getImagePipeline().fetchDecodedImage(imageRequest, context);
|
||||
|
||||
dataSource.subscribe(subscriber, CallerThreadExecutor.getInstance());
|
||||
}
|
||||
|
||||
private void finished() {
|
||||
synchronized(this.count) {
|
||||
int val = this.count.incrementAndGet();
|
||||
|
||||
if(val >= 2 && this.callback != null) {
|
||||
this.callback.call(this.largeIconImage, this.bigPictureImage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package im.status.ethereum.pushnotifications
|
||||
|
||||
import androidx.annotation.Nullable
|
||||
import com.facebook.common.executors.CallerThreadExecutor
|
||||
import com.facebook.common.references.CloseableReference
|
||||
import com.facebook.datasource.DataSource
|
||||
import com.facebook.drawee.backends.pipeline.Fresco
|
||||
import com.facebook.imagepipeline.common.Priority
|
||||
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber
|
||||
import com.facebook.imagepipeline.image.CloseableImage
|
||||
import com.facebook.imagepipeline.request.ImageRequest
|
||||
import com.facebook.imagepipeline.request.ImageRequestBuilder
|
||||
import android.util.Log
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import im.status.ethereum.pushnotifications.PushNotification.Companion.LOG_TAG
|
||||
|
||||
class PushNotificationPicturesAggregator(
|
||||
private val callback: (Bitmap?, Bitmap?) -> Unit
|
||||
) {
|
||||
private val count = AtomicInteger(0)
|
||||
private var largeIconImage: Bitmap? = null
|
||||
private var bigPictureImage: Bitmap? = null
|
||||
|
||||
fun setBigPicture(bitmap: Bitmap?) {
|
||||
bigPictureImage = bitmap
|
||||
finished()
|
||||
}
|
||||
|
||||
fun setBigPictureUrl(context: Context, url: String?) {
|
||||
if (url == null) {
|
||||
setBigPicture(null)
|
||||
return
|
||||
}
|
||||
|
||||
val uri = try {
|
||||
Uri.parse(url)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(LOG_TAG, "Failed to parse bigPictureUrl", ex)
|
||||
setBigPicture(null)
|
||||
return
|
||||
}
|
||||
|
||||
downloadRequest(context, uri, object : BaseBitmapDataSubscriber() {
|
||||
override fun onNewResultImpl(@Nullable bitmap: Bitmap?) {
|
||||
setBigPicture(bitmap)
|
||||
}
|
||||
|
||||
override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage>>) {
|
||||
setBigPicture(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun setLargeIcon(bitmap: Bitmap?) {
|
||||
largeIconImage = bitmap
|
||||
finished()
|
||||
}
|
||||
|
||||
fun setLargeIconUrl(context: Context, url: String?) {
|
||||
if (url == null) {
|
||||
setLargeIcon(null)
|
||||
return
|
||||
}
|
||||
|
||||
val uri = try {
|
||||
Uri.parse(url)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(LOG_TAG, "Failed to parse largeIconUrl", ex)
|
||||
setLargeIcon(null)
|
||||
return
|
||||
}
|
||||
|
||||
downloadRequest(context, uri, object : BaseBitmapDataSubscriber() {
|
||||
override fun onNewResultImpl(@Nullable bitmap: Bitmap?) {
|
||||
setLargeIcon(bitmap)
|
||||
}
|
||||
|
||||
override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage>>) {
|
||||
setLargeIcon(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun downloadRequest(context: Context, uri: Uri, subscriber: BaseBitmapDataSubscriber) {
|
||||
val imageRequest = ImageRequestBuilder
|
||||
.newBuilderWithSource(uri)
|
||||
.setRequestPriority(Priority.HIGH)
|
||||
.setLowestPermittedRequestLevel(ImageRequest.RequestLevel.FULL_FETCH)
|
||||
.build()
|
||||
|
||||
if (!Fresco.hasBeenInitialized()) {
|
||||
Fresco.initialize(context)
|
||||
}
|
||||
|
||||
val dataSource = Fresco.getImagePipeline().fetchDecodedImage(imageRequest, context)
|
||||
dataSource.subscribe(subscriber, CallerThreadExecutor.getInstance())
|
||||
}
|
||||
|
||||
private fun finished() {
|
||||
synchronized(count) {
|
||||
if (count.incrementAndGet() >= 2) {
|
||||
callback(largeIconImage, bigPictureImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
package im.status.ethereum.pushnotifications;
|
||||
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.*;
|
||||
import android.app.Application;
|
||||
import android.app.NotificationManager;
|
||||
import android.os.Bundle;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.IntentFilter;
|
||||
import static org.mockito.Mockito.*;
|
||||
import org.mockito.Spy;
|
||||
|
||||
public class PushNotificationHelperTest {
|
||||
|
||||
PushNotificationHelper helper;
|
||||
|
||||
|
||||
@Test
|
||||
public void testAddStatusMessage() {
|
||||
final Application context = mock(Application.class);
|
||||
final NotificationManager manager = mock(NotificationManager.class);
|
||||
final IntentFilter filter = mock(IntentFilter.class);
|
||||
|
||||
when(context.getSystemService(NotificationManager.class)).thenReturn(manager);
|
||||
|
||||
// Create a spy for PushNotificationHelper
|
||||
PushNotificationHelper realHelper = new PushNotificationHelper(context, filter);
|
||||
helper = spy(realHelper);
|
||||
|
||||
Bundle bundle = mock(Bundle.class);
|
||||
Bundle personBundle = mock(Bundle.class);
|
||||
|
||||
String conversationId = "conversation-id";
|
||||
String deepLink = "deep-link";
|
||||
|
||||
when(bundle.getString("conversationId")).thenReturn(conversationId);
|
||||
when(bundle.getString("id")).thenReturn("id");
|
||||
when(bundle.getString("message")).thenReturn("message");
|
||||
when(bundle.getDouble("timestamp")).thenReturn(100000.0);
|
||||
when(bundle.getString("deepLink")).thenReturn(deepLink);
|
||||
|
||||
when(personBundle.getString("name")).thenReturn("name");
|
||||
when(bundle.getBundle("notificationAuthor")).thenReturn(personBundle);
|
||||
|
||||
|
||||
doNothing().when(helper).showMessages(bundle);
|
||||
|
||||
helper.addStatusMessage(bundle);
|
||||
assertNotNull(helper.getMessageGroup(conversationId));
|
||||
}
|
||||
}
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package im.status.ethereum.pushnotifications
|
||||
|
||||
import android.app.Application
|
||||
import android.app.NotificationManager
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Bundle
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Test
|
||||
import org.mockito.Mockito.*
|
||||
import org.mockito.Spy
|
||||
|
||||
class PushNotificationHelperTest {
|
||||
|
||||
private lateinit var helper: PushNotificationHelper
|
||||
|
||||
@Test
|
||||
fun testAddStatusMessage() {
|
||||
val context = mock(Application::class.java)
|
||||
val manager = mock(NotificationManager::class.java)
|
||||
val filter = mock(IntentFilter::class.java)
|
||||
|
||||
`when`(context.getSystemService(NotificationManager::class.java)).thenReturn(manager)
|
||||
|
||||
// Create a spy for PushNotificationHelper
|
||||
val realHelper = PushNotificationHelper(context, filter)
|
||||
helper = spy(realHelper)
|
||||
|
||||
val bundle = mock(Bundle::class.java)
|
||||
val personBundle = mock(Bundle::class.java)
|
||||
|
||||
val conversationId = "conversation-id"
|
||||
val deepLink = "deep-link"
|
||||
|
||||
`when`(bundle.getString("conversationId")).thenReturn(conversationId)
|
||||
`when`(bundle.getString("id")).thenReturn("id")
|
||||
`when`(bundle.getString("message")).thenReturn("message")
|
||||
`when`(bundle.getDouble("timestamp")).thenReturn(100000.0)
|
||||
`when`(bundle.getString("deepLink")).thenReturn(deepLink)
|
||||
|
||||
`when`(personBundle.getString("name")).thenReturn("name")
|
||||
`when`(bundle.getBundle("notificationAuthor")).thenReturn(personBundle)
|
||||
|
||||
doNothing().`when`(helper).showMessages(bundle)
|
||||
|
||||
helper.addStatusMessage(bundle)
|
||||
assertNotNull(helper.getMessageGroup(conversationId))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user