diff --git a/plugin.xml b/plugin.xml
index 5aeb3d5f8..a096565bd 100755
--- a/plugin.xml
+++ b/plugin.xml
@@ -33,11 +33,17 @@
+
+
+
+
+
+
@@ -71,14 +77,46 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Eingehender Videoanruf
+ Unknown caller
+ Annehmen
+ Ablehnen
+
+
+
+ #00054b
+
+
+
+
+
+
+
+
+
+
diff --git a/src/android/com/adobe/phonegap/push/AndroidUtils.kt b/src/android/com/adobe/phonegap/push/AndroidUtils.kt
new file mode 100755
index 000000000..d5ed6d257
--- /dev/null
+++ b/src/android/com/adobe/phonegap/push/AndroidUtils.kt
@@ -0,0 +1,29 @@
+package com.adobe.phonegap.push
+
+import android.content.Context
+import android.content.Intent
+import android.content.SharedPreferences
+import com.google.firebase.messaging.FirebaseMessagingService
+
+object AndroidUtils {
+
+ /**
+ * Get the Application Name from Label
+ */
+ fun getAppName(context: Context): String {
+ return context.packageManager.getApplicationLabel(context.applicationInfo) as String
+ }
+
+ fun intentForLaunchActivity(context: Context): Intent? {
+ val pm = context.packageManager
+ val packageName = context.packageName
+ return pm?.getLaunchIntentForPackage(packageName)
+ }
+
+ fun getPushSharedPref(context: Context): SharedPreferences {
+ return context.getSharedPreferences(
+ PushConstants.COM_ADOBE_PHONEGAP_PUSH,
+ FirebaseMessagingService.MODE_PRIVATE
+ )
+ }
+}
diff --git a/src/android/com/adobe/phonegap/push/BackgroundActionButtonHandler.kt b/src/android/com/adobe/phonegap/push/BackgroundActionButtonHandler.kt
index 3df4539c0..22c1a2abd 100644
--- a/src/android/com/adobe/phonegap/push/BackgroundActionButtonHandler.kt
+++ b/src/android/com/adobe/phonegap/push/BackgroundActionButtonHandler.kt
@@ -28,7 +28,7 @@ class BackgroundActionButtonHandler : BroadcastReceiver() {
val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- notificationManager.cancel(FCMService.getAppName(context), notId)
+ notificationManager.cancel(AndroidUtils.getAppName(context), notId)
intent.extras?.let { extras ->
Log.d(TAG, "Intent Extras: $extras")
diff --git a/src/android/com/adobe/phonegap/push/BackgroundHandlerActivity.kt b/src/android/com/adobe/phonegap/push/BackgroundHandlerActivity.kt
index fb805731e..82cfc1867 100644
--- a/src/android/com/adobe/phonegap/push/BackgroundHandlerActivity.kt
+++ b/src/android/com/adobe/phonegap/push/BackgroundHandlerActivity.kt
@@ -43,11 +43,11 @@ class BackgroundHandlerActivity : Activity() {
Log.d(TAG, "Start In Background: $startOnBackground")
Log.d(TAG, "Dismissed: $dismissed")
- FCMService().setNotification(notId, "")
+ NotificationUtils.setNotification(notId, "")
if (!startOnBackground) {
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
- notificationManager.cancel(FCMService.getAppName(this), notId)
+ notificationManager.cancel(AndroidUtils.getAppName(this), notId)
}
processPushBundle()
diff --git a/src/android/com/adobe/phonegap/push/FCMService.kt b/src/android/com/adobe/phonegap/push/FCMService.kt
index 890283206..cf4bb6c90 100644
--- a/src/android/com/adobe/phonegap/push/FCMService.kt
+++ b/src/android/com/adobe/phonegap/push/FCMService.kt
@@ -1,36 +1,29 @@
package com.adobe.phonegap.push
+import android.Manifest
import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationManager
import android.app.PendingIntent
-import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
+import android.content.pm.PackageManager
import android.graphics.*
-import android.net.Uri
import android.os.Build
import android.os.Bundle
-import android.provider.Settings
-import android.text.Spanned
import android.util.Log
+import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
-import androidx.core.app.RemoteInput
-import androidx.core.text.HtmlCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.app.TaskStackBuilder
+import com.adobe.phonegap.push.AndroidUtils.getPushSharedPref
import com.adobe.phonegap.push.PushPlugin.Companion.isActive
import com.adobe.phonegap.push.PushPlugin.Companion.isInForeground
import com.adobe.phonegap.push.PushPlugin.Companion.sendExtras
import com.adobe.phonegap.push.PushPlugin.Companion.setApplicationIconBadgeNumber
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
-import org.json.JSONArray
-import org.json.JSONException
-import org.json.JSONObject
-import java.io.IOException
-import java.io.InputStream
-import java.net.HttpURLConnection
-import java.net.URL
import java.security.SecureRandom
import java.util.*
@@ -40,1159 +33,415 @@ import java.util.*
@Suppress("HardCodedStringLiteral")
@SuppressLint("NewApi", "LongLogTag", "LogConditional")
class FCMService : FirebaseMessagingService() {
- companion object {
- private const val TAG = "${PushPlugin.PREFIX_TAG} (FCMService)"
- private val messageMap = HashMap>()
-
- private val FLAG_MUTABLE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- PendingIntent.FLAG_MUTABLE
- } else {
- 0
- }
- private val FLAG_IMMUTABLE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- PendingIntent.FLAG_IMMUTABLE
- } else {
- 0
- }
-
- /**
- * Get the Application Name from Label
- */
- fun getAppName(context: Context): String {
- return context.packageManager.getApplicationLabel(context.applicationInfo) as String
- }
- }
-
- private val context: Context
- get() = applicationContext
-
- private val pushSharedPref: SharedPreferences
- get() = context.getSharedPreferences(
- PushConstants.COM_ADOBE_PHONEGAP_PUSH,
- MODE_PRIVATE
- )
-
- /**
- * Called when a new token is generated, after app install or token changes.
- *
- * @param token
- */
- override fun onNewToken(token: String) {
- super.onNewToken(token)
- Log.d(TAG, "Refreshed token: $token")
-
- // TODO: Implement this method to send any registration to your app's servers.
- //sendRegistrationToServer(token);
- }
-
- /**
- * Set Notification
- * If message is empty or null, the message list is cleared.
- *
- * @param notId
- * @param message
- */
- fun setNotification(notId: Int, message: String?) {
- var messageList = messageMap[notId]
-
- if (messageList == null) {
- messageList = ArrayList()
- messageMap[notId] = messageList
- }
-
- if (message == null || message.isEmpty()) {
- messageList.clear()
- } else {
- messageList.add(message)
- }
- }
-
- /**
- * On Message Received
- */
- override fun onMessageReceived(message: RemoteMessage) {
- val from = message.from
- Log.d(TAG, "onMessageReceived (from=$from)")
-
- var extras = Bundle()
-
- message.notification?.let {
- extras.putString(PushConstants.TITLE, it.title)
- extras.putString(PushConstants.MESSAGE, it.body)
- extras.putString(PushConstants.SOUND, it.sound)
- extras.putString(PushConstants.ICON, it.icon)
- extras.putString(PushConstants.COLOR, it.color)
- }
-
- for ((key, value) in message.data) {
- extras.putString(key, value)
+ companion object {
+ private const val TAG = "${PushPlugin.PREFIX_TAG} (FCMService)"
}
- if (isAvailableSender(from)) {
- val messageKey = pushSharedPref.getString(PushConstants.MESSAGE_KEY, PushConstants.MESSAGE)
- val titleKey = pushSharedPref.getString(PushConstants.TITLE_KEY, PushConstants.TITLE)
-
- extras = normalizeExtras(extras, messageKey, titleKey)
+ private val context: Context
+ get() = applicationContext
- // Clear Badge
- val clearBadge = pushSharedPref.getBoolean(PushConstants.CLEAR_BADGE, false)
- if (clearBadge) {
- setApplicationIconBadgeNumber(context, 0)
- }
+ private val pushSharedPref: SharedPreferences
+ get() = getPushSharedPref(context)
- // Foreground
- extras.putBoolean(PushConstants.FOREGROUND, isInForeground)
+ /**
+ * Called when a new token is generated, after app install or token changes.
+ *
+ * @param token
+ */
+ override fun onNewToken(token: String) {
+ super.onNewToken(token)
+ Log.d(TAG, "Refreshed token: $token")
- // if we are in the foreground and forceShow is `false` only send data
- val forceShow = pushSharedPref.getBoolean(PushConstants.FORCE_SHOW, false)
- if (!forceShow && isInForeground) {
- Log.d(TAG, "Do Not Force & Is In Foreground")
- extras.putBoolean(PushConstants.COLDSTART, false)
- sendExtras(extras)
- } else if (forceShow && isInForeground) {
- Log.d(TAG, "Force & Is In Foreground")
- extras.putBoolean(PushConstants.COLDSTART, false)
- showNotificationIfPossible(extras)
- } else {
- Log.d(TAG, "In Background")
- extras.putBoolean(PushConstants.COLDSTART, isActive)
- showNotificationIfPossible(extras)
- }
+ // TODO: Implement this method to send any registration to your app's servers.
+ //sendRegistrationToServer(token);
}
- }
- private fun replaceKey(oldKey: String, newKey: String, extras: Bundle, newExtras: Bundle) {
- /*
- * Change a values key in the extras bundle
+ /**
+ * On Message Received
*/
- var value = extras[oldKey]
- if (value != null) {
- when (value) {
- is String -> {
- value = localizeKey(newKey, value)
- newExtras.putString(newKey, value as String?)
- }
-
- is Boolean -> newExtras.putBoolean(newKey, (value as Boolean?) ?: return)
-
- is Number -> {
- newExtras.putDouble(newKey, value.toDouble())
+ override fun onMessageReceived(message: RemoteMessage) {
+ val from = message.from
+ Log.d(TAG, "onMessageReceived (from=$from)")
+
+ var extras = Bundle()
+
+ message.notification?.let {
+ extras.putString(PushConstants.TITLE, it.title)
+ extras.putString(PushConstants.MESSAGE, it.body)
+ extras.putString(PushConstants.SOUND, it.sound)
+ extras.putString(PushConstants.ICON, it.icon)
+ extras.putString(PushConstants.COLOR, it.color)
}
- else -> {
- newExtras.putString(newKey, value.toString())
+ for ((key, value) in message.data) {
+ extras.putString(key, value)
}
- }
- }
- }
- private fun localizeKey(key: String, value: String): String {
- /*
- * Normalize localization for key
- */
- return when (key) {
- PushConstants.TITLE,
- PushConstants.MESSAGE,
- PushConstants.SUMMARY_TEXT,
- -> {
- try {
- val localeObject = JSONObject(value)
- val localeKey = localeObject.getString(PushConstants.LOC_KEY)
- val localeFormatData = ArrayList()
+ if (PushUtils.isAvailableSender(pushSharedPref, from)) {
+ val messageKey =
+ pushSharedPref.getString(PushConstants.MESSAGE_KEY, PushConstants.MESSAGE)
+ val titleKey = pushSharedPref.getString(PushConstants.TITLE_KEY, PushConstants.TITLE)
- if (!localeObject.isNull(PushConstants.LOC_DATA)) {
- val localeData = localeObject.getString(PushConstants.LOC_DATA)
- val localeDataArray = JSONArray(localeData)
+ extras = PushUtils.normalizeExtras(context, extras, messageKey, titleKey)
- for (i in 0 until localeDataArray.length()) {
- localeFormatData.add(localeDataArray.getString(i))
+ // Clear Badge
+ val clearBadge = pushSharedPref.getBoolean(PushConstants.CLEAR_BADGE, false)
+ if (clearBadge) {
+ setApplicationIconBadgeNumber(context, 0)
}
- }
-
- val resourceId = context.resources.getIdentifier(
- localeKey,
- "string",
- context.packageName
- )
- if (resourceId != 0) {
- context.resources.getString(resourceId, *localeFormatData.toTypedArray())
- } else {
- Log.d(TAG, "Can't Find Locale Resource (key=$localeKey)")
- value
- }
- } catch (e: JSONException) {
- Log.d(TAG, "No Locale Found (key= $key, error=${e.message})")
- value
- }
- }
- else -> value
- }
- }
-
- private fun normalizeKey(
- key: String,
- messageKey: String?,
- titleKey: String?,
- newExtras: Bundle,
- ): String {
- /*
- * Replace alternate keys with our canonical value
- */
- return when {
- key == PushConstants.BODY
- || key == PushConstants.ALERT
- || key == PushConstants.MP_MESSAGE
- || key == PushConstants.GCM_NOTIFICATION_BODY
- || key == PushConstants.TWILIO_BODY
- || key == messageKey
- || key == PushConstants.AWS_PINPOINT_BODY
- -> {
- PushConstants.MESSAGE
- }
-
- key == PushConstants.TWILIO_TITLE || key == PushConstants.SUBJECT || key == titleKey -> {
- PushConstants.TITLE
- }
-
- key == PushConstants.MSGCNT || key == PushConstants.BADGE -> {
- PushConstants.COUNT
- }
-
- key == PushConstants.SOUNDNAME || key == PushConstants.TWILIO_SOUND -> {
- PushConstants.SOUND
- }
-
- key == PushConstants.AWS_PINPOINT_PICTURE -> {
- newExtras.putString(PushConstants.STYLE, PushConstants.STYLE_PICTURE)
- PushConstants.PICTURE
- }
-
- key.startsWith(PushConstants.GCM_NOTIFICATION) -> {
- key.substring(PushConstants.GCM_NOTIFICATION.length + 1, key.length)
- }
-
- key.startsWith(PushConstants.GCM_N) -> {
- key.substring(PushConstants.GCM_N.length + 1, key.length)
- }
-
- key.startsWith(PushConstants.UA_PREFIX) -> {
- key.substring(PushConstants.UA_PREFIX.length + 1, key.length).lowercase()
- }
-
- key.startsWith(PushConstants.AWS_PINPOINT_PREFIX) -> {
- key.substring(PushConstants.AWS_PINPOINT_PREFIX.length + 1, key.length)
- }
-
- else -> key
- }
- }
-
- private fun normalizeExtras(
- extras: Bundle,
- messageKey: String?,
- titleKey: String?,
- ): Bundle {
- /*
- * Parse bundle into normalized keys.
- */
- Log.d(TAG, "normalize extras")
-
- val it: Iterator = extras.keySet().iterator()
- val newExtras = Bundle()
-
- while (it.hasNext()) {
- val key = it.next()
- Log.d(TAG, "key = $key")
-
- // If normalizeKey, the key is "data" or "message" and the value is a json object extract
- // This is to support parse.com and other services. Issue #147 and pull #218
- if (
- key == PushConstants.PARSE_COM_DATA ||
- key == PushConstants.MESSAGE ||
- key == messageKey
- ) {
- val json = extras[key]
-
- // Make sure data is in json object string format
- if (json is String && json.startsWith("{")) {
- Log.d(TAG, "extracting nested message data from key = $key")
-
- try {
- // If object contains message keys promote each value to the root of the bundle
- val data = JSONObject(json)
- if (
- data.has(PushConstants.ALERT)
- || data.has(PushConstants.MESSAGE)
- || data.has(PushConstants.BODY)
- || data.has(PushConstants.TITLE)
- || data.has(messageKey)
- || data.has(titleKey)
- ) {
- val jsonKeys = data.keys()
-
- while (jsonKeys.hasNext()) {
- var jsonKey = jsonKeys.next()
- Log.d(TAG, "key = data/$jsonKey")
-
- var value = data.getString(jsonKey)
- jsonKey = normalizeKey(jsonKey, messageKey, titleKey, newExtras)
- value = localizeKey(jsonKey, value)
- newExtras.putString(jsonKey, value)
- }
- } else if (data.has(PushConstants.LOC_KEY) || data.has(PushConstants.LOC_DATA)) {
- val newKey = normalizeKey(key, messageKey, titleKey, newExtras)
- Log.d(TAG, "replace key $key with $newKey")
- replaceKey(key, newKey, extras, newExtras)
+ if ("true" == message.data["voip"]) {
+ if ("true" == message.data["isCancelPush"]) {
+ IncomingCallHelper.dismissVOIPNotification(context)
+ IncomingCallActivity.dismissUnlockScreenNotification(this.applicationContext)
+ } else {
+ showVOIPNotification(context, message.data)
+ }
+ } else {
+ // Foreground
+ extras.putBoolean(PushConstants.FOREGROUND, isInForeground)
+
+ // if we are in the foreground and forceShow is `false` only send data
+ val forceShow = pushSharedPref.getBoolean(PushConstants.FORCE_SHOW, false)
+ if (!forceShow && isInForeground) {
+ Log.d(TAG, "Do Not Force & Is In Foreground")
+ extras.putBoolean(PushConstants.COLDSTART, false)
+ sendExtras(extras)
+ } else if (forceShow && isInForeground) {
+ Log.d(TAG, "Force & Is In Foreground")
+ extras.putBoolean(PushConstants.COLDSTART, false)
+ showNotificationIfPossible(context, extras)
+ } else {
+ Log.d(TAG, "In Background")
+ extras.putBoolean(PushConstants.COLDSTART, isActive)
+ showNotificationIfPossible(context, extras)
+ }
}
- } catch (e: JSONException) {
- Log.e(TAG, "normalizeExtras: JSON exception")
- }
- } else {
- val newKey = normalizeKey(key, messageKey, titleKey, newExtras)
- Log.d(TAG, "replace key $key with $newKey")
- replaceKey(key, newKey, extras, newExtras)
- }
- } else if (key == "notification") {
- val value = extras.getBundle(key)
- val iterator: Iterator = value!!.keySet().iterator()
-
- while (iterator.hasNext()) {
- val notificationKey = iterator.next()
- Log.d(TAG, "notificationKey = $notificationKey")
-
- val newKey = normalizeKey(notificationKey, messageKey, titleKey, newExtras)
- Log.d(TAG, "Replace key $notificationKey with $newKey")
-
- var valueData = value.getString(notificationKey)
- valueData = localizeKey(newKey, valueData!!)
- newExtras.putString(newKey, valueData)
}
- continue
- // In case we weren't working on the payload data node or the notification node,
- // normalize the key.
- // This allows to have "message" as the payload data key without colliding
- // with the other "message" key (holding the body of the payload)
- // See issue #1663
- } else {
- val newKey = normalizeKey(key, messageKey, titleKey, newExtras)
- Log.d(TAG, "replace key $key with $newKey")
- replaceKey(key, newKey, extras, newExtras)
- }
- } // while
- return newExtras
- }
-
- private fun extractBadgeCount(extras: Bundle?): Int {
- var count = -1
-
- try {
- extras?.getString(PushConstants.COUNT)?.let {
- count = it.toInt()
- }
- } catch (e: NumberFormatException) {
- Log.e(TAG, e.localizedMessage, e)
}
- return count
- }
-
- private fun showNotificationIfPossible(extras: Bundle?) {
- // Send a notification if there is a message or title, otherwise just send data
- extras?.let {
- val message = it.getString(PushConstants.MESSAGE)
- val title = it.getString(PushConstants.TITLE)
- val contentAvailable = it.getString(PushConstants.CONTENT_AVAILABLE)
- val forceStart = it.getString(PushConstants.FORCE_START)
- val badgeCount = extractBadgeCount(extras)
-
- if (badgeCount >= 0) {
- setApplicationIconBadgeNumber(context, badgeCount)
- }
-
- if (badgeCount == 0) {
- val mNotificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
- mNotificationManager.cancelAll()
- }
-
- Log.d(TAG, "message=$message")
- Log.d(TAG, "title=$title")
- Log.d(TAG, "contentAvailable=$contentAvailable")
- Log.d(TAG, "forceStart=$forceStart")
- Log.d(TAG, "badgeCount=$badgeCount")
-
- val hasMessage = message != null && message.isNotEmpty()
- val hasTitle = title != null && title.isNotEmpty()
-
- if (hasMessage || hasTitle) {
- Log.d(TAG, "Create Notification")
-
- if (!hasTitle) {
- extras.putString(PushConstants.TITLE, getAppName(this))
- }
-
- createNotification(extras)
- }
-
- if (!isActive && forceStart == "1") {
- Log.d(TAG, "The app is not running, attempting to start in the background")
-
- val intent = Intent(this, PushHandlerActivity::class.java).apply {
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- putExtra(PushConstants.PUSH_BUNDLE, extras)
- putExtra(PushConstants.START_IN_BACKGROUND, true)
- putExtra(PushConstants.FOREGROUND, false)
+ private fun createNotification(context: Context, extras: Bundle?) {
+ val mNotificationManager =
+ context.getSystemService(FirebaseMessagingService.NOTIFICATION_SERVICE) as NotificationManager
+ val appName = AndroidUtils.getAppName(context)
+ val notId = PushUtils.parseNotificationIdToInt(extras)
+ val notificationIntent = Intent(context, PushHandlerActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ putExtra(PushConstants.PUSH_BUNDLE, extras)
+ putExtra(PushConstants.NOT_ID, notId)
}
+ val random = SecureRandom()
+ var requestCode = random.nextInt()
- startActivity(intent)
- } else if (contentAvailable == "1") {
- Log.d(
- TAG,
- "The app is not running and content available is true, sending notification event"
- )
-
- sendExtras(extras)
- }
- }
- }
-
- private fun createNotification(extras: Bundle?) {
- val mNotificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
- val appName = getAppName(this)
- val notId = parseNotificationIdToInt(extras)
- val notificationIntent = Intent(this, PushHandlerActivity::class.java).apply {
- addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
- putExtra(PushConstants.PUSH_BUNDLE, extras)
- putExtra(PushConstants.NOT_ID, notId)
- }
- val random = SecureRandom()
- var requestCode = random.nextInt()
- val contentIntent = PendingIntent.getActivity(
- this,
- requestCode,
- notificationIntent,
- PendingIntent.FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE
- )
- val dismissedNotificationIntent = Intent(
- this,
- PushDismissedHandler::class.java
- ).apply {
- putExtra(PushConstants.PUSH_BUNDLE, extras)
- putExtra(PushConstants.NOT_ID, notId)
- putExtra(PushConstants.DISMISSED, true)
-
- action = PushConstants.PUSH_DISMISSED
- }
-
- requestCode = random.nextInt()
-
- val deleteIntent = PendingIntent.getBroadcast(
- this,
- requestCode,
- dismissedNotificationIntent,
- PendingIntent.FLAG_CANCEL_CURRENT or FLAG_IMMUTABLE
- )
-
- val mBuilder: NotificationCompat.Builder =
- createNotificationBuilder(extras, mNotificationManager)
-
- mBuilder.setWhen(System.currentTimeMillis())
- .setContentTitle(fromHtml(extras?.getString(PushConstants.TITLE)))
- .setTicker(fromHtml(extras?.getString(PushConstants.TITLE)))
- .setContentIntent(contentIntent)
- .setDeleteIntent(deleteIntent)
- .setAutoCancel(true)
-
- val localIcon = pushSharedPref.getString(PushConstants.ICON, null)
- val localIconColor = pushSharedPref.getString(PushConstants.ICON_COLOR, null)
- val soundOption = pushSharedPref.getBoolean(PushConstants.SOUND, true)
- val vibrateOption = pushSharedPref.getBoolean(PushConstants.VIBRATE, true)
-
- Log.d(TAG, "stored icon=$localIcon")
- Log.d(TAG, "stored iconColor=$localIconColor")
- Log.d(TAG, "stored sound=$soundOption")
- Log.d(TAG, "stored vibrate=$vibrateOption")
-
- /*
- * Notification Vibration
- */
- setNotificationVibration(extras, vibrateOption, mBuilder)
-
- /*
- * Notification Icon Color
- *
- * Sets the small-icon background color of the notification.
- * To use, add the `iconColor` key to plugin android options
- */
- setNotificationIconColor(extras?.getString(PushConstants.COLOR), mBuilder, localIconColor)
-
- /*
- * Notification Icon
- *
- * Sets the small-icon of the notification.
- *
- * - checks the plugin options for `icon` key
- * - if none, uses the application icon
- *
- * The icon value must be a string that maps to a drawable resource.
- * If no resource is found, falls
- */
- setNotificationSmallIcon(extras, mBuilder, localIcon)
-
- /*
- * Notification Large-Icon
- *
- * Sets the large-icon of the notification
- *
- * - checks the gcm data for the `image` key
- * - checks to see if remote image, loads it.
- * - checks to see if assets image, Loads It.
- * - checks to see if resource image, LOADS IT!
- * - if none, we don't set the large icon
- */
- setNotificationLargeIcon(extras, mBuilder)
-
- /*
- * Notification Sound
- */
- if (soundOption) {
- setNotificationSound(extras, mBuilder)
- }
-
- /*
- * LED Notification
- */
- setNotificationLedColor(extras, mBuilder)
-
- /*
- * Priority Notification
- */
- setNotificationPriority(extras, mBuilder)
-
- /*
- * Notification message
- */
- setNotificationMessage(notId, extras, mBuilder)
-
- /*
- * Notification count
- */
- setNotificationCount(extras, mBuilder)
-
- /*
- * Notification ongoing
- */
- setNotificationOngoing(extras, mBuilder)
-
- /*
- * Notification count
- */
- setVisibility(extras, mBuilder)
-
- /*
- * Notification add actions
- */
- createActions(extras, mBuilder, notId)
- mNotificationManager.notify(appName, notId, mBuilder.build())
- }
-
- private fun createNotificationBuilder(
- extras: Bundle?,
- notificationManager: NotificationManager
- ): NotificationCompat.Builder {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- var channelID: String? = null
-
- if (extras != null) {
- channelID = extras.getString(PushConstants.ANDROID_CHANNEL_ID)
- }
-
- // if the push payload specifies a channel use it
- return if (channelID != null) {
- NotificationCompat.Builder(context, channelID)
- } else {
- val channels = notificationManager.notificationChannels
-
- channelID = if (channels.size == 1) {
- channels[0].id.toString()
- } else {
- PushConstants.DEFAULT_CHANNEL_ID
- }
-
- Log.d(TAG, "Using channel ID = $channelID")
- NotificationCompat.Builder(context, channelID)
- }
- } else {
- return NotificationCompat.Builder(context)
- }
- }
-
- private fun updateIntent(
- intent: Intent,
- callback: String,
- extras: Bundle?,
- foreground: Boolean,
- notId: Int,
- ) {
- intent.apply {
- putExtra(PushConstants.CALLBACK, callback)
- putExtra(PushConstants.PUSH_BUNDLE, extras)
- putExtra(PushConstants.FOREGROUND, foreground)
- putExtra(PushConstants.NOT_ID, notId)
- }
- }
-
- private fun createActions(
- extras: Bundle?,
- mBuilder: NotificationCompat.Builder,
- notId: Int,
- ) {
- Log.d(TAG, "create actions: with in-line")
-
- if (extras == null) {
- Log.d(TAG, "create actions: extras is null, skipping")
- return
- }
-
- val actions = extras.getString(PushConstants.ACTIONS)
- if (actions != null) {
- try {
- val actionsArray = JSONArray(actions)
- val wActions = ArrayList()
-
- for (i in 0 until actionsArray.length()) {
- val min = 1
- val max = 2000000000
- val random = SecureRandom()
- val uniquePendingIntentRequestCode = random.nextInt(max - min + 1) + min
-
- Log.d(TAG, "adding action")
-
- val action = actionsArray.getJSONObject(i)
-
- Log.d(TAG, "adding callback = " + action.getString(PushConstants.CALLBACK))
-
- val foreground = action.optBoolean(PushConstants.FOREGROUND, true)
- val inline = action.optBoolean("inline", false)
- var intent: Intent?
- var pIntent: PendingIntent?
- val callback = action.getString(PushConstants.CALLBACK)
-
- when {
- inline -> {
- Log.d(TAG, "Version: ${Build.VERSION.SDK_INT} = ${Build.VERSION_CODES.M}")
-
- intent = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
- Log.d(TAG, "Push Activity")
- Intent(this, PushHandlerActivity::class.java)
- } else {
- Log.d(TAG, "Push Receiver")
- Intent(this, BackgroundActionButtonHandler::class.java)
- }
-
- updateIntent(intent, callback, extras, foreground, notId)
-
- pIntent = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
- Log.d(TAG, "push activity for notId $notId")
-
+ val contentIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ TaskStackBuilder.create(context).run {
+ addNextIntentWithParentStack(notificationIntent)
PendingIntent.getActivity(
- this,
- uniquePendingIntentRequestCode,
- intent,
- PendingIntent.FLAG_ONE_SHOT or FLAG_MUTABLE
- )
- } else {
- Log.d(TAG, "push receiver for notId $notId")
-
- PendingIntent.getBroadcast(
- this,
- uniquePendingIntentRequestCode,
- intent,
- PendingIntent.FLAG_ONE_SHOT or FLAG_MUTABLE
+ context,
+ requestCode,
+ notificationIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT or NotificationUtils.FLAG_IMMUTABLE
)
- }
- }
-
- foreground -> {
- intent = Intent(this, PushHandlerActivity::class.java)
- updateIntent(intent, callback, extras, foreground, notId)
- pIntent = PendingIntent.getActivity(
- this, uniquePendingIntentRequestCode,
- intent,
- PendingIntent.FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE
- )
}
-
- else -> {
- intent = Intent(this, BackgroundActionButtonHandler::class.java)
- updateIntent(intent, callback, extras, foreground, notId)
- pIntent = PendingIntent.getBroadcast(
- this, uniquePendingIntentRequestCode,
- intent,
- PendingIntent.FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE
- )
- }
- }
- val actionBuilder = NotificationCompat.Action.Builder(
- getImageId(action.optString(PushConstants.ICON, "")),
- action.getString(PushConstants.TITLE),
- pIntent
- )
-
- var remoteInput: RemoteInput?
-
- if (inline) {
- Log.d(TAG, "Create Remote Input")
-
- val replyLabel = action.optString(
- PushConstants.INLINE_REPLY_LABEL,
- "Enter your reply here"
- )
-
- remoteInput = RemoteInput.Builder(PushConstants.INLINE_REPLY)
- .setLabel(replyLabel)
- .build()
-
- actionBuilder.addRemoteInput(remoteInput)
- }
-
- val wAction: NotificationCompat.Action = actionBuilder.build()
- wActions.add(actionBuilder.build())
-
- if (inline) {
- mBuilder.addAction(wAction)
- } else {
- mBuilder.addAction(
- getImageId(action.optString(PushConstants.ICON, "")),
- action.getString(PushConstants.TITLE),
- pIntent
- )
- }
- }
-
- mBuilder.extend(NotificationCompat.WearableExtender().addActions(wActions))
- wActions.clear()
- } catch (e: JSONException) {
- // nope
- }
- }
- }
-
- private fun setNotificationCount(extras: Bundle?, mBuilder: NotificationCompat.Builder) {
- val count = extractBadgeCount(extras)
- if (count >= 0) {
- Log.d(TAG, "count =[$count]")
- mBuilder.setNumber(count)
- }
- }
-
- private fun setVisibility(extras: Bundle?, mBuilder: NotificationCompat.Builder) {
- extras?.getString(PushConstants.VISIBILITY)?.let { visibilityStr ->
- try {
- val visibilityInt = visibilityStr.toInt()
-
- if (
- visibilityInt >= NotificationCompat.VISIBILITY_SECRET
- && visibilityInt <= NotificationCompat.VISIBILITY_PUBLIC
- ) {
- mBuilder.setVisibility(visibilityInt)
} else {
- Log.e(TAG, "Visibility parameter must be between -1 and 1")
- }
- } catch (e: NumberFormatException) {
- e.printStackTrace()
- }
- }
- }
-
- private fun setNotificationVibration(
- extras: Bundle?,
- vibrateOption: Boolean,
- mBuilder: NotificationCompat.Builder,
- ) {
- if (extras == null) {
- Log.d(TAG, "setNotificationVibration: extras is null, skipping")
- return
- }
-
- val vibrationPattern = extras.getString(PushConstants.VIBRATION_PATTERN)
- if (vibrationPattern != null) {
- val items = convertToTypedArray(vibrationPattern)
- val results = LongArray(items.size)
- for (i in items.indices) {
- try {
- results[i] = items[i].trim { it <= ' ' }.toLong()
- } catch (nfe: NumberFormatException) {
- }
- }
- mBuilder.setVibrate(results)
- } else {
- if (vibrateOption) {
- mBuilder.setDefaults(Notification.DEFAULT_VIBRATE)
- }
- }
- }
-
- private fun setNotificationOngoing(extras: Bundle?, mBuilder: NotificationCompat.Builder) {
- extras?.getString(PushConstants.ONGOING, "false")?.let {
- mBuilder.setOngoing(it.toBoolean())
- }
- }
-
- private fun setNotificationMessage(
- notId: Int,
- extras: Bundle?,
- mBuilder: NotificationCompat.Builder,
- ) {
- extras?.let {
- val message = it.getString(PushConstants.MESSAGE)
-
- when (it.getString(PushConstants.STYLE, PushConstants.STYLE_TEXT)) {
- PushConstants.STYLE_INBOX -> {
- setNotification(notId, message)
- mBuilder.setContentText(fromHtml(message))
-
- messageMap[notId]?.let { messageList ->
- val sizeList = messageList.size
-
- if (sizeList > 1) {
- val sizeListMessage = sizeList.toString()
- var stacking: String? = "$sizeList more"
-
- it.getString(PushConstants.SUMMARY_TEXT)?.let { summaryText ->
- stacking = summaryText.replace("%n%", sizeListMessage)
- }
-
- val notificationInbox = NotificationCompat.InboxStyle().run {
- setBigContentTitle(fromHtml(it.getString(PushConstants.TITLE)))
- setSummaryText(fromHtml(stacking))
- }.also { inbox ->
- for (i in messageList.indices.reversed()) {
- inbox.addLine(fromHtml(messageList[i]))
- }
- }
-
- mBuilder.setStyle(notificationInbox)
- } else {
- message?.let { message ->
- val bigText = NotificationCompat.BigTextStyle().run {
- bigText(fromHtml(message))
- setBigContentTitle(fromHtml(it.getString(PushConstants.TITLE)))
- }
-
- mBuilder.setStyle(bigText)
- }
- }
- }
+ PendingIntent.getActivity(
+ context,
+ requestCode,
+ notificationIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT or NotificationUtils.FLAG_IMMUTABLE
+ )
}
- PushConstants.STYLE_PICTURE -> {
- setNotification(notId, "")
- val bigPicture = NotificationCompat.BigPictureStyle().run {
- bigPicture(getBitmapFromURL(it.getString(PushConstants.PICTURE)))
- setBigContentTitle(fromHtml(it.getString(PushConstants.TITLE)))
- setSummaryText(fromHtml(it.getString(PushConstants.SUMMARY_TEXT)))
- }
+ val dismissedNotificationIntent = Intent(
+ context,
+ PushDismissedHandler::class.java
+ ).apply {
+ putExtra(PushConstants.PUSH_BUNDLE, extras)
+ putExtra(PushConstants.NOT_ID, notId)
+ putExtra(PushConstants.DISMISSED, true)
- mBuilder.apply {
- setContentTitle(fromHtml(it.getString(PushConstants.TITLE)))
- setContentText(fromHtml(message))
- setStyle(bigPicture)
- }
+ action = PushConstants.PUSH_DISMISSED
}
- else -> {
- setNotification(notId, "")
+ requestCode = random.nextInt()
- message?.let { messageStr ->
- val bigText = NotificationCompat.BigTextStyle().run {
- bigText(fromHtml(messageStr))
- setBigContentTitle(fromHtml(it.getString(PushConstants.TITLE)))
+ val deleteIntent = PendingIntent.getBroadcast(
+ context,
+ requestCode,
+ dismissedNotificationIntent,
+ PendingIntent.FLAG_CANCEL_CURRENT or NotificationUtils.FLAG_IMMUTABLE
+ )
- it.getString(PushConstants.SUMMARY_TEXT)?.let { summaryText ->
- setSummaryText(fromHtml(summaryText))
- }
- }
+ val mBuilder: NotificationCompat.Builder =
+ NotificationUtils.createNotificationBuilder(context, extras, mNotificationManager)
+
+ mBuilder.setWhen(System.currentTimeMillis())
+ .setContentTitle(extras?.getString(PushConstants.TITLE)?.fromHtml())
+ .setTicker((extras?.getString(PushConstants.TITLE)?.fromHtml()))
+ .setContentIntent(contentIntent)
+ .setDeleteIntent(deleteIntent)
+ .setAutoCancel(true)
+
+ val localIcon = pushSharedPref.getString(PushConstants.ICON, null)
+ val localIconColor = pushSharedPref.getString(PushConstants.ICON_COLOR, null)
+ val soundOption = pushSharedPref.getBoolean(PushConstants.SOUND, true)
+ val vibrateOption = pushSharedPref.getBoolean(PushConstants.VIBRATE, true)
+
+ Log.d(TAG, "stored icon=$localIcon")
+ Log.d(TAG, "stored iconColor=$localIconColor")
+ Log.d(TAG, "stored sound=$soundOption")
+ Log.d(TAG, "stored vibrate=$vibrateOption")
+
+ /*
+ * Notification Vibration
+ */
+ NotificationUtils.setNotificationVibration(extras, vibrateOption, mBuilder)
+
+ /*
+ * Notification Icon Color
+ *
+ * Sets the small-icon background color of the notification.
+ * To use, add the `iconColor` key to plugin android options
+ */
+ PushUtils.setNotificationIconColor(
+ extras?.getString(PushConstants.COLOR),
+ mBuilder,
+ localIconColor
+ )
- mBuilder.setContentText(fromHtml(messageStr))
- mBuilder.setStyle(bigText)
- }
+ /*
+ * Notification Icon
+ *
+ * Sets the small-icon of the notification.
+ *
+ * - checks the plugin options for `icon` key
+ * - if none, uses the application icon
+ *
+ * The icon value must be a string that maps to a drawable resource.
+ * If no resource is found, falls
+ */
+ PushUtils.setNotificationSmallIcon(context, extras, mBuilder, localIcon)
+
+ /*
+ * Notification Large-Icon
+ *
+ * Sets the large-icon of the notification
+ *
+ * - checks the gcm data for the `image` key
+ * - checks to see if remote image, loads it.
+ * - checks to see if assets image, Loads It.
+ * - checks to see if resource image, LOADS IT!
+ * - if none, we don't set the large icon
+ */
+ PushUtils.setNotificationLargeIcon(context, extras, mBuilder)
+
+ /*
+ * Notification Sound
+ */
+ if (soundOption) {
+ NotificationUtils.setNotificationSound(context, extras, mBuilder)
}
- }
- }
- }
- private fun setNotificationSound(extras: Bundle?, mBuilder: NotificationCompat.Builder) {
- extras?.let {
- val soundName = it.getString(PushConstants.SOUNDNAME) ?: it.getString(PushConstants.SOUND)
-
- when {
- soundName == PushConstants.SOUND_RINGTONE -> {
- mBuilder.setSound(Settings.System.DEFAULT_RINGTONE_URI)
+ /*
+ * LED Notification
+ */
+ NotificationUtils.setNotificationLedColor(extras, mBuilder)
+
+ /*
+ * Priority Notification
+ */
+ NotificationUtils.setNotificationPriority(extras, mBuilder)
+
+ /*
+ * Notification message
+ */
+ NotificationUtils.setNotificationMessage(notId, extras, mBuilder)
+
+ /*
+ * Notification count
+ */
+ NotificationUtils.setNotificationCount(extras, mBuilder)
+
+ /*
+ * Notification ongoing
+ */
+ NotificationUtils.setNotificationOngoing(extras, mBuilder)
+
+ /*
+ * Notification count
+ */
+ NotificationUtils.setVisibility(extras, mBuilder)
+
+ /*
+ * Notification add actions
+ */
+ NotificationUtils.createActions(context, extras, mBuilder, notId)
+ mNotificationManager.notify(appName, notId, mBuilder.build())
+ }
+
+ private fun showVOIPNotification(context: Context, messageData: Map) {
+ NotificationUtils.createNotificationChannel(context)
+
+ // Prepare data from messageData
+ var caller: String? = "Unknown caller"
+ if (messageData.containsKey("caller")) {
+ caller = messageData["caller"]
}
+ val callId = messageData["callId"]
+ val callbackUrl = messageData["callbackUrl"]
- soundName != null && !soundName.contentEquals(PushConstants.SOUND_DEFAULT) -> {
- val sound = Uri.parse(
- "${ContentResolver.SCHEME_ANDROID_RESOURCE}://${context.packageName}/raw/$soundName"
- )
-
- Log.d(TAG, "Sound URL: $sound")
-
- mBuilder.setSound(sound)
+ // Read the message title from messageData
+ var title: String? = "Eingehender Anruf"
+ if (messageData.containsKey("body")) {
+ title = messageData["body"]
}
- else -> {
- mBuilder.setSound(Settings.System.DEFAULT_NOTIFICATION_URI)
- }
- }
- }
- }
+ // Update Webhook status to CONNECTED
+ IncomingCallHelper.updateWebhookVOIPStatus(
+ callbackUrl,
+ callId,
+ IncomingCallActivity.VOIP_CONNECTED
+ )
- private fun convertToTypedArray(item: String): Array {
- return item.replace("\\[".toRegex(), "")
- .replace("]".toRegex(), "")
- .split(",")
- .toTypedArray()
- }
+ // Intent for LockScreen or tapping on notification
+ val fullScreenIntent = Intent(context, IncomingCallActivity::class.java)
+ fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ fullScreenIntent.putExtra("caller", caller)
+ fullScreenIntent.putExtra(IncomingCallHelper.EXTRA_CALLBACK_URL, callbackUrl)
+ fullScreenIntent.putExtra(IncomingCallHelper.EXTRA_CALL_ID, callId)
- private fun setNotificationLedColor(extras: Bundle?, mBuilder: NotificationCompat.Builder) {
- extras?.let { it ->
- it.getString(PushConstants.LED_COLOR)?.let { ledColor ->
- // Convert ledColor to Int Typed Array
- val items = convertToTypedArray(ledColor)
- val results = IntArray(items.size)
+ val fullScreenPendingIntent = PendingIntent.getActivity(
+ context, 0, fullScreenIntent,
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ )
- for (i in items.indices) {
- try {
- results[i] = items[i].trim { it <= ' ' }.toInt()
- } catch (nfe: NumberFormatException) {
- Log.e(TAG, "Number Format Exception: $nfe")
- }
- }
+ // Intent for tapping on Answer
+ val acceptIntent = Intent(context, IncomingCallActionHandlerActivity::class.java)
+ acceptIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ acceptIntent.putExtra(IncomingCallHelper.EXTRA_BUTTON_ACTION, IncomingCallActivity.VOIP_ACCEPT)
+ acceptIntent.putExtra(IncomingCallHelper.EXTRA_CALLBACK_URL, callbackUrl)
+ acceptIntent.putExtra(IncomingCallHelper.EXTRA_CALL_ID, callId)
- if (results.size == 4) {
- val (alpha, red, green, blue) = results
- mBuilder.setLights(Color.argb(alpha, red, green, blue), 500, 500)
- } else {
- Log.e(TAG, "ledColor parameter must be an array of length == 4 (ARGB)")
- }
- }
- }
- }
+ val acceptPendingIntent = PendingIntent.getActivity(context, 10,
+ acceptIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
- private fun setNotificationPriority(extras: Bundle?, mBuilder: NotificationCompat.Builder) {
- extras?.let { it ->
- it.getString(PushConstants.PRIORITY)?.let { priorityStr ->
- try {
- val priority = priorityStr.toInt()
+ // Intent for tapping on Reject
+ val declineIntent = Intent(context, IncomingCallActionHandlerActivity::class.java)
+ declineIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ declineIntent.putExtra(IncomingCallHelper.EXTRA_BUTTON_ACTION, IncomingCallActivity.VOIP_DECLINE)
+ declineIntent.putExtra(IncomingCallHelper.EXTRA_CALLBACK_URL, callbackUrl)
+ declineIntent.putExtra(IncomingCallHelper.EXTRA_CALL_ID, callId)
- if (
- priority >= NotificationCompat.PRIORITY_MIN
- && priority <= NotificationCompat.PRIORITY_MAX
- ) {
- mBuilder.priority = priority
- } else {
- Log.e(TAG, "Priority parameter must be between -2 and 2")
- }
- } catch (e: NumberFormatException) {
- e.printStackTrace()
+ val declinePendingIntent = PendingIntent.getActivity(
+ context, 20,
+ declineIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+ val pushicon = resources.getIdentifier("pushicon", "drawable", packageName)
+ val notificationBuilder =
+ NotificationCompat.Builder(context, NotificationUtils.CHANNEL_VOIP)
+ .setSmallIcon(pushicon)
+ .setContentTitle(title)
+ .setContentText(caller)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setCategory(NotificationCompat.CATEGORY_ALARM)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setFullScreenIntent(fullScreenPendingIntent, true) // Show Accept button
+ .addAction(
+ NotificationCompat.Action(
+ 0,
+ "Annehmen",
+ acceptPendingIntent
+ )
+ ) // Show decline action
+ .addAction(
+ NotificationCompat.Action(
+ 0,
+ "Ablehnen",
+ declinePendingIntent
+ )
+ ) // Make notification dismiss on user input action
+ .setAutoCancel(true) // Cannot be swiped by user
+ .setOngoing(true) // Set ringtone to notification (< Android O)
+ .setSound(NotificationUtils.defaultRingtoneUri())
+ val incomingCallNotification: Notification = notificationBuilder.build()
+ val notificationManager = NotificationManagerCompat.from(context)
+
+ // Display notification
+ if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS)
+ != PackageManager.PERMISSION_GRANTED
+ ) {
+ // TODO: Consider calling
+ // ActivityCompat#requestPermissions
+ // here to request the missing permissions, and then overriding
+ // public void onRequestPermissionsResult(int requestCode, String[] permissions,
+ // int[] grantResults)
+ // to handle the case where the user grants the permission. See the documentation
+ // for ActivityCompat#requestPermissions for more details.
+ return
}
- }
+ notificationManager.notify(NotificationUtils.VOIP_NOTIFICATION_ID, incomingCallNotification)
}
- }
-
- private fun getCircleBitmap(bitmap: Bitmap?): Bitmap? {
- if (bitmap == null) {
- return null
- }
-
- val output = Bitmap.createBitmap(
- bitmap.width,
- bitmap.height,
- Bitmap.Config.ARGB_8888
- )
-
- val paint = Paint().apply {
- isAntiAlias = true
- color = Color.RED
- xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
- }
-
- Canvas(output).apply {
- drawARGB(0, 0, 0, 0)
-
- val cx = (bitmap.width / 2).toFloat()
- val cy = (bitmap.height / 2).toFloat()
- val radius = if (cx < cy) cx else cy
- val rect = Rect(0, 0, bitmap.width, bitmap.height)
-
- drawCircle(cx, cy, radius, paint)
- drawBitmap(bitmap, rect, rect, paint)
- }
-
- bitmap.recycle()
- return output
- }
-
- private fun setNotificationLargeIcon(
- extras: Bundle?,
- mBuilder: NotificationCompat.Builder,
- ) {
- extras?.let {
- val gcmLargeIcon = it.getString(PushConstants.IMAGE)
- val imageType = it.getString(PushConstants.IMAGE_TYPE, PushConstants.IMAGE_TYPE_SQUARE)
- if (gcmLargeIcon != null && gcmLargeIcon != "") {
- if (
- gcmLargeIcon.startsWith("http://")
- || gcmLargeIcon.startsWith("https://")
- ) {
- val bitmap = getBitmapFromURL(gcmLargeIcon)
-
- if (PushConstants.IMAGE_TYPE_SQUARE.equals(imageType, ignoreCase = true)) {
- mBuilder.setLargeIcon(bitmap)
- } else {
- val bm = getCircleBitmap(bitmap)
- mBuilder.setLargeIcon(bm)
- }
-
- Log.d(TAG, "Using remote large-icon from GCM")
- } else {
- try {
- val inputStream: InputStream = assets.open(gcmLargeIcon)
+ // END of VoIP implementation
- val bitmap = BitmapFactory.decodeStream(inputStream)
+ private fun showNotificationIfPossible(context: Context, extras: Bundle?) {
+ // Send a notification if there is a message or title, otherwise just send data
+ extras?.let {
+ val message = it.getString(PushConstants.MESSAGE)
+ val title = it.getString(PushConstants.TITLE)
+ val contentAvailable = it.getString(PushConstants.CONTENT_AVAILABLE)
+ val forceStart = it.getString(PushConstants.FORCE_START)
+ val badgeCount = PushUtils.extractBadgeCount(extras)
- if (PushConstants.IMAGE_TYPE_SQUARE.equals(imageType, ignoreCase = true)) {
- mBuilder.setLargeIcon(bitmap)
- } else {
- val bm = getCircleBitmap(bitmap)
- mBuilder.setLargeIcon(bm)
+ if (badgeCount >= 0) {
+ PushPlugin.setApplicationIconBadgeNumber(context, badgeCount)
}
- Log.d(TAG, "Using assets large-icon from GCM")
- } catch (e: IOException) {
- val largeIconId: Int = getImageId(gcmLargeIcon)
-
- if (largeIconId != 0) {
- val largeIconBitmap = BitmapFactory.decodeResource(context.resources, largeIconId)
- mBuilder.setLargeIcon(largeIconBitmap)
- Log.d(TAG, "Using resources large-icon from GCM")
- } else {
- Log.d(TAG, "Not large icon settings")
+ if (badgeCount == 0) {
+ val mNotificationManager =
+ context.getSystemService(FirebaseMessagingService.NOTIFICATION_SERVICE) as NotificationManager
+ mNotificationManager.cancelAll()
}
- }
- }
- }
- }
- }
- private fun getImageId(icon: String): Int {
- var iconId = context.resources.getIdentifier(icon, PushConstants.DRAWABLE, context.packageName)
- if (iconId == 0) {
- iconId = context.resources.getIdentifier(icon, "mipmap", context.packageName)
- }
- return iconId
- }
+ Log.d(TAG, "message=$message")
+ Log.d(TAG, "title=$title")
+ Log.d(TAG, "contentAvailable=$contentAvailable")
+ Log.d(TAG, "forceStart=$forceStart")
+ Log.d(TAG, "badgeCount=$badgeCount")
- private fun setNotificationSmallIcon(
- extras: Bundle?,
- mBuilder: NotificationCompat.Builder,
- localIcon: String?,
- ) {
- extras?.let {
- val icon = it.getString(PushConstants.ICON)
+ val hasMessage = !message.isNullOrEmpty()
+ val hasTitle = !title.isNullOrEmpty()
- val iconId = when {
- icon != null && icon != "" -> {
- getImageId(icon)
- }
+ if (hasMessage || hasTitle) {
+ Log.d(TAG, "Create Notification")
- localIcon != null && localIcon != "" -> {
- getImageId(localIcon)
- }
+ if (!hasTitle) {
+ extras.putString(PushConstants.TITLE, AndroidUtils.getAppName(context))
+ }
- else -> {
- Log.d(TAG, "No icon resource found from settings, using application icon")
- context.applicationInfo.icon
- }
- }
+ createNotification(context, extras)
+ }
- mBuilder.setSmallIcon(iconId)
- }
- }
+ if (!isActive && forceStart == "1") {
+ Log.d(TAG, "The app is not running, attempting to start in the background")
- private fun setNotificationIconColor(
- color: String?,
- mBuilder: NotificationCompat.Builder,
- localIconColor: String?,
- ) {
- val iconColor = when {
- color != null && color != "" -> {
- try {
- Color.parseColor(color)
- } catch (e: IllegalArgumentException) {
- Log.e(TAG, "Couldn't parse color from Android options")
- }
- }
+ val intent = Intent(context, PushHandlerActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ putExtra(PushConstants.PUSH_BUNDLE, extras)
+ putExtra(PushConstants.START_IN_BACKGROUND, true)
+ putExtra(PushConstants.FOREGROUND, false)
+ }
- localIconColor != null && localIconColor != "" -> {
- try {
- Color.parseColor(localIconColor)
- } catch (e: IllegalArgumentException) {
- Log.e(TAG, "Couldn't parse color from android options")
+ context.startActivity(intent)
+ } else if (contentAvailable == "1") {
+ Log.d(
+ TAG,
+ "The app is not running and content available is true, sending notification event"
+ )
+ PushPlugin.sendExtras(extras)
+ }
}
- }
-
- else -> {
- Log.d(TAG, "No icon color settings found")
- 0
- }
- }
-
- if (iconColor != 0) {
- mBuilder.color = iconColor
}
- }
-
- private fun getBitmapFromURL(strURL: String?): Bitmap? {
- return try {
- val url = URL(strURL)
- val connection = (url.openConnection() as HttpURLConnection).apply {
- connectTimeout = 15000
- doInput = true
- connect()
- }
- val input = connection.inputStream
- BitmapFactory.decodeStream(input)
- } catch (e: IOException) {
- e.printStackTrace()
- null
- }
- }
-
- private fun parseNotificationIdToInt(extras: Bundle?): Int {
- var returnVal = 0
-
- try {
- returnVal = extras!!.getString(PushConstants.NOT_ID)!!.toInt()
- } catch (e: NumberFormatException) {
- Log.e(TAG, "NumberFormatException occurred: ${PushConstants.NOT_ID}: ${e.message}")
- } catch (e: Exception) {
- Log.e(TAG, "Exception occurred when parsing ${PushConstants.NOT_ID}: ${e.message}")
- }
-
- return returnVal
- }
-
- private fun fromHtml(source: String?): Spanned? {
- return if (source != null) HtmlCompat.fromHtml(source, HtmlCompat.FROM_HTML_MODE_LEGACY) else null
- }
-
- private fun isAvailableSender(from: String?): Boolean {
- val savedSenderID = pushSharedPref.getString(PushConstants.SENDER_ID, "")
- Log.d(TAG, "sender id = $savedSenderID")
- return from == savedSenderID || from!!.startsWith("/topics/")
- }
}
diff --git a/src/android/com/adobe/phonegap/push/IncomingCallActionHandlerActivity.kt b/src/android/com/adobe/phonegap/push/IncomingCallActionHandlerActivity.kt
new file mode 100755
index 000000000..80bf7555b
--- /dev/null
+++ b/src/android/com/adobe/phonegap/push/IncomingCallActionHandlerActivity.kt
@@ -0,0 +1,32 @@
+package com.adobe.phonegap.push
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+
+class IncomingCallActionHandlerActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ Log.d(LOG_TAG, "onCreate()")
+ handleNotification(this, intent)
+ finish()
+ }
+
+ override fun onNewIntent(intent: Intent) {
+ super.onNewIntent(intent)
+ Log.d(LOG_TAG, "onNewIntent()")
+ handleNotification(this, intent)
+ finish()
+ }
+
+ companion object {
+ private const val LOG_TAG = "Push_IncomingCallActionHandlerActivity"
+
+ private fun handleNotification(context: Context, intent: Intent) {
+ val voipStatus = intent.getStringExtra(IncomingCallHelper.EXTRA_BUTTON_ACTION) ?: return
+ IncomingCallHelper.handleActionCall(context, intent, voipStatus)
+ }
+ }
+}
diff --git a/src/android/com/adobe/phonegap/push/IncomingCallActivity.kt b/src/android/com/adobe/phonegap/push/IncomingCallActivity.kt
new file mode 100755
index 000000000..a08569cd4
--- /dev/null
+++ b/src/android/com/adobe/phonegap/push/IncomingCallActivity.kt
@@ -0,0 +1,223 @@
+package com.adobe.phonegap.push
+
+import android.Manifest
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.app.KeyguardManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.PackageManager
+import android.graphics.drawable.Drawable
+import android.os.Build
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import android.view.WindowManager
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.core.app.ActivityCompat
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.vectordrawable.graphics.drawable.Animatable2Compat
+import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
+
+private const val POST_NOTIFICATIONS_REQUEST_CODE = 8234
+
+class IncomingCallActivity : Activity() {
+
+ var caller: String = ""
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ showWhenLockedAndTurnScreenOn()
+ super.onCreate(savedInstanceState)
+ Log.d("", "IncomingCallActivity.onCreate()")
+ val activityIncomingCallRes = resources.getIdentifier("activity_incoming_call", "layout", packageName)
+ setContentView(activityIncomingCallRes)
+ window.setFlags(
+ WindowManager.LayoutParams.FLAG_FULLSCREEN,
+ WindowManager.LayoutParams.FLAG_FULLSCREEN
+ )
+ instance = this
+
+ val tvCallerRes = resources.getIdentifier("tvCaller", "id", packageName)
+ val btnAcceptRes = resources.getIdentifier("btnAccept", "id", packageName)
+ val btnDeclineRes = resources.getIdentifier("btnDecline", "id", packageName)
+ val ivAnimatedCircleRes = resources.getIdentifier("ivAnimatedCircle", "id", packageName)
+ val circleAnimationAvdRes = resources.getIdentifier("circle_animation_avd", "drawable", packageName)
+
+ caller = intent?.extras?.getString("caller") ?: ""
+ (findViewById(tvCallerRes)).text = caller
+ val btnAccept: Button = findViewById(btnAcceptRes)
+ val btnDecline: Button = findViewById(btnDeclineRes)
+
+ btnAccept.setOnClickListener { v -> requestPhoneUnlock() }
+ btnDecline.setOnClickListener { v -> declineIncomingVoIP() }
+
+ val animatedCircle: ImageView = findViewById(ivAnimatedCircleRes)
+ val drawableCompat = AnimatedVectorDrawableCompat.create(this, circleAnimationAvdRes)
+ animatedCircle.setImageDrawable(drawableCompat)
+ drawableCompat?.registerAnimationCallback(object : Animatable2Compat.AnimationCallback() {
+ private val fHandler = Handler(Looper.getMainLooper())
+ override fun onAnimationEnd(drawable: Drawable?) {
+ super.onAnimationEnd(drawable)
+ if (instance != null) {
+ fHandler.post(drawableCompat::start)
+ }
+ }
+ })
+ drawableCompat?.start()
+ }
+
+ private fun showWhenLockedAndTurnScreenOn() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
+ setShowWhenLocked(true)
+ setTurnScreenOn(true)
+ } else {
+ window.addFlags(
+ WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+ or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+ )
+ }
+ }
+
+ override fun onBackPressed() {
+ // Do nothing on back button
+ }
+
+ private fun requestPhoneUnlock() {
+ val km = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
+ val context = this.applicationContext
+ if (km.isKeyguardLocked) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ km.requestDismissKeyguard(this, object : KeyguardManager.KeyguardDismissCallback() {
+ override fun onDismissSucceeded() {
+ super.onDismissSucceeded()
+ acceptIncomingVoIP()
+ }
+
+ override fun onDismissCancelled() {
+ super.onDismissCancelled()
+ }
+
+ override fun onDismissError() {
+ super.onDismissError()
+ }
+ })
+ } else {
+ acceptIncomingVoIP()
+ if (km.isKeyguardSecure) {
+ // Register receiver for dismissing "Unlock Screen" notification
+ phoneUnlockBR = PhoneUnlockBroadcastReceiver()
+ val filter = IntentFilter()
+ filter.addAction(Intent.ACTION_USER_PRESENT)
+ phoneUnlockBR?.apply {
+ context?.registerReceiver(this as BroadcastReceiver, filter)
+ }
+ showUnlockScreenNotification()
+ } else {
+ val myLock: KeyguardManager.KeyguardLock = km.newKeyguardLock("AnswerCall")
+ myLock?.disableKeyguard()
+ }
+ }
+ } else {
+ acceptIncomingVoIP()
+ }
+ }
+
+ fun acceptIncomingVoIP() {
+ Log.d("IC", "acceptIncomingVoIP")
+ IncomingCallHelper.handleActionCall(applicationContext, intent, VOIP_ACCEPT)
+ }
+
+ private fun declineIncomingVoIP() {
+ Log.d("IC", "declineIncomingVoIP")
+ IncomingCallHelper.handleActionCall(applicationContext, intent, VOIP_DECLINE)
+ }
+
+ @SuppressLint("MissingPermission")
+ private fun showUnlockScreenNotification() {
+ val notificationBuilder = NotificationCompat.Builder(this, PushConstants.DEFAULT_CHANNEL_ID)
+ .setSmallIcon(resources.getIdentifier("pushicon", "drawable", packageName))
+ .setContentTitle("Ongoing call with $caller")
+ .setContentText("Please unlock your device to continue")
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setCategory(NotificationCompat.CATEGORY_MESSAGE)
+ .setAutoCancel(false)
+ .setOngoing(true)
+ .setStyle(NotificationCompat.BigTextStyle())
+ .setSound(null)
+ val ongoingCallNotification = notificationBuilder.build()
+ val notificationManager = NotificationManagerCompat.from(this.applicationContext)
+ // Display notification
+ if (!isPostNotificationsGranted()) {
+ requestPostNotifications()
+ } else {
+ notificationManager.notify(NOTIFICATION_MESSAGE_ID, ongoingCallNotification)
+ }
+ }
+
+ private fun isPostNotificationsGranted(): Boolean {
+ return ActivityCompat.checkSelfPermission(
+ this,
+ Manifest.permission.POST_NOTIFICATIONS
+ ) == PackageManager.PERMISSION_GRANTED
+ }
+
+ private fun requestPostNotifications() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ requestPermissions(
+ arrayOf(Manifest.permission.POST_NOTIFICATIONS),
+ POST_NOTIFICATIONS_REQUEST_CODE
+ )
+ }
+ }
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
+ if (requestCode == POST_NOTIFICATIONS_REQUEST_CODE &&
+ grantResults.getOrNull(0) == PackageManager.PERMISSION_GRANTED
+ ) {
+ showUnlockScreenNotification()
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ Log.d("", "IncomingCallActivity.onCreate()")
+ instance = null
+ }
+
+ class PhoneUnlockBroadcastReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if (intent.action.equals(Intent.ACTION_USER_PRESENT)) {
+ dismissUnlockScreenNotification(context.applicationContext)
+ }
+ }
+ }
+
+ companion object {
+
+ const val VOIP_CONNECTED = "connected"
+ const val VOIP_ACCEPT = "pickup"
+ const val VOIP_DECLINE = "declined_callee"
+ private const val NOTIFICATION_MESSAGE_ID = 1337
+
+ var instance: IncomingCallActivity? = null
+
+ var phoneUnlockBR: PhoneUnlockBroadcastReceiver? = null
+ fun dismissUnlockScreenNotification(applicationContext: Context) {
+ NotificationManagerCompat.from(applicationContext).cancel(NOTIFICATION_MESSAGE_ID)
+ if (phoneUnlockBR != null) {
+ applicationContext.unregisterReceiver(phoneUnlockBR)
+ phoneUnlockBR = null
+ }
+ }
+ }
+}
diff --git a/src/android/com/adobe/phonegap/push/IncomingCallHelper.kt b/src/android/com/adobe/phonegap/push/IncomingCallHelper.kt
new file mode 100755
index 000000000..393714a7f
--- /dev/null
+++ b/src/android/com/adobe/phonegap/push/IncomingCallHelper.kt
@@ -0,0 +1,69 @@
+package com.adobe.phonegap.push
+
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+import androidx.core.app.NotificationManagerCompat
+import okhttp3.Call
+import okhttp3.Callback
+import okhttp3.HttpUrl
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import java.io.IOException
+import kotlin.system.exitProcess
+
+object IncomingCallHelper {
+
+ const val EXTRA_BUTTON_ACTION = "extra_button_action"
+ const val EXTRA_CALLBACK_URL = "extra_callback_url"
+ const val EXTRA_CALL_ID = "extra_call_id"
+
+ fun updateWebhookVOIPStatus(url: String?, callId: String?, status: String, callback: ((Boolean) -> Unit)? = null) {
+
+ val client = OkHttpClient()
+ val urlBuilder = HttpUrl.parse(url)?.newBuilder()
+ urlBuilder?.addQueryParameter("id", callId)
+ urlBuilder?.addQueryParameter("input", status)
+ val urlBuilt: String = urlBuilder?.build().toString()
+ val request = Request.Builder().url(urlBuilt).build()
+ client.newCall(request)
+ .enqueue(object : Callback {
+ override fun onFailure(call: Call, e: IOException) {
+ Log.d("", "Update For CallId $callId and Status $status failed")
+ callback?.invoke(false)
+ }
+ override fun onResponse(call: Call, response: Response) {
+ Log.d("", "Update For CallId $callId and Status $status successful")
+ callback?.invoke(true)
+ }
+ })
+ }
+
+ fun dismissVOIPNotification(context: Context) {
+ NotificationManagerCompat.from(context).cancel(NotificationUtils.VOIP_NOTIFICATION_ID)
+ IncomingCallActivity.instance?.finish()
+ }
+
+ fun handleActionCall(context: Context, intent: Intent, voipStatus: String) {
+ val callbackUrl = intent.getStringExtra(EXTRA_CALLBACK_URL)
+ val callId = intent.getStringExtra(EXTRA_CALL_ID)
+
+ // Handle actiontest
+ dismissVOIPNotification(context)
+
+ // Update Webhook status to CONNECTED
+ updateWebhookVOIPStatus(callbackUrl, callId, voipStatus) { result ->
+ if (result) { checkRedirectIfNext(context, voipStatus) }
+ }
+ }
+
+ private fun checkRedirectIfNext(context: Context, voipStatus: String) {
+ // Start cordova activity on answer
+ if (voipStatus == IncomingCallActivity.VOIP_ACCEPT) {
+ context.startActivity(AndroidUtils.intentForLaunchActivity(context))
+ } else {
+ exitProcess(0)
+ }
+ }
+}
diff --git a/src/android/com/adobe/phonegap/push/NotificationUtils.kt b/src/android/com/adobe/phonegap/push/NotificationUtils.kt
new file mode 100755
index 000000000..ce5582948
--- /dev/null
+++ b/src/android/com/adobe/phonegap/push/NotificationUtils.kt
@@ -0,0 +1,512 @@
+package com.adobe.phonegap.push
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.graphics.Color
+import android.media.AudioAttributes
+import android.media.RingtoneManager
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.provider.Settings
+import android.util.Log
+import androidx.core.app.NotificationCompat
+import androidx.core.app.RemoteInput
+import androidx.core.app.TaskStackBuilder
+import org.json.JSONArray
+import org.json.JSONException
+import java.security.SecureRandom
+import java.util.ArrayList
+import java.util.HashMap
+
+object NotificationUtils {
+ private const val TAG = "${PushPlugin.PREFIX_TAG} (NotificationUtils)"
+
+ private val messageMap = HashMap>()
+
+ private val FLAG_MUTABLE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ PendingIntent.FLAG_MUTABLE
+ } else {
+ 0
+ }
+
+ val FLAG_IMMUTABLE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ PendingIntent.FLAG_IMMUTABLE
+ } else {
+ 0
+ }
+
+ // VoIP
+ const val CHANNEL_VOIP = "Voip"
+ private const val CHANNEL_NAME = "TCVoip"
+ const val VOIP_NOTIFICATION_ID = 168697
+
+ /**
+ * Set Notification
+ * If message is empty or null, the message list is cleared.
+ *
+ * @param notId
+ * @param message
+ */
+ fun setNotification(notId: Int, message: String?) {
+ var messageList = messageMap[notId]
+
+ if (messageList == null) {
+ messageList = ArrayList()
+ messageMap[notId] = messageList
+ }
+
+ if (message.isNullOrEmpty()) {
+ messageList.clear()
+ } else {
+ messageList.add(message)
+ }
+ }
+
+ fun defaultRingtoneUri(): Uri {
+ return RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
+ }
+
+ fun createNotificationChannel(context: Context) {
+ // Create the NotificationChannel, but only on API 26+ because
+ // the NotificationChannel class is new and not in the support library
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val importance: Int = NotificationManager.IMPORTANCE_HIGH
+ val channel = NotificationChannel(CHANNEL_VOIP, CHANNEL_NAME, importance)
+ channel.description = "Channel For VOIP Calls"
+ channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
+ // Set ringtone to notification (>= Android O)
+ val audioAttributes = AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
+ .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
+ .build()
+ channel.setSound(defaultRingtoneUri(), audioAttributes)
+
+ // Register the channel with the system; you can't change the importance
+ // or other notification behaviors after this
+ val notificationManager: NotificationManager = context.getSystemService(NotificationManager::class.java)
+ notificationManager.createNotificationChannel(channel)
+ }
+ }
+
+ fun createNotificationBuilder(
+ context: Context,
+ extras: Bundle?,
+ notificationManager: NotificationManager
+ ): NotificationCompat.Builder {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ var channelID: String? = null
+
+ if (extras != null) {
+ channelID = extras.getString(PushConstants.ANDROID_CHANNEL_ID)
+ }
+
+ // if the push payload specifies a channel use it
+ return if (channelID != null) {
+ NotificationCompat.Builder(context, channelID)
+ } else {
+ val channels = notificationManager.notificationChannels
+
+ channelID = if (channels.size == 1) {
+ channels[0].id.toString()
+ } else {
+ PushConstants.DEFAULT_CHANNEL_ID
+ }
+
+ Log.d(TAG, "Using channel ID = $channelID")
+ NotificationCompat.Builder(context, channelID)
+ }
+ } else {
+ return NotificationCompat.Builder(context)
+ }
+ }
+
+ fun createActions(
+ context: Context,
+ extras: Bundle?,
+ mBuilder: NotificationCompat.Builder,
+ notId: Int,
+ ) {
+ Log.d(TAG, "create actions: with in-line")
+
+ if (extras == null) {
+ Log.d(TAG, "create actions: extras is null, skipping")
+ return
+ }
+
+ val actions = extras.getString(PushConstants.ACTIONS)
+ if (actions != null) {
+ try {
+ val actionsArray = JSONArray(actions)
+ val wActions = ArrayList()
+
+ for (i in 0 until actionsArray.length()) {
+ val min = 1
+ val max = 2000000000
+ val random = SecureRandom()
+ val uniquePendingIntentRequestCode = random.nextInt(max - min + 1) + min
+
+ Log.d(TAG, "adding action")
+
+ val action = actionsArray.getJSONObject(i)
+
+ Log.d(TAG, "adding callback = " + action.getString(PushConstants.CALLBACK))
+
+ val foreground = action.optBoolean(PushConstants.FOREGROUND, true)
+ val inline = action.optBoolean("inline", false)
+ var intent: Intent
+ var pIntent: PendingIntent?
+ val callback = action.getString(PushConstants.CALLBACK)
+
+ when {
+ inline -> {
+ Log.d(TAG, "Version: ${Build.VERSION.SDK_INT} = ${Build.VERSION_CODES.M}")
+
+ intent = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
+ Log.d(TAG, "Push Activity")
+ Intent(context, PushHandlerActivity::class.java)
+ } else {
+ Log.d(TAG, "Push Receiver")
+ Intent(context, BackgroundActionButtonHandler::class.java)
+ }
+
+ PushUtils.updateIntent(intent, callback, extras, foreground, notId)
+
+ pIntent = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
+ Log.d(TAG, "push activity for notId $notId")
+
+ PendingIntent.getActivity(
+ context,
+ uniquePendingIntentRequestCode,
+ intent,
+ PendingIntent.FLAG_ONE_SHOT or NotificationUtils.FLAG_MUTABLE
+ )
+
+ } else if (foreground) {
+ Log.d(TAG, "push receiver for notId $notId")
+ PendingIntent.getBroadcast(
+ context,
+ uniquePendingIntentRequestCode,
+ intent,
+ PendingIntent.FLAG_ONE_SHOT or NotificationUtils.FLAG_MUTABLE
+ )
+ } else {
+ // Only add on platform levels that support FLAG_MUTABLE
+ val flag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE else PendingIntent.FLAG_UPDATE_CURRENT
+ if (context.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.S &&
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ intent = Intent(context, OnNotificationReceiverActivity::class.java)
+ PushUtils.updateIntent(intent, action.getString(PushConstants.CALLBACK), extras, foreground, notId)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ TaskStackBuilder.create(context).run {
+ addNextIntentWithParentStack(intent)
+ PendingIntent.getActivity(context, uniquePendingIntentRequestCode, intent, flag)
+ }
+ } else {
+ PendingIntent.getActivity(context, uniquePendingIntentRequestCode, intent, flag)
+ }
+
+ } else {
+ intent = Intent(context, BackgroundActionButtonHandler::class.java)
+ PushUtils.updateIntent(intent, action.getString(PushConstants.CALLBACK), extras, foreground, notId)
+ PendingIntent.getBroadcast(context, uniquePendingIntentRequestCode, intent, flag)
+ }
+ }
+ }
+
+ foreground -> {
+ intent = Intent(context, PushHandlerActivity::class.java)
+ PushUtils.updateIntent(intent, callback, extras, foreground, notId)
+
+ pIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ TaskStackBuilder.create(context).run {
+ addNextIntentWithParentStack(intent)
+ PendingIntent.getActivity(
+ context, uniquePendingIntentRequestCode,
+ intent, PendingIntent.FLAG_UPDATE_CURRENT or NotificationUtils.FLAG_IMMUTABLE
+ )
+ }
+ } else {
+ PendingIntent.getActivity(
+ context, uniquePendingIntentRequestCode,
+ intent, PendingIntent.FLAG_UPDATE_CURRENT or NotificationUtils.FLAG_IMMUTABLE
+ )
+ }
+ }
+ else -> {
+ intent = Intent(context, BackgroundActionButtonHandler::class.java)
+ PushUtils.updateIntent(intent, callback, extras, foreground, notId)
+ pIntent = PendingIntent.getBroadcast(
+ context, uniquePendingIntentRequestCode,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or NotificationUtils.FLAG_IMMUTABLE
+ )
+ }
+ }
+ val actionBuilder = NotificationCompat.Action.Builder(
+ PushUtils.getImageId(context, action.optString(PushConstants.ICON, "")),
+ action.getString(PushConstants.TITLE),
+ pIntent
+ )
+
+ var remoteInput: RemoteInput?
+
+ if (inline) {
+ Log.d(TAG, "Create Remote Input")
+
+ val replyLabel = action.optString(
+ PushConstants.INLINE_REPLY_LABEL,
+ "Enter your reply here"
+ )
+
+ remoteInput = RemoteInput.Builder(PushConstants.INLINE_REPLY)
+ .setLabel(replyLabel)
+ .build()
+
+ actionBuilder.addRemoteInput(remoteInput)
+ }
+
+ val wAction: NotificationCompat.Action = actionBuilder.build()
+ wActions.add(actionBuilder.build())
+
+ if (inline) {
+ mBuilder.addAction(wAction)
+ } else {
+ mBuilder.addAction(
+ PushUtils.getImageId(context, action.optString(PushConstants.ICON, "")),
+ action.getString(PushConstants.TITLE),
+ pIntent
+ )
+ }
+ }
+
+ mBuilder.extend(NotificationCompat.WearableExtender().addActions(wActions))
+ wActions.clear()
+ } catch (e: JSONException) {
+ // nope
+ }
+ }
+ }
+
+ fun setNotificationCount(extras: Bundle?, mBuilder: NotificationCompat.Builder) {
+ val count = PushUtils.extractBadgeCount(extras)
+ if (count >= 0) {
+ Log.d(TAG, "count =[$count]")
+ mBuilder.setNumber(count)
+ }
+ }
+
+ fun setVisibility(extras: Bundle?, mBuilder: NotificationCompat.Builder) {
+ extras?.getString(PushConstants.VISIBILITY)?.let { visibilityStr ->
+ try {
+ val visibilityInt = PushUtils.getNotificationVisibility(visibilityStr)
+ if (
+ visibilityInt >= NotificationCompat.VISIBILITY_SECRET
+ && visibilityInt <= NotificationCompat.VISIBILITY_PUBLIC
+ ) {
+ mBuilder.setVisibility(visibilityInt)
+ } else {
+ Log.e(TAG, "Visibility parameter must be between -1 and 1")
+ }
+ } catch (e: NumberFormatException) {
+ e.printStackTrace()
+ }
+ }
+ }
+
+ fun setNotificationVibration(
+ extras: Bundle?,
+ vibrateOption: Boolean,
+ mBuilder: NotificationCompat.Builder,
+ ) {
+ if (extras == null) {
+ Log.d(TAG, "setNotificationVibration: extras is null, skipping")
+ return
+ }
+
+ val vibrationPattern = extras.getString(PushConstants.VIBRATION_PATTERN)
+ if (vibrationPattern != null) {
+ val items = vibrationPattern.convertToTypedArray()
+ val results = LongArray(items.size)
+ for (i in items.indices) {
+ try {
+ results[i] = items[i].trim { it <= ' ' }.toLong()
+ } catch (nfe: NumberFormatException) {
+ Log.e(TAG, "", nfe)
+ }
+ }
+ mBuilder.setVibrate(results)
+ } else {
+ if (vibrateOption) {
+ mBuilder.setDefaults(Notification.DEFAULT_VIBRATE)
+ }
+ }
+ }
+
+ fun setNotificationOngoing(extras: Bundle?, mBuilder: NotificationCompat.Builder) {
+ extras?.getString(PushConstants.ONGOING, "false")?.let {
+ mBuilder.setOngoing(it.toBoolean())
+ }
+ }
+
+ fun setNotificationMessage(
+ notId: Int,
+ extras: Bundle?,
+ mBuilder: NotificationCompat.Builder,
+ ) {
+ extras?.let {
+ val message = it.getString(PushConstants.MESSAGE)
+
+ when (it.getString(PushConstants.STYLE, PushConstants.STYLE_TEXT)) {
+ PushConstants.STYLE_INBOX -> {
+ NotificationUtils.setNotification(notId, message)
+ mBuilder.setContentText(message?.fromHtml())
+
+ NotificationUtils.messageMap[notId]?.let { messageList ->
+ val sizeList = messageList.size
+
+ if (sizeList > 1) {
+ val sizeListMessage = sizeList.toString()
+ var stacking: String? = "$sizeList more"
+
+ it.getString(PushConstants.SUMMARY_TEXT)?.let { summaryText ->
+ stacking = summaryText.replace("%n%", sizeListMessage)
+ }
+
+ val notificationInbox = NotificationCompat.InboxStyle().run {
+ setBigContentTitle(it.getString(PushConstants.TITLE)?.fromHtml())
+ setSummaryText(stacking?.fromHtml())
+ }.also { inbox ->
+ for (i in messageList.indices.reversed()) {
+ inbox.addLine(messageList[i]?.fromHtml())
+ }
+ }
+
+ mBuilder.setStyle(notificationInbox)
+ } else {
+ message?.let { message ->
+ val bigText = NotificationCompat.BigTextStyle().run {
+ bigText(message.fromHtml())
+ setBigContentTitle(it.getString(PushConstants.TITLE)?.fromHtml())
+ }
+
+ mBuilder.setStyle(bigText)
+ }
+ }
+ }
+ }
+
+ PushConstants.STYLE_PICTURE -> {
+ NotificationUtils.setNotification(notId, "")
+ val bigPicture = NotificationCompat.BigPictureStyle().run {
+ bigPicture(PushUtils.getBitmapFromURL(it.getString(PushConstants.PICTURE)))
+ setBigContentTitle(it.getString(PushConstants.TITLE)?.fromHtml())
+ setSummaryText(it.getString(PushConstants.SUMMARY_TEXT)?.fromHtml())
+ }
+
+ mBuilder.apply {
+ setContentTitle(it.getString(PushConstants.TITLE)?.fromHtml())
+ setContentText(message?.fromHtml())
+ setStyle(bigPicture)
+ }
+ }
+
+ else -> {
+ NotificationUtils.setNotification(notId, "")
+
+ message?.let { messageStr ->
+ val bigText = NotificationCompat.BigTextStyle().run {
+ bigText(messageStr.fromHtml())
+ setBigContentTitle(it.getString(PushConstants.TITLE)?.fromHtml())
+
+ it.getString(PushConstants.SUMMARY_TEXT)?.let { summaryText ->
+ setSummaryText(summaryText.fromHtml())
+ }
+ }
+
+ mBuilder.setContentText(messageStr.fromHtml())
+ mBuilder.setStyle(bigText)
+ }
+ }
+ }
+ }
+ }
+
+ fun setNotificationSound(context: Context, extras: Bundle?, mBuilder: NotificationCompat.Builder) {
+ extras?.let {
+ val soundName = it.getString(PushConstants.SOUNDNAME) ?: it.getString(PushConstants.SOUND)
+
+ when {
+ soundName == PushConstants.SOUND_RINGTONE -> {
+ mBuilder.setSound(Settings.System.DEFAULT_RINGTONE_URI)
+ }
+
+ soundName != null && !soundName.contentEquals(PushConstants.SOUND_DEFAULT) -> {
+ val sound = Uri.parse(
+ "${ContentResolver.SCHEME_ANDROID_RESOURCE}://${context.packageName}/raw/$soundName"
+ )
+
+ Log.d(TAG, "Sound URL: $sound")
+
+ mBuilder.setSound(sound)
+ }
+
+ else -> {
+ mBuilder.setSound(Settings.System.DEFAULT_NOTIFICATION_URI)
+ }
+ }
+ }
+ }
+
+ fun setNotificationLedColor(extras: Bundle?, mBuilder: NotificationCompat.Builder) {
+ extras?.let { it ->
+ it.getString(PushConstants.LED_COLOR)?.let { ledColor ->
+ // Convert ledColor to Int Typed Array
+ val items = ledColor.convertToTypedArray()
+ val results = IntArray(items.size)
+
+ for (i in items.indices) {
+ try {
+ results[i] = items[i].trim { it <= ' ' }.toInt()
+ } catch (nfe: NumberFormatException) {
+ Log.e(TAG, "Number Format Exception: $nfe")
+ }
+ }
+
+ if (results.size == 4) {
+ val (alpha, red, green, blue) = results
+ mBuilder.setLights(Color.argb(alpha, red, green, blue), 500, 500)
+ } else {
+ Log.e(TAG, "ledColor parameter must be an array of length == 4 (ARGB)")
+ }
+ }
+ }
+ }
+
+ fun setNotificationPriority(extras: Bundle?, mBuilder: NotificationCompat.Builder) {
+ extras?.let { it ->
+ it.getString(PushConstants.PRIORITY)?.let { priorityStr ->
+ try {
+ val priority = priorityStr.toInt()
+
+ if (
+ priority >= NotificationCompat.PRIORITY_MIN
+ && priority <= NotificationCompat.PRIORITY_MAX
+ ) {
+ mBuilder.priority = priority
+ } else {
+ Log.e(TAG, "Priority parameter must be between -2 and 2")
+ }
+ } catch (e: NumberFormatException) {
+ e.printStackTrace()
+ }
+ }
+ }
+ }
+}
diff --git a/src/android/com/adobe/phonegap/push/OnNotificationReceiverActivity.kt b/src/android/com/adobe/phonegap/push/OnNotificationReceiverActivity.kt
new file mode 100644
index 000000000..7ff298fab
--- /dev/null
+++ b/src/android/com/adobe/phonegap/push/OnNotificationReceiverActivity.kt
@@ -0,0 +1,45 @@
+package com.adobe.phonegap.push
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+
+class OnNotificationReceiverActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ Log.d(LOG_TAG, "OnNotificationReceiverActivity.onCreate()")
+ handleNotification(this, getIntent())
+ finish()
+ }
+
+ override fun onNewIntent(intent: Intent) {
+ super.onNewIntent(intent)
+ Log.d(LOG_TAG, "OnNotificationReceiverActivity.onNewIntent()")
+ handleNotification(this, intent)
+ finish()
+ }
+
+ companion object {
+ private const val LOG_TAG = "Push_OnNotificationReceiverActivity"
+ private fun handleNotification(context: Context, intent: Intent) {
+ try {
+ val pm = context.packageManager
+ val launchIntent = pm.getLaunchIntentForPackage(context.getPackageName())
+ launchIntent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
+ val data = intent.extras
+ if (data?.containsKey("messageType") == false ) { data?.putString("messageType", "notification") }
+ data?.putString("tap", if (PushPlugin.isInBackground) "background" else "foreground")
+ Log.d(LOG_TAG, "OnNotificationReceiverActivity.handleNotification(): " + data.toString())
+ PushPlugin.sendExtras(data)
+ data?.apply {
+ launchIntent?.putExtras(data)
+ }
+ context.startActivity(launchIntent)
+ } catch (e: Exception) {
+ Log.e(LOG_TAG, e.localizedMessage, e)
+ }
+ }
+ }
+}
diff --git a/src/android/com/adobe/phonegap/push/PushDismissedHandler.kt b/src/android/com/adobe/phonegap/push/PushDismissedHandler.kt
index 92c19db4f..217bee348 100644
--- a/src/android/com/adobe/phonegap/push/PushDismissedHandler.kt
+++ b/src/android/com/adobe/phonegap/push/PushDismissedHandler.kt
@@ -24,7 +24,7 @@ class PushDismissedHandler : BroadcastReceiver() {
if (intent.action == PushConstants.PUSH_DISMISSED) {
val notID = intent.getIntExtra(PushConstants.NOT_ID, 0)
Log.d(TAG, "not id = $notID")
- FCMService().setNotification(notID, "")
+ NotificationUtils.setNotification(notID, "")
}
}
}
diff --git a/src/android/com/adobe/phonegap/push/PushHandlerActivity.kt b/src/android/com/adobe/phonegap/push/PushHandlerActivity.kt
index 1a60c078f..71d241836 100644
--- a/src/android/com/adobe/phonegap/push/PushHandlerActivity.kt
+++ b/src/android/com/adobe/phonegap/push/PushHandlerActivity.kt
@@ -39,11 +39,11 @@ class PushHandlerActivity : Activity() {
val startOnBackground = extras.getBoolean(PushConstants.START_IN_BACKGROUND, false)
val dismissed = extras.getBoolean(PushConstants.DISMISSED, false)
- FCMService().setNotification(notId, "")
+ NotificationUtils.setNotification(notId, "")
if (!startOnBackground) {
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
- notificationManager.cancel(FCMService.getAppName(this), notId)
+ notificationManager.cancel(AndroidUtils.getAppName(this), notId)
}
val notHaveInlineReply = processPushBundle()
diff --git a/src/android/com/adobe/phonegap/push/PushPlugin.kt b/src/android/com/adobe/phonegap/push/PushPlugin.kt
index 904c4262b..1cc989a6b 100644
--- a/src/android/com/adobe/phonegap/push/PushPlugin.kt
+++ b/src/android/com/adobe/phonegap/push/PushPlugin.kt
@@ -35,892 +35,900 @@ import java.util.concurrent.ExecutionException
@Suppress("HardCodedStringLiteral")
@SuppressLint("LongLogTag", "LogConditional")
class PushPlugin : CordovaPlugin() {
- companion object {
- const val PREFIX_TAG: String = "cordova-plugin-push"
- private const val TAG: String = "$PREFIX_TAG (PushPlugin)"
+ companion object {
+ const val PREFIX_TAG: String = "cordova-plugin-push"
+ private const val TAG: String = "$PREFIX_TAG (PushPlugin)"
- private const val REQ_CODE_INITIALIZE_PLUGIN = 0
+ private const val REQ_CODE_INITIALIZE_PLUGIN = 0
- /**
- * Is the WebView in the foreground?
- */
- var isInForeground: Boolean = false
+ /**
+ * Is the WebView in the foreground?
+ */
+ var isInForeground: Boolean = false
- private var pushContext: CallbackContext? = null
- private var pluginInitData: JSONArray? = null
- private var gWebView: CordovaWebView? = null
- private val gCachedExtras = Collections.synchronizedList(ArrayList())
+ private var pushContext: CallbackContext? = null
+ private var pluginInitData: JSONArray? = null
+ private var gWebView: CordovaWebView? = null
+ private val gCachedExtras = Collections.synchronizedList(ArrayList())
- /**
- *
- */
- fun sendEvent(json: JSONObject?) {
- val pluginResult = PluginResult(PluginResult.Status.OK, json)
- .apply { keepCallback = true }
- pushContext?.sendPluginResult(pluginResult)
- }
+ /**
+ *
+ */
+ fun sendEvent(json: JSONObject?) {
+ val pluginResult = PluginResult(PluginResult.Status.OK, json)
+ .apply { keepCallback = true }
+ pushContext?.sendPluginResult(pluginResult)
+ }
+
+ /**
+ * Sends the push bundle extras to the client application. If the client
+ * application isn't currently active and the no-cache flag is not set, it is
+ * cached for later processing.
+ *
+ * @param extras
+ */
+ @JvmStatic
+ fun sendExtras(extras: Bundle?) {
+ /**
+ * Serializes a bundle to JSON.
+ *
+ * @param extras
+ *
+ * @return JSONObject|null
+ */
+ fun convertBundleToJson(extras: Bundle): JSONObject? {
+ Log.d(TAG, "Convert Extras to JSON")
- /**
- * Sends the push bundle extras to the client application. If the client
- * application isn't currently active and the no-cache flag is not set, it is
- * cached for later processing.
- *
- * @param extras
- */
- @JvmStatic
- fun sendExtras(extras: Bundle?) {
- /**
- * Serializes a bundle to JSON.
- *
- * @param extras
- *
- * @return JSONObject|null
- */
- fun convertBundleToJson(extras: Bundle): JSONObject? {
- Log.d(TAG, "Convert Extras to JSON")
-
- try {
- val json = JSONObject()
- val additionalData = JSONObject()
-
- // Add any keys that need to be in top level json to this set
- val jsonKeySet: HashSet = HashSet()
-
- Collections.addAll(
- jsonKeySet,
- PushConstants.TITLE,
- PushConstants.MESSAGE,
- PushConstants.COUNT,
- PushConstants.SOUND,
- PushConstants.IMAGE
- )
-
- val it: Iterator = extras.keySet().iterator()
-
- while (it.hasNext()) {
- val key = it.next()
- val value = extras[key]
-
- Log.d(TAG, "Extras Iteration: key=$key")
-
- when {
- jsonKeySet.contains(key) -> {
- json.put(key, value)
- }
-
- key == PushConstants.COLDSTART -> {
- additionalData.put(key, extras.getBoolean(PushConstants.COLDSTART))
- }
-
- key == PushConstants.FOREGROUND -> {
- additionalData.put(key, extras.getBoolean(PushConstants.FOREGROUND))
- }
-
- key == PushConstants.DISMISSED -> {
- additionalData.put(key, extras.getBoolean(PushConstants.DISMISSED))
- }
-
- value is String -> {
try {
- // Try to figure out if the value is another JSON object
- when {
- value.startsWith("{") -> {
- additionalData.put(key, JSONObject(value))
+ val json = JSONObject()
+ val additionalData = JSONObject()
+
+ // Add any keys that need to be in top level json to this set
+ val jsonKeySet: HashSet = HashSet()
+
+ Collections.addAll(
+ jsonKeySet,
+ PushConstants.TITLE,
+ PushConstants.MESSAGE,
+ PushConstants.COUNT,
+ PushConstants.SOUND,
+ PushConstants.IMAGE
+ )
+
+ val it: Iterator = extras.keySet().iterator()
+
+ while (it.hasNext()) {
+ val key = it.next()
+ val value = extras[key]
+
+ Log.d(TAG, "Extras Iteration: key=$key")
+
+ when {
+ jsonKeySet.contains(key) -> {
+ json.put(key, value)
+ }
+
+ key == PushConstants.COLDSTART -> {
+ additionalData.put(key, extras.getBoolean(PushConstants.COLDSTART))
+ }
+
+ key == PushConstants.FOREGROUND -> {
+ additionalData.put(key, extras.getBoolean(PushConstants.FOREGROUND))
+ }
+
+ key == PushConstants.DISMISSED -> {
+ additionalData.put(key, extras.getBoolean(PushConstants.DISMISSED))
+ }
+
+ value is String -> {
+ try {
+ // Try to figure out if the value is another JSON object
+ when {
+ value.startsWith("{") -> {
+ additionalData.put(key, JSONObject(value))
+ }
+
+ value.startsWith("[") -> {
+ additionalData.put(key, JSONArray(value))
+ }
+
+ else -> {
+ additionalData.put(key, value)
+ }
+ }
+ } catch (e: Exception) {
+ additionalData.put(key, value)
+ }
+ }
+ }
}
- value.startsWith("[") -> {
- additionalData.put(key, JSONArray(value))
+ json.put(PushConstants.ADDITIONAL_DATA, additionalData)
+
+ Log.v(TAG, "Extras To JSON Result: $json")
+ return json
+ } catch (e: JSONException) {
+ Log.e(TAG, "convertBundleToJson had a JSON Exception")
+ }
+
+ return null
+ }
+
+ extras?.let {
+ val noCache = it.getString(PushConstants.NO_CACHE)
+
+ if (gWebView != null) {
+ sendEvent(convertBundleToJson(extras))
+ } else if (noCache != "1") {
+ Log.v(TAG, "sendExtras: Caching extras to send at a later time.")
+ gCachedExtras.add(extras)
+ }
+ }
+ }
+
+ /**
+ * Retrieves the badge count from SharedPreferences
+ *
+ * @param context
+ *
+ * @return Int
+ */
+ fun getApplicationIconBadgeNumber(context: Context): Int {
+ val settings = context.getSharedPreferences(PushConstants.BADGE, Context.MODE_PRIVATE)
+ return settings.getInt(PushConstants.BADGE, 0)
+ }
+
+ /**
+ * Sets badge count on application icon and in SharedPreferences
+ *
+ * @param context
+ * @param badgeCount
+ */
+ @JvmStatic
+ fun setApplicationIconBadgeNumber(context: Context, badgeCount: Int) {
+ if (badgeCount > 0) {
+ ShortcutBadger.applyCount(context, badgeCount)
+ } else {
+ ShortcutBadger.removeCount(context)
+ }
+
+ context.getSharedPreferences(PushConstants.BADGE, Context.MODE_PRIVATE)
+ .edit()?.apply {
+ putInt(PushConstants.BADGE, badgeCount.coerceAtLeast(0))
+ apply()
+ }
+ }
+
+ val isInBackground: Boolean
+ get() = !isInForeground
+
+ /**
+ * @return Boolean Active is true when the Cordova WebView is present.
+ */
+ val isActive: Boolean
+ get() = gWebView != null
+ }
+
+ private val activity: Activity
+ get() = cordova.activity
+
+ private val applicationContext: Context
+ get() = activity.applicationContext
+
+ private val notificationManager: NotificationManager
+ get() = (activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager)
+
+ private val appName: String
+ get() = activity.packageManager.getApplicationLabel(activity.applicationInfo) as String
+
+ @TargetApi(26)
+ @Throws(JSONException::class)
+ private fun listChannels(): JSONArray {
+ val channels = JSONArray()
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val notificationChannels = notificationManager.notificationChannels
+
+ for (notificationChannel in notificationChannels) {
+ val channel = JSONObject().apply {
+ put(PushConstants.CHANNEL_ID, notificationChannel.id)
+ put(PushConstants.CHANNEL_DESCRIPTION, notificationChannel.description)
+ }
+
+ channels.put(channel)
+ }
+ }
+
+ return channels
+ }
+
+ @TargetApi(26)
+ private fun deleteChannel(channelId: String) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ notificationManager.deleteNotificationChannel(channelId)
+ }
+ }
+
+ @TargetApi(26)
+ @Throws(JSONException::class)
+ private fun createChannel(channel: JSONObject?) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ channel?.let {
+ NotificationChannel(
+ it.getString(PushConstants.CHANNEL_ID),
+ it.optString(PushConstants.CHANNEL_DESCRIPTION, appName),
+ it.optInt(
+ PushConstants.CHANNEL_IMPORTANCE,
+ NotificationManager.IMPORTANCE_DEFAULT
+ )
+ ).apply {
+ /**
+ * Enable Lights when Light Color is set.
+ */
+ val mLightColor = it.optInt(PushConstants.CHANNEL_LIGHT_COLOR, -1)
+ if (mLightColor != -1) {
+ enableLights(true)
+ lightColor = mLightColor
}
- else -> {
- additionalData.put(key, value)
+ /**
+ * Set Lock Screen Visibility.
+ */
+ lockscreenVisibility = channel.optInt(
+ PushConstants.VISIBILITY,
+ NotificationCompat.VISIBILITY_PUBLIC
+ )
+
+ /**
+ * Set if badge should be shown
+ */
+ setShowBadge(it.optBoolean(PushConstants.BADGE, true))
+
+ /**
+ * Sound Settings
+ */
+ val (soundUri, audioAttributes) = getNotificationChannelSound(it)
+ setSound(soundUri, audioAttributes)
+
+ /**
+ * Set vibration settings.
+ * Data can be either JSONArray or Boolean value.
+ */
+ val (hasVibration, vibrationPatternArray) = getNotificationChannelVibration(it)
+ if (vibrationPatternArray != null) {
+ vibrationPattern = vibrationPatternArray
+ } else {
+ enableVibration(hasVibration)
}
- }
- } catch (e: Exception) {
- additionalData.put(key, value)
+
+ notificationManager.createNotificationChannel(this)
}
- }
}
- }
+ }
+ }
- json.put(PushConstants.ADDITIONAL_DATA, additionalData)
+ private fun getNotificationChannelSound(channelData: JSONObject): Pair {
+ val audioAttributes = AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+ .build()
+
+ val sound = channelData.optString(PushConstants.SOUND, PushConstants.SOUND_DEFAULT)
+
+ return when {
+ sound == PushConstants.SOUND_RINGTONE -> Pair(
+ Settings.System.DEFAULT_RINGTONE_URI,
+ audioAttributes
+ )
+
+ // Disable sound for this notification channel if an empty string is passed.
+ // https://stackoverflow.com/a/47144981/6194193
+ sound.isEmpty() -> Pair(null, null)
+
+ // E.g. android.resource://org.apache.cordova/raw/
+ sound != PushConstants.SOUND_DEFAULT -> {
+ val scheme = ContentResolver.SCHEME_ANDROID_RESOURCE
+ val packageName = applicationContext.packageName
+
+ Pair(
+ Uri.parse("${scheme}://$packageName/raw/$sound"),
+ audioAttributes
+ )
+ }
- Log.v(TAG, "Extras To JSON Result: $json")
- return json
- } catch (e: JSONException) {
- Log.e(TAG, "convertBundleToJson had a JSON Exception")
+ else -> Pair(Settings.System.DEFAULT_NOTIFICATION_URI, audioAttributes)
}
+ }
- return null
- }
+ private fun getNotificationChannelVibration(channelData: JSONObject): Pair {
+ var patternArray: LongArray? = null
+ val mVibrationPattern = channelData.optJSONArray(PushConstants.CHANNEL_VIBRATION)
- extras?.let {
- val noCache = it.getString(PushConstants.NO_CACHE)
+ if (mVibrationPattern != null) {
+ val patternLength = mVibrationPattern.length()
+ patternArray = LongArray(patternLength)
- if (gWebView != null) {
- sendEvent(convertBundleToJson(extras))
- } else if (noCache != "1") {
- Log.v(TAG, "sendExtras: Caching extras to send at a later time.")
- gCachedExtras.add(extras)
+ for (i in 0 until patternLength) {
+ patternArray[i] = mVibrationPattern.optLong(i)
+ }
}
- }
+
+ return Pair(
+ channelData.optBoolean(PushConstants.CHANNEL_VIBRATION, true),
+ patternArray
+ )
}
- /**
- * Retrieves the badge count from SharedPreferences
- *
- * @param context
- *
- * @return Int
- */
- fun getApplicationIconBadgeNumber(context: Context): Int {
- val settings = context.getSharedPreferences(PushConstants.BADGE, Context.MODE_PRIVATE)
- return settings.getInt(PushConstants.BADGE, 0)
+ @TargetApi(26)
+ private fun createDefaultNotificationChannelIfNeeded(options: JSONObject?) {
+ // only call on Android O and above
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channels = notificationManager.notificationChannels
+
+ for (i in channels.indices) {
+ if (PushConstants.DEFAULT_CHANNEL_ID == channels[i].id) {
+ return
+ }
+ }
+
+ try {
+ options?.apply {
+ put(PushConstants.CHANNEL_ID, PushConstants.DEFAULT_CHANNEL_ID)
+ putOpt(PushConstants.CHANNEL_DESCRIPTION, appName)
+ }
+
+ createChannel(options)
+ } catch (e: JSONException) {
+ Log.e(TAG, "Execute: JSON Exception ${e.message}")
+ }
+ }
}
/**
- * Sets badge count on application icon and in SharedPreferences
+ * Performs various push plugin related tasks:
+ *
+ * - Initialize
+ * - Unregister
+ * - Has Notification Permission Check
+ * - Set Icon Badge Number
+ * - Get Icon Badge Number
+ * - Clear All Notifications
+ * - Clear Notification
+ * - Subscribe
+ * - Unsubscribe
+ * - Create Channel
+ * - Delete Channel
+ * - List Channels
*
- * @param context
- * @param badgeCount
+ * @param action
+ * @param data
+ * @param callbackContext
*/
- @JvmStatic
- fun setApplicationIconBadgeNumber(context: Context, badgeCount: Int) {
- if (badgeCount > 0) {
- ShortcutBadger.applyCount(context, badgeCount)
- } else {
- ShortcutBadger.removeCount(context)
- }
-
- context.getSharedPreferences(PushConstants.BADGE, Context.MODE_PRIVATE)
- .edit()?.apply {
- putInt(PushConstants.BADGE, badgeCount.coerceAtLeast(0))
- apply()
+ override fun execute(
+ action: String,
+ data: JSONArray,
+ callbackContext: CallbackContext
+ ): Boolean {
+ Log.v(TAG, "Execute: Action = $action")
+
+ gWebView = webView
+
+ when (action) {
+ PushConstants.INITIALIZE -> executeActionInitialize(data, callbackContext)
+ PushConstants.UNREGISTER -> executeActionUnregister(data, callbackContext)
+ PushConstants.FINISH -> callbackContext.success()
+ PushConstants.HAS_PERMISSION -> executeActionHasPermission(callbackContext)
+ PushConstants.SET_APPLICATION_ICON_BADGE_NUMBER -> executeActionSetIconBadgeNumber(
+ data, callbackContext
+ )
+
+ PushConstants.GET_APPLICATION_ICON_BADGE_NUMBER -> executeActionGetIconBadgeNumber(
+ callbackContext
+ )
+
+ PushConstants.CLEAR_ALL_NOTIFICATIONS -> executeActionClearAllNotifications(
+ callbackContext
+ )
+
+ PushConstants.SUBSCRIBE -> executeActionSubscribe(data, callbackContext)
+ PushConstants.UNSUBSCRIBE -> executeActionUnsubscribe(data, callbackContext)
+ PushConstants.CREATE_CHANNEL -> executeActionCreateChannel(data, callbackContext)
+ PushConstants.DELETE_CHANNEL -> executeActionDeleteChannel(data, callbackContext)
+ PushConstants.LIST_CHANNELS -> executeActionListChannels(callbackContext)
+ PushConstants.CLEAR_NOTIFICATION -> executeActionClearNotification(
+ data,
+ callbackContext
+ )
+
+ else -> {
+ Log.e(TAG, "Execute: Invalid Action $action")
+ callbackContext.sendPluginResult(PluginResult(PluginResult.Status.INVALID_ACTION))
+ return false
+ }
}
+ return true
}
- /**
- * @return Boolean Active is true when the Cordova WebView is present.
- */
- val isActive: Boolean
- get() = gWebView != null
- }
+ private fun executeActionInitialize(data: JSONArray, callbackContext: CallbackContext) {
+ // Better Logging
+ fun formatLogMessage(msg: String): String = "Execute::Initialize: ($msg)"
- private val activity: Activity
- get() = cordova.activity
+ pushContext = callbackContext
+ pluginInitData = data;
- private val applicationContext: Context
- get() = activity.applicationContext
+ var hasPermission = checkForPostNotificationsPermission()
+ if (!hasPermission)
+ return
- private val notificationManager: NotificationManager
- get() = (activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager)
+ cordova.threadPool.execute(Runnable {
+ Log.v(TAG, formatLogMessage("Data=$data"))
- private val appName: String
- get() = activity.packageManager.getApplicationLabel(activity.applicationInfo) as String
+ val sharedPref = applicationContext.getSharedPreferences(
+ PushConstants.COM_ADOBE_PHONEGAP_PUSH,
+ Context.MODE_PRIVATE
+ )
+ var jo: JSONObject? = null
+ var senderID: String? = null
- @TargetApi(26)
- @Throws(JSONException::class)
- private fun listChannels(): JSONArray {
- val channels = JSONArray()
+ try {
+ jo = data.getJSONObject(0).getJSONObject(PushConstants.ANDROID)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- val notificationChannels = notificationManager.notificationChannels
+ val senderIdResId = activity.resources.getIdentifier(
+ PushConstants.GCM_DEFAULT_SENDER_ID,
+ "string",
+ activity.packageName
+ )
+ senderID = activity.getString(senderIdResId)
- for (notificationChannel in notificationChannels) {
- val channel = JSONObject().apply {
- put(PushConstants.CHANNEL_ID, notificationChannel.id)
- put(PushConstants.CHANNEL_DESCRIPTION, notificationChannel.description)
- }
+ // If no NotificationChannels exist create the default one
+ createDefaultNotificationChannelIfNeeded(jo)
- channels.put(channel)
- }
- }
-
- return channels
- }
-
- @TargetApi(26)
- private fun deleteChannel(channelId: String) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- notificationManager.deleteNotificationChannel(channelId)
- }
- }
-
- @TargetApi(26)
- @Throws(JSONException::class)
- private fun createChannel(channel: JSONObject?) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- channel?.let {
- NotificationChannel(
- it.getString(PushConstants.CHANNEL_ID),
- it.optString(PushConstants.CHANNEL_DESCRIPTION, appName),
- it.optInt(PushConstants.CHANNEL_IMPORTANCE, NotificationManager.IMPORTANCE_DEFAULT)
- ).apply {
- /**
- * Enable Lights when Light Color is set.
- */
- val mLightColor = it.optInt(PushConstants.CHANNEL_LIGHT_COLOR, -1)
- if (mLightColor != -1) {
- enableLights(true)
- lightColor = mLightColor
- }
-
- /**
- * Set Lock Screen Visibility.
- */
- lockscreenVisibility = channel.optInt(
- PushConstants.VISIBILITY,
- NotificationCompat.VISIBILITY_PUBLIC
- )
-
- /**
- * Set if badge should be shown
- */
- setShowBadge(it.optBoolean(PushConstants.BADGE, true))
-
- /**
- * Sound Settings
- */
- val (soundUri, audioAttributes) = getNotificationChannelSound(it)
- setSound(soundUri, audioAttributes)
-
- /**
- * Set vibration settings.
- * Data can be either JSONArray or Boolean value.
- */
- val (hasVibration, vibrationPatternArray) = getNotificationChannelVibration(it)
- if (vibrationPatternArray != null) {
- vibrationPattern = vibrationPatternArray
- } else {
- enableVibration(hasVibration)
- }
-
- notificationManager.createNotificationChannel(this)
- }
- }
- }
- }
+ Log.v(TAG, formatLogMessage("JSONObject=$jo"))
+ Log.v(TAG, formatLogMessage("senderID=$senderID"))
- private fun getNotificationChannelSound(channelData: JSONObject): Pair {
- val audioAttributes = AudioAttributes.Builder()
- .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
- .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
- .build()
+ val token = try {
+ try {
+ Tasks.await(FirebaseMessaging.getInstance().token)
+ } catch (e: ExecutionException) {
+ throw e.cause ?: e
+ }
+ } catch (e: IllegalStateException) {
+ Log.e(TAG, formatLogMessage("Firebase Token Exception ${e.message}"))
+ null
+ } catch (e: ExecutionException) {
+ Log.e(TAG, formatLogMessage("Firebase Token Exception ${e.message}"))
+ null
+ } catch (e: InterruptedException) {
+ Log.e(TAG, formatLogMessage("Firebase Token Exception ${e.message}"))
+ null
+ }
- val sound = channelData.optString(PushConstants.SOUND, PushConstants.SOUND_DEFAULT)
+ if (token != "") {
+ val registration =
+ JSONObject().put(PushConstants.REGISTRATION_ID, token).apply {
+ put(PushConstants.REGISTRATION_TYPE, PushConstants.FCM)
+ }
- return when {
- sound == PushConstants.SOUND_RINGTONE -> Pair(
- Settings.System.DEFAULT_RINGTONE_URI,
- audioAttributes
- )
+ Log.v(TAG, formatLogMessage("onRegistered=$registration"))
- // Disable sound for this notification channel if an empty string is passed.
- // https://stackoverflow.com/a/47144981/6194193
- sound.isEmpty() -> Pair(null, null)
+ val topics = jo.optJSONArray(PushConstants.TOPICS)
+ subscribeToTopics(topics)
- // E.g. android.resource://org.apache.cordova/raw/
- sound != PushConstants.SOUND_DEFAULT -> {
- val scheme = ContentResolver.SCHEME_ANDROID_RESOURCE
- val packageName = applicationContext.packageName
+ sendEvent(registration)
+ } else {
+ callbackContext.error("Empty registration ID received from FCM")
+ return@Runnable
+ }
+ } catch (e: JSONException) {
+ Log.e(TAG, formatLogMessage("JSON Exception ${e.message}"))
+ callbackContext.error(e.message)
+ } catch (e: IOException) {
+ Log.e(TAG, formatLogMessage("IO Exception ${e.message}"))
+ callbackContext.error(e.message)
+ } catch (e: NotFoundException) {
+ Log.e(TAG, formatLogMessage("Resources NotFoundException Exception ${e.message}"))
+ callbackContext.error(e.message)
+ }
- Pair(
- Uri.parse("${scheme}://$packageName/raw/$sound"),
- audioAttributes
- )
- }
+ jo?.let {
+ /**
+ * Add Shared Preferences
+ *
+ * Make sure to remove the preferences in the Remove step.
+ */
+ sharedPref.edit()?.apply {
+ /**
+ * Set Icon
+ */
+ try {
+ putString(PushConstants.ICON, it.getString(PushConstants.ICON))
+ } catch (e: JSONException) {
+ Log.d(TAG, formatLogMessage("No Icon Options"))
+ }
- else -> Pair(Settings.System.DEFAULT_NOTIFICATION_URI, audioAttributes)
- }
- }
+ /**
+ * Set Icon Color
+ */
+ try {
+ putString(PushConstants.ICON_COLOR, it.getString(PushConstants.ICON_COLOR))
+ } catch (e: JSONException) {
+ Log.d(TAG, formatLogMessage("No Icon Color Options"))
+ }
+
+ /**
+ * Clear badge count when true
+ */
+ val clearBadge = it.optBoolean(PushConstants.CLEAR_BADGE, false)
+ putBoolean(PushConstants.CLEAR_BADGE, clearBadge)
- private fun getNotificationChannelVibration(channelData: JSONObject): Pair {
- var patternArray: LongArray? = null
- val mVibrationPattern = channelData.optJSONArray(PushConstants.CHANNEL_VIBRATION)
+ if (clearBadge) {
+ setApplicationIconBadgeNumber(applicationContext, 0)
+ }
- if (mVibrationPattern != null) {
- val patternLength = mVibrationPattern.length()
- patternArray = LongArray(patternLength)
+ /**
+ * Set Sound
+ */
+ putBoolean(PushConstants.SOUND, it.optBoolean(PushConstants.SOUND, true))
+
+ /**
+ * Set Vibrate
+ */
+ putBoolean(PushConstants.VIBRATE, it.optBoolean(PushConstants.VIBRATE, true))
+
+ /**
+ * Set Clear Notifications
+ */
+ putBoolean(
+ PushConstants.CLEAR_NOTIFICATIONS,
+ it.optBoolean(PushConstants.CLEAR_NOTIFICATIONS, true)
+ )
+
+ /**
+ * Set Force Show
+ */
+ putBoolean(
+ PushConstants.FORCE_SHOW,
+ it.optBoolean(PushConstants.FORCE_SHOW, false)
+ )
+
+ /**
+ * Set SenderID
+ */
+ putString(PushConstants.SENDER_ID, senderID)
+
+ /**
+ * Set Message Key
+ */
+ putString(PushConstants.MESSAGE_KEY, it.optString(PushConstants.MESSAGE_KEY))
+
+ /**
+ * Set Title Key
+ */
+ putString(PushConstants.TITLE_KEY, it.optString(PushConstants.TITLE_KEY))
+
+ apply()
+ }
+ }
- for (i in 0 until patternLength) {
- patternArray[i] = mVibrationPattern.optLong(i)
- }
+ if (gCachedExtras.isNotEmpty()) {
+ Log.v(TAG, formatLogMessage("Sending Cached Extras"))
+
+ synchronized(gCachedExtras) {
+ val gCachedExtrasIterator: Iterator = gCachedExtras.iterator()
+
+ while (gCachedExtrasIterator.hasNext()) {
+ sendExtras(gCachedExtrasIterator.next())
+ }
+ }
+
+ gCachedExtras.clear()
+ }
+ })
}
- return Pair(
- channelData.optBoolean(PushConstants.CHANNEL_VIBRATION, true),
- patternArray
- )
- }
+ private fun checkForPostNotificationsPermission(): Boolean {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (!PermissionHelper.hasPermission(this, Manifest.permission.POST_NOTIFICATIONS)) {
+ //PermissionHelper.requestPermission(
+ // this,
+ // REQ_CODE_INITIALIZE_PLUGIN,
+ // Manifest.permission.POST_NOTIFICATIONS
+ //)
+ return false
+ }
+ }
- @TargetApi(26)
- private fun createDefaultNotificationChannelIfNeeded(options: JSONObject?) {
- // only call on Android O and above
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- val channels = notificationManager.notificationChannels
+ return true
+ }
+
+ private fun executeActionUnregister(data: JSONArray, callbackContext: CallbackContext) {
+ // Better Logging
+ fun formatLogMessage(msg: String): String = "Execute::Unregister: ($msg)"
+
+ cordova.threadPool.execute {
+ try {
+ val sharedPref = applicationContext.getSharedPreferences(
+ PushConstants.COM_ADOBE_PHONEGAP_PUSH,
+ Context.MODE_PRIVATE
+ )
+ val topics = data.optJSONArray(0)
+
+ if (topics != null) {
+ unsubscribeFromTopics(topics)
+ } else {
+ try {
+ Tasks.await(FirebaseMessaging.getInstance().deleteToken())
+ } catch (e: ExecutionException) {
+ throw e.cause ?: e
+ }
+ Log.v(TAG, formatLogMessage("UNREGISTER"))
+
+ /**
+ * Remove Shared Preferences
+ *
+ * Make sure to remove what was in the Initialize step.
+ */
+ sharedPref.edit()?.apply {
+ remove(PushConstants.ICON)
+ remove(PushConstants.ICON_COLOR)
+ remove(PushConstants.CLEAR_BADGE)
+ remove(PushConstants.SOUND)
+ remove(PushConstants.VIBRATE)
+ remove(PushConstants.CLEAR_NOTIFICATIONS)
+ remove(PushConstants.FORCE_SHOW)
+ remove(PushConstants.SENDER_ID)
+ remove(PushConstants.MESSAGE_KEY)
+ remove(PushConstants.TITLE_KEY)
+
+ apply()
+ }
+ }
- for (i in channels.indices) {
- if (PushConstants.DEFAULT_CHANNEL_ID == channels[i].id) {
- return
+ callbackContext.success()
+ } catch (e: IOException) {
+ Log.e(TAG, formatLogMessage("IO Exception ${e.message}"))
+ callbackContext.error(e.message)
+ } catch (e: InterruptedException) {
+ Log.e(TAG, formatLogMessage("Interrupted ${e.message}"))
+ callbackContext.error(e.message)
+ }
}
- }
+ }
+
+ private fun executeActionHasPermission(callbackContext: CallbackContext) {
+ // Better Logging
+ fun formatLogMessage(msg: String): String = "Execute::HasPermission: ($msg)"
+
+ cordova.threadPool.execute {
+ try {
+ val isNotificationEnabled = NotificationManagerCompat.from(applicationContext)
+ .areNotificationsEnabled()
- try {
- options?.apply {
- put(PushConstants.CHANNEL_ID, PushConstants.DEFAULT_CHANNEL_ID)
- putOpt(PushConstants.CHANNEL_DESCRIPTION, appName)
+ Log.d(TAG, formatLogMessage("Has Notification Permission: $isNotificationEnabled"))
+
+ val jo = JSONObject().apply {
+ put(PushConstants.IS_ENABLED, isNotificationEnabled)
+ }
+
+ val pluginResult = PluginResult(PluginResult.Status.OK, jo).apply {
+ keepCallback = true
+ }
+
+ callbackContext.sendPluginResult(pluginResult)
+ } catch (e: UnknownError) {
+ callbackContext.error(e.message)
+ } catch (e: JSONException) {
+ callbackContext.error(e.message)
+ }
}
+ }
- createChannel(options)
- } catch (e: JSONException) {
- Log.e(TAG, "Execute: JSON Exception ${e.message}")
- }
- }
- }
-
- /**
- * Performs various push plugin related tasks:
- *
- * - Initialize
- * - Unregister
- * - Has Notification Permission Check
- * - Set Icon Badge Number
- * - Get Icon Badge Number
- * - Clear All Notifications
- * - Clear Notification
- * - Subscribe
- * - Unsubscribe
- * - Create Channel
- * - Delete Channel
- * - List Channels
- *
- * @param action
- * @param data
- * @param callbackContext
- */
- override fun execute(
- action: String,
- data: JSONArray,
- callbackContext: CallbackContext
- ): Boolean {
- Log.v(TAG, "Execute: Action = $action")
-
- gWebView = webView
-
- when (action) {
- PushConstants.INITIALIZE -> executeActionInitialize(data, callbackContext)
- PushConstants.UNREGISTER -> executeActionUnregister(data, callbackContext)
- PushConstants.FINISH -> callbackContext.success()
- PushConstants.HAS_PERMISSION -> executeActionHasPermission(callbackContext)
- PushConstants.SET_APPLICATION_ICON_BADGE_NUMBER -> executeActionSetIconBadgeNumber(
- data, callbackContext
- )
- PushConstants.GET_APPLICATION_ICON_BADGE_NUMBER -> executeActionGetIconBadgeNumber(
- callbackContext
- )
- PushConstants.CLEAR_ALL_NOTIFICATIONS -> executeActionClearAllNotifications(callbackContext)
- PushConstants.SUBSCRIBE -> executeActionSubscribe(data, callbackContext)
- PushConstants.UNSUBSCRIBE -> executeActionUnsubscribe(data, callbackContext)
- PushConstants.CREATE_CHANNEL -> executeActionCreateChannel(data, callbackContext)
- PushConstants.DELETE_CHANNEL -> executeActionDeleteChannel(data, callbackContext)
- PushConstants.LIST_CHANNELS -> executeActionListChannels(callbackContext)
- PushConstants.CLEAR_NOTIFICATION -> executeActionClearNotification(data, callbackContext)
- else -> {
- Log.e(TAG, "Execute: Invalid Action $action")
- callbackContext.sendPluginResult(PluginResult(PluginResult.Status.INVALID_ACTION))
- return false
- }
- }
- return true
- }
-
- private fun executeActionInitialize(data: JSONArray, callbackContext: CallbackContext) {
- // Better Logging
- fun formatLogMessage(msg: String): String = "Execute::Initialize: ($msg)"
-
- pushContext = callbackContext
- pluginInitData = data;
-
- var hasPermission = checkForPostNotificationsPermission()
- if (!hasPermission)
- return
-
- cordova.threadPool.execute(Runnable {
- Log.v(TAG, formatLogMessage("Data=$data"))
-
- val sharedPref = applicationContext.getSharedPreferences(
- PushConstants.COM_ADOBE_PHONEGAP_PUSH,
- Context.MODE_PRIVATE
- )
- var jo: JSONObject? = null
- var senderID: String? = null
-
- try {
- jo = data.getJSONObject(0).getJSONObject(PushConstants.ANDROID)
-
- val senderIdResId = activity.resources.getIdentifier(
- PushConstants.GCM_DEFAULT_SENDER_ID,
- "string",
- activity.packageName
- )
- senderID = activity.getString(senderIdResId)
-
- // If no NotificationChannels exist create the default one
- createDefaultNotificationChannelIfNeeded(jo)
-
- Log.v(TAG, formatLogMessage("JSONObject=$jo"))
- Log.v(TAG, formatLogMessage("senderID=$senderID"))
-
- val token = try {
- try {
- Tasks.await(FirebaseMessaging.getInstance().token)
- } catch (e: ExecutionException) {
- throw e.cause ?: e
- }
- } catch (e: IllegalStateException) {
- Log.e(TAG, formatLogMessage("Firebase Token Exception ${e.message}"))
- null
- } catch (e: ExecutionException) {
- Log.e(TAG, formatLogMessage("Firebase Token Exception ${e.message}"))
- null
- } catch (e: InterruptedException) {
- Log.e(TAG, formatLogMessage("Firebase Token Exception ${e.message}"))
- null
+ private fun executeActionSetIconBadgeNumber(data: JSONArray, callbackContext: CallbackContext) {
+ fun formatLogMessage(msg: String): String = "Execute::SetIconBadgeNumber: ($msg)"
+
+ cordova.threadPool.execute {
+ Log.v(TAG, formatLogMessage("data=$data"))
+
+ try {
+ val badgeCount = data.getJSONObject(0).getInt(PushConstants.BADGE)
+ setApplicationIconBadgeNumber(applicationContext, badgeCount)
+ } catch (e: JSONException) {
+ callbackContext.error(e.message)
+ }
+
+ callbackContext.success()
}
+ }
- if (token != "") {
- val registration = JSONObject().put(PushConstants.REGISTRATION_ID, token).apply {
- put(PushConstants.REGISTRATION_TYPE, PushConstants.FCM)
- }
+ private fun executeActionGetIconBadgeNumber(callbackContext: CallbackContext) {
+ cordova.threadPool.execute {
+ Log.v(TAG, "Execute::GetIconBadgeNumber")
+ callbackContext.success(getApplicationIconBadgeNumber(applicationContext))
+ }
+ }
- Log.v(TAG, formatLogMessage("onRegistered=$registration"))
+ private fun executeActionClearAllNotifications(callbackContext: CallbackContext) {
+ cordova.threadPool.execute {
+ Log.v(TAG, "Execute Clear All Notifications")
+ clearAllNotifications()
+ callbackContext.success()
+ }
+ }
- val topics = jo.optJSONArray(PushConstants.TOPICS)
- subscribeToTopics(topics)
+ private fun executeActionSubscribe(data: JSONArray, callbackContext: CallbackContext) {
+ cordova.threadPool.execute {
+ try {
+ Log.v(TAG, "Execute::Subscribe")
+ val topic = data.getString(0)
+ subscribeToTopic(topic)
+ callbackContext.success()
+ } catch (e: JSONException) {
+ callbackContext.error(e.message)
+ }
+ }
+ }
- sendEvent(registration)
- } else {
- callbackContext.error("Empty registration ID received from FCM")
- return@Runnable
+ private fun executeActionUnsubscribe(data: JSONArray, callbackContext: CallbackContext) {
+ cordova.threadPool.execute {
+ try {
+ Log.v(TAG, "Execute::Unsubscribe")
+ val topic = data.getString(0)
+ unsubscribeFromTopic(topic)
+ callbackContext.success()
+ } catch (e: JSONException) {
+ callbackContext.error(e.message)
+ }
}
- } catch (e: JSONException) {
- Log.e(TAG, formatLogMessage("JSON Exception ${e.message}"))
- callbackContext.error(e.message)
- } catch (e: IOException) {
- Log.e(TAG, formatLogMessage("IO Exception ${e.message}"))
- callbackContext.error(e.message)
- } catch (e: NotFoundException) {
- Log.e(TAG, formatLogMessage("Resources NotFoundException Exception ${e.message}"))
- callbackContext.error(e.message)
- }
-
- jo?.let {
- /**
- * Add Shared Preferences
- *
- * Make sure to remove the preferences in the Remove step.
- */
- sharedPref.edit()?.apply {
- /**
- * Set Icon
- */
- try {
- putString(PushConstants.ICON, it.getString(PushConstants.ICON))
- } catch (e: JSONException) {
- Log.d(TAG, formatLogMessage("No Icon Options"))
- }
-
- /**
- * Set Icon Color
- */
- try {
- putString(PushConstants.ICON_COLOR, it.getString(PushConstants.ICON_COLOR))
- } catch (e: JSONException) {
- Log.d(TAG, formatLogMessage("No Icon Color Options"))
- }
-
- /**
- * Clear badge count when true
- */
- val clearBadge = it.optBoolean(PushConstants.CLEAR_BADGE, false)
- putBoolean(PushConstants.CLEAR_BADGE, clearBadge)
-
- if (clearBadge) {
- setApplicationIconBadgeNumber(applicationContext, 0)
- }
-
- /**
- * Set Sound
- */
- putBoolean(PushConstants.SOUND, it.optBoolean(PushConstants.SOUND, true))
-
- /**
- * Set Vibrate
- */
- putBoolean(PushConstants.VIBRATE, it.optBoolean(PushConstants.VIBRATE, true))
-
- /**
- * Set Clear Notifications
- */
- putBoolean(
- PushConstants.CLEAR_NOTIFICATIONS,
- it.optBoolean(PushConstants.CLEAR_NOTIFICATIONS, true)
- )
-
- /**
- * Set Force Show
- */
- putBoolean(
- PushConstants.FORCE_SHOW,
- it.optBoolean(PushConstants.FORCE_SHOW, false)
- )
-
- /**
- * Set SenderID
- */
- putString(PushConstants.SENDER_ID, senderID)
-
- /**
- * Set Message Key
- */
- putString(PushConstants.MESSAGE_KEY, it.optString(PushConstants.MESSAGE_KEY))
-
- /**
- * Set Title Key
- */
- putString(PushConstants.TITLE_KEY, it.optString(PushConstants.TITLE_KEY))
-
- commit()
+ }
+
+ private fun executeActionCreateChannel(data: JSONArray, callbackContext: CallbackContext) {
+ cordova.threadPool.execute {
+ try {
+ Log.v(TAG, "Execute::CreateChannel")
+ createChannel(data.getJSONObject(0))
+ callbackContext.success()
+ } catch (e: JSONException) {
+ callbackContext.error(e.message)
+ }
}
- }
+ }
- if (gCachedExtras.isNotEmpty()) {
- Log.v(TAG, formatLogMessage("Sending Cached Extras"))
+ private fun executeActionDeleteChannel(data: JSONArray, callbackContext: CallbackContext) {
+ cordova.threadPool.execute {
+ try {
+ val channelId = data.getString(0)
+ Log.v(TAG, "Execute::DeleteChannel channelId=$channelId")
+ deleteChannel(channelId)
+ callbackContext.success()
+ } catch (e: JSONException) {
+ callbackContext.error(e.message)
+ }
+ }
+ }
- synchronized(gCachedExtras) {
- val gCachedExtrasIterator: Iterator = gCachedExtras.iterator()
+ private fun executeActionListChannels(callbackContext: CallbackContext) {
+ cordova.threadPool.execute {
+ try {
+ Log.v(TAG, "Execute::ListChannels")
+ callbackContext.success(listChannels())
+ } catch (e: JSONException) {
+ callbackContext.error(e.message)
+ }
+ }
+ }
- while (gCachedExtrasIterator.hasNext()) {
- sendExtras(gCachedExtrasIterator.next())
- }
+ private fun executeActionClearNotification(data: JSONArray, callbackContext: CallbackContext) {
+ cordova.threadPool.execute {
+ try {
+ val notificationId = data.getInt(0)
+ Log.v(TAG, "Execute::ClearNotification notificationId=$notificationId")
+ clearNotification(notificationId)
+ callbackContext.success()
+ } catch (e: JSONException) {
+ callbackContext.error(e.message)
+ }
}
+ }
- gCachedExtras.clear()
- }
- })
- }
-
- private fun checkForPostNotificationsPermission(): Boolean {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- if (!PermissionHelper.hasPermission(this, Manifest.permission.POST_NOTIFICATIONS))
- {
- PermissionHelper.requestPermission(
- this,
- REQ_CODE_INITIALIZE_PLUGIN,
- Manifest.permission.POST_NOTIFICATIONS
- )
- return false
- }
+ /**
+ * Initialize
+ */
+ override fun initialize(cordova: CordovaInterface, webView: CordovaWebView) {
+ super.initialize(cordova, webView)
+ isInForeground = true
}
- return true
- }
+ /**
+ * Handle when the view is being paused
+ */
+ override fun onPause(multitasking: Boolean) {
+ isInForeground = false
+ super.onPause(multitasking)
+ }
- private fun executeActionUnregister(data: JSONArray, callbackContext: CallbackContext) {
- // Better Logging
- fun formatLogMessage(msg: String): String = "Execute::Unregister: ($msg)"
+ /**
+ * Handle when the view is resuming
+ */
+ override fun onResume(multitasking: Boolean) {
+ super.onResume(multitasking)
+ isInForeground = true
+ }
- cordova.threadPool.execute {
- try {
- val sharedPref = applicationContext.getSharedPreferences(
- PushConstants.COM_ADOBE_PHONEGAP_PUSH,
- Context.MODE_PRIVATE
- )
- val topics = data.optJSONArray(0)
-
- if (topics != null) {
- unsubscribeFromTopics(topics)
- } else {
- try {
- Tasks.await(FirebaseMessaging.getInstance().deleteToken())
- } catch (e: ExecutionException) {
- throw e.cause ?: e
- }
- Log.v(TAG, formatLogMessage("UNREGISTER"))
-
- /**
- * Remove Shared Preferences
- *
- * Make sure to remove what was in the Initialize step.
- */
- sharedPref.edit()?.apply {
- remove(PushConstants.ICON)
- remove(PushConstants.ICON_COLOR)
- remove(PushConstants.CLEAR_BADGE)
- remove(PushConstants.SOUND)
- remove(PushConstants.VIBRATE)
- remove(PushConstants.CLEAR_NOTIFICATIONS)
- remove(PushConstants.FORCE_SHOW)
- remove(PushConstants.SENDER_ID)
- remove(PushConstants.MESSAGE_KEY)
- remove(PushConstants.TITLE_KEY)
-
- commit()
- }
+ /**
+ * Handle when the view is being destroyed
+ */
+ override fun onDestroy() {
+ isInForeground = false
+ gWebView = null
+
+ // Clear Notification
+ applicationContext.getSharedPreferences(PushConstants.COM_ADOBE_PHONEGAP_PUSH,
+ Context.MODE_PRIVATE)
+ .apply {
+ if (getBoolean(PushConstants.CLEAR_NOTIFICATIONS, true)) {
+ clearAllNotifications()
+ }
}
- callbackContext.success()
- } catch (e: IOException) {
- Log.e(TAG, formatLogMessage("IO Exception ${e.message}"))
- callbackContext.error(e.message)
- } catch (e: InterruptedException) {
- Log.e(TAG, formatLogMessage("Interrupted ${e.message}"))
- callbackContext.error(e.message)
- }
+ super.onDestroy()
}
- }
- private fun executeActionHasPermission(callbackContext: CallbackContext) {
- // Better Logging
- fun formatLogMessage(msg: String): String = "Execute::HasPermission: ($msg)"
+ private fun clearAllNotifications() {
+ notificationManager.cancelAll()
+ }
- cordova.threadPool.execute {
- try {
- val isNotificationEnabled = NotificationManagerCompat.from(applicationContext)
- .areNotificationsEnabled()
+ private fun clearNotification(id: Int) {
+ notificationManager.cancel(appName, id)
+ }
- Log.d(TAG, formatLogMessage("Has Notification Permission: $isNotificationEnabled"))
+ private fun subscribeToTopics(topics: JSONArray?) {
+ topics?.let {
+ for (i in 0 until it.length()) {
+ val topicKey = it.optString(i, null)
+ subscribeToTopic(topicKey)
+ }
+ }
+ }
- val jo = JSONObject().apply {
- put(PushConstants.IS_ENABLED, isNotificationEnabled)
+ private fun unsubscribeFromTopics(topics: JSONArray?) {
+ topics?.let {
+ for (i in 0 until it.length()) {
+ val topic = it.optString(i, null)
+ unsubscribeFromTopic(topic)
+ }
}
+ }
- val pluginResult = PluginResult(PluginResult.Status.OK, jo).apply {
- keepCallback = true
+ private fun subscribeToTopic(topic: String?) {
+ topic?.let {
+ Log.d(TAG, "Subscribing to Topic: $it")
+ FirebaseMessaging.getInstance().subscribeToTopic(it)
}
+ }
- callbackContext.sendPluginResult(pluginResult)
- } catch (e: UnknownError) {
- callbackContext.error(e.message)
- } catch (e: JSONException) {
- callbackContext.error(e.message)
- }
- }
- }
-
- private fun executeActionSetIconBadgeNumber(data: JSONArray, callbackContext: CallbackContext) {
- fun formatLogMessage(msg: String): String = "Execute::SetIconBadgeNumber: ($msg)"
-
- cordova.threadPool.execute {
- Log.v(TAG, formatLogMessage("data=$data"))
-
- try {
- val badgeCount = data.getJSONObject(0).getInt(PushConstants.BADGE)
- setApplicationIconBadgeNumber(applicationContext, badgeCount)
- } catch (e: JSONException) {
- callbackContext.error(e.message)
- }
-
- callbackContext.success()
- }
- }
-
- private fun executeActionGetIconBadgeNumber(callbackContext: CallbackContext) {
- cordova.threadPool.execute {
- Log.v(TAG, "Execute::GetIconBadgeNumber")
- callbackContext.success(getApplicationIconBadgeNumber(applicationContext))
- }
- }
-
- private fun executeActionClearAllNotifications(callbackContext: CallbackContext) {
- cordova.threadPool.execute {
- Log.v(TAG, "Execute Clear All Notifications")
- clearAllNotifications()
- callbackContext.success()
- }
- }
-
- private fun executeActionSubscribe(data: JSONArray, callbackContext: CallbackContext) {
- cordova.threadPool.execute {
- try {
- Log.v(TAG, "Execute::Subscribe")
- val topic = data.getString(0)
- subscribeToTopic(topic)
- callbackContext.success()
- } catch (e: JSONException) {
- callbackContext.error(e.message)
- }
- }
- }
-
- private fun executeActionUnsubscribe(data: JSONArray, callbackContext: CallbackContext) {
- cordova.threadPool.execute {
- try {
- Log.v(TAG, "Execute::Unsubscribe")
- val topic = data.getString(0)
- unsubscribeFromTopic(topic)
- callbackContext.success()
- } catch (e: JSONException) {
- callbackContext.error(e.message)
- }
- }
- }
-
- private fun executeActionCreateChannel(data: JSONArray, callbackContext: CallbackContext) {
- cordova.threadPool.execute {
- try {
- Log.v(TAG, "Execute::CreateChannel")
- createChannel(data.getJSONObject(0))
- callbackContext.success()
- } catch (e: JSONException) {
- callbackContext.error(e.message)
- }
- }
- }
-
- private fun executeActionDeleteChannel(data: JSONArray, callbackContext: CallbackContext) {
- cordova.threadPool.execute {
- try {
- val channelId = data.getString(0)
- Log.v(TAG, "Execute::DeleteChannel channelId=$channelId")
- deleteChannel(channelId)
- callbackContext.success()
- } catch (e: JSONException) {
- callbackContext.error(e.message)
- }
- }
- }
-
- private fun executeActionListChannels(callbackContext: CallbackContext) {
- cordova.threadPool.execute {
- try {
- Log.v(TAG, "Execute::ListChannels")
- callbackContext.success(listChannels())
- } catch (e: JSONException) {
- callbackContext.error(e.message)
- }
- }
- }
-
- private fun executeActionClearNotification(data: JSONArray, callbackContext: CallbackContext) {
- cordova.threadPool.execute {
- try {
- val notificationId = data.getInt(0)
- Log.v(TAG, "Execute::ClearNotification notificationId=$notificationId")
- clearNotification(notificationId)
- callbackContext.success()
- } catch (e: JSONException) {
- callbackContext.error(e.message)
- }
- }
- }
-
- /**
- * Initialize
- */
- override fun initialize(cordova: CordovaInterface, webView: CordovaWebView) {
- super.initialize(cordova, webView)
- isInForeground = true
- }
-
- /**
- * Handle when the view is being paused
- */
- override fun onPause(multitasking: Boolean) {
- isInForeground = false
- super.onPause(multitasking)
- }
-
- /**
- * Handle when the view is resuming
- */
- override fun onResume(multitasking: Boolean) {
- super.onResume(multitasking)
- isInForeground = true
- }
-
- /**
- * Handle when the view is being destroyed
- */
- override fun onDestroy() {
- isInForeground = false
- gWebView = null
-
- // Clear Notification
- applicationContext.getSharedPreferences(
- PushConstants.COM_ADOBE_PHONEGAP_PUSH,
- Context.MODE_PRIVATE
- ).apply {
- if (getBoolean(PushConstants.CLEAR_NOTIFICATIONS, true)) {
- clearAllNotifications()
- }
- }
-
- super.onDestroy()
- }
-
- private fun clearAllNotifications() {
- notificationManager.cancelAll()
- }
-
- private fun clearNotification(id: Int) {
- notificationManager.cancel(appName, id)
- }
-
- private fun subscribeToTopics(topics: JSONArray?) {
- topics?.let {
- for (i in 0 until it.length()) {
- val topicKey = it.optString(i, null)
- subscribeToTopic(topicKey)
- }
- }
- }
-
- private fun unsubscribeFromTopics(topics: JSONArray?) {
- topics?.let {
- for (i in 0 until it.length()) {
- val topic = it.optString(i, null)
- unsubscribeFromTopic(topic)
- }
- }
- }
-
- private fun subscribeToTopic(topic: String?) {
- topic?.let {
- Log.d(TAG, "Subscribing to Topic: $it")
- FirebaseMessaging.getInstance().subscribeToTopic(it)
- }
- }
-
- private fun unsubscribeFromTopic(topic: String?) {
- topic?.let {
- Log.d(TAG, "Unsubscribing to topic: $it")
- FirebaseMessaging.getInstance().unsubscribeFromTopic(it)
- }
- }
-
- override fun onRequestPermissionResult(
- requestCode: Int,
- permissions: Array?,
- grantResults: IntArray?
- ) {
- super.onRequestPermissionResult(requestCode, permissions, grantResults)
-
- for (r in grantResults!!) {
- if (r == PackageManager.PERMISSION_DENIED) {
- pushContext?.sendPluginResult(
- PluginResult(
- PluginResult.Status.ILLEGAL_ACCESS_EXCEPTION,
- "Permission to post notifications was denied by the user"
- )
- )
- return
- }
+ private fun unsubscribeFromTopic(topic: String?) {
+ topic?.let {
+ Log.d(TAG, "Unsubscribing to topic: $it")
+ FirebaseMessaging.getInstance().unsubscribeFromTopic(it)
+ }
}
- if (requestCode == REQ_CODE_INITIALIZE_PLUGIN)
- {
- executeActionInitialize(pluginInitData!!, pushContext!!)
+ override fun onRequestPermissionResult(requestCode: Int,
+ permissions: Array?, grantResults: IntArray?) {
+ super.onRequestPermissionResult(requestCode, permissions, grantResults)
+ val results = grantResults ?: IntArray(0)
+ for (r in results) {
+ if (r == PackageManager.PERMISSION_DENIED) {
+ pushContext?.sendPluginResult(
+ PluginResult(PluginResult.Status.ILLEGAL_ACCESS_EXCEPTION,
+ "Permission to post notifications was denied by the user"
+ )
+ )
+ return
+ }
+ }
+ if (requestCode == REQ_CODE_INITIALIZE_PLUGIN) {
+ executeActionInitialize(pluginInitData!!, pushContext!!)
+ }
}
- }
}
diff --git a/src/android/com/adobe/phonegap/push/PushUtils.kt b/src/android/com/adobe/phonegap/push/PushUtils.kt
new file mode 100755
index 000000000..2ff231a23
--- /dev/null
+++ b/src/android/com/adobe/phonegap/push/PushUtils.kt
@@ -0,0 +1,488 @@
+package com.adobe.phonegap.push
+
+import android.content.Context
+import android.content.Intent
+import android.content.SharedPreferences
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffXfermode
+import android.graphics.Rect
+import android.os.Bundle
+import android.util.Log
+import androidx.core.app.NotificationCompat
+import org.json.JSONArray
+import org.json.JSONException
+import org.json.JSONObject
+import java.io.IOException
+import java.io.InputStream
+import java.net.HttpURLConnection
+import java.net.URL
+import java.util.ArrayList
+
+object PushUtils {
+ private const val TAG = "${PushPlugin.PREFIX_TAG} (PushUtils)"
+
+ const val VISIBILITY_PUBLIC_STR = "PUBLIC"
+ const val VISIBILITY_PRIVATE_STR = "PRIVATE"
+ const val VISIBILITY_SECRET_STR = "SECRET"
+
+ private fun replaceKey(context: Context, oldKey: String, newKey: String, extras: Bundle, newExtras: Bundle) {
+ /*
+ * Change a values key in the extras bundle
+ */
+ var value = extras[oldKey]
+ if (value != null) {
+ when (value) {
+ is String -> {
+ value = localizeKey(context, newKey, value)
+ newExtras.putString(newKey, value as String?)
+ }
+
+ is Boolean -> newExtras.putBoolean(newKey, (value as Boolean?) ?: return)
+
+ is Number -> {
+ newExtras.putDouble(newKey, value.toDouble())
+ }
+
+ else -> {
+ newExtras.putString(newKey, value.toString())
+ }
+ }
+ }
+ }
+
+ private fun localizeKey(context: Context, key: String, value: String?): String {
+ /*
+ * Normalize localization for key
+ */
+ value ?: return ""
+ return when (key) {
+ PushConstants.TITLE,
+ PushConstants.MESSAGE,
+ PushConstants.SUMMARY_TEXT,
+ -> {
+ try {
+ val localeObject = JSONObject(value)
+ val localeKey = localeObject.getString(PushConstants.LOC_KEY)
+ val localeFormatData = ArrayList()
+
+ if (!localeObject.isNull(PushConstants.LOC_DATA)) {
+ val localeData = localeObject.getString(PushConstants.LOC_DATA)
+ val localeDataArray = JSONArray(localeData)
+
+ for (i in 0 until localeDataArray.length()) {
+ localeFormatData.add(localeDataArray.getString(i))
+ }
+ }
+
+ val resourceId = context.resources.getIdentifier(
+ localeKey,
+ "string",
+ context.packageName
+ )
+
+ if (resourceId != 0) {
+ context.resources.getString(resourceId, *localeFormatData.toTypedArray())
+ } else {
+ Log.d(TAG, "Can't Find Locale Resource (key=$localeKey)")
+ value
+ }
+ } catch (e: JSONException) {
+ Log.d(TAG, "No Locale Found (key= $key, error=${e.message})")
+ value
+ }
+ }
+ else -> value
+ }
+ }
+
+ private fun normalizeKey(
+ key: String,
+ messageKey: String?,
+ titleKey: String?,
+ newExtras: Bundle,
+ ): String {
+ /*
+ * Replace alternate keys with our canonical value
+ */
+ return when {
+ key == PushConstants.BODY
+ || key == PushConstants.ALERT
+ || key == PushConstants.MP_MESSAGE
+ || key == PushConstants.GCM_NOTIFICATION_BODY
+ || key == PushConstants.TWILIO_BODY
+ || key == messageKey
+ || key == PushConstants.AWS_PINPOINT_BODY
+ -> {
+ PushConstants.MESSAGE
+ }
+
+ key == PushConstants.TWILIO_TITLE || key == PushConstants.SUBJECT || key == titleKey -> {
+ PushConstants.TITLE
+ }
+
+ key == PushConstants.MSGCNT || key == PushConstants.BADGE -> {
+ PushConstants.COUNT
+ }
+
+ key == PushConstants.SOUNDNAME || key == PushConstants.TWILIO_SOUND -> {
+ PushConstants.SOUND
+ }
+
+ key == PushConstants.AWS_PINPOINT_PICTURE -> {
+ newExtras.putString(PushConstants.STYLE, PushConstants.STYLE_PICTURE)
+ PushConstants.PICTURE
+ }
+
+ key.startsWith(PushConstants.GCM_NOTIFICATION) -> {
+ key.substring(PushConstants.GCM_NOTIFICATION.length + 1, key.length)
+ }
+
+ key.startsWith(PushConstants.GCM_N) -> {
+ key.substring(PushConstants.GCM_N.length + 1, key.length)
+ }
+
+ key.startsWith(PushConstants.UA_PREFIX) -> {
+ key.substring(PushConstants.UA_PREFIX.length + 1, key.length).lowercase()
+ }
+
+ key.startsWith(PushConstants.AWS_PINPOINT_PREFIX) -> {
+ key.substring(PushConstants.AWS_PINPOINT_PREFIX.length + 1, key.length)
+ }
+
+ else -> key
+ }
+ }
+
+ fun normalizeExtras(
+ context: Context,
+ extras: Bundle,
+ messageKey: String?,
+ titleKey: String?,
+ ): Bundle {
+ /*
+ * Parse bundle into normalized keys.
+ */
+ Log.d(TAG, "normalize extras")
+
+ val it: Iterator = extras.keySet().iterator()
+ val newExtras = Bundle()
+
+ while (it.hasNext()) {
+ val key = it.next()
+ Log.d(TAG, "key = $key")
+
+ // If normalizeKey, the key is "data" or "message" and the value is a json object extract
+ // This is to support parse.com and other services. Issue #147 and pull #218
+ if (
+ key == PushConstants.PARSE_COM_DATA ||
+ key == PushConstants.MESSAGE ||
+ key == messageKey
+ ) {
+ val json = extras[key]
+
+ // Make sure data is in json object string format
+ if (json is String && json.startsWith("{")) {
+ Log.d(TAG, "extracting nested message data from key = $key")
+
+ try {
+ // If object contains message keys promote each value to the root of the bundle
+ val data = JSONObject(json)
+ if (
+ data.has(PushConstants.ALERT)
+ || data.has(PushConstants.MESSAGE)
+ || data.has(PushConstants.BODY)
+ || data.has(PushConstants.TITLE)
+ || data.has(messageKey)
+ || data.has(titleKey)
+ ) {
+ val jsonKeys = data.keys()
+
+ while (jsonKeys.hasNext()) {
+ var jsonKey = jsonKeys.next()
+ Log.d(TAG, "key = data/$jsonKey")
+
+ var value = data.getString(jsonKey)
+ jsonKey = normalizeKey(jsonKey, messageKey, titleKey, newExtras)
+ value = localizeKey(context, jsonKey, value)
+ newExtras.putString(jsonKey, value)
+ }
+ } else if (data.has(PushConstants.LOC_KEY) || data.has(PushConstants.LOC_DATA)) {
+ val newKey = normalizeKey(key, messageKey, titleKey, newExtras)
+ Log.d(TAG, "replace key $key with $newKey")
+ replaceKey(context, key, newKey, extras, newExtras)
+ }
+ } catch (e: JSONException) {
+ Log.e(TAG, "normalizeExtras: JSON exception")
+ }
+ } else {
+ val newKey = normalizeKey(key, messageKey, titleKey, newExtras)
+ Log.d(TAG, "replace key $key with $newKey")
+ replaceKey(context, key, newKey, extras, newExtras)
+ }
+ } else if (key == "notification") {
+ val value = extras.getBundle(key)
+ val iterator: Iterator = value!!.keySet().iterator()
+
+ while (iterator.hasNext()) {
+ val notificationKey = iterator.next()
+ Log.d(TAG, "notificationKey = $notificationKey")
+
+ val newKey = normalizeKey(notificationKey, messageKey, titleKey, newExtras)
+ Log.d(TAG, "Replace key $notificationKey with $newKey")
+
+ var valueData = value.getString(notificationKey)
+ valueData = localizeKey(context, newKey, valueData!!)
+ newExtras.putString(newKey, valueData)
+ }
+ continue
+ // In case we weren't working on the payload data node or the notification node,
+ // normalize the key.
+ // This allows to have "message" as the payload data key without colliding
+ // with the other "message" key (holding the body of the payload)
+ // See issue #1663
+ } else {
+ val newKey = normalizeKey(key, messageKey, titleKey, newExtras)
+ Log.d(TAG, "replace key $key with $newKey")
+ replaceKey(context, key, newKey, extras, newExtras)
+ }
+ } // while
+ return newExtras
+ }
+
+ fun extractBadgeCount(extras: Bundle?): Int {
+ var count = -1
+
+ try {
+ extras?.getString(PushConstants.COUNT)?.let {
+ count = it.toInt()
+ }
+ } catch (e: NumberFormatException) {
+ Log.e(TAG, e.localizedMessage, e)
+ }
+
+ return count
+ }
+
+ fun updateIntent(
+ intent: Intent,
+ callback: String,
+ extras: Bundle?,
+ foreground: Boolean,
+ notId: Int,
+ ) {
+ intent.apply {
+ putExtra(PushConstants.CALLBACK, callback)
+ putExtra(PushConstants.PUSH_BUNDLE, extras)
+ putExtra(PushConstants.FOREGROUND, foreground)
+ putExtra(PushConstants.NOT_ID, notId)
+ }
+ }
+
+
+ private fun getCircleBitmap(bitmap: Bitmap?): Bitmap? {
+ if (bitmap == null) {
+ return null
+ }
+
+ val output = Bitmap.createBitmap(
+ bitmap.width,
+ bitmap.height,
+ Bitmap.Config.ARGB_8888
+ )
+
+ val paint = Paint().apply {
+ isAntiAlias = true
+ color = Color.RED
+ xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
+ }
+
+ Canvas(output).apply {
+ drawARGB(0, 0, 0, 0)
+
+ val cx = (bitmap.width / 2).toFloat()
+ val cy = (bitmap.height / 2).toFloat()
+ val radius = if (cx < cy) cx else cy
+ val rect = Rect(0, 0, bitmap.width, bitmap.height)
+
+ drawCircle(cx, cy, radius, paint)
+ drawBitmap(bitmap, rect, rect, paint)
+ }
+
+ bitmap.recycle()
+ return output
+ }
+
+ fun setNotificationLargeIcon(context: Context,
+ extras: Bundle?,
+ mBuilder: NotificationCompat.Builder,
+ ) {
+ extras?.let {
+ val gcmLargeIcon = it.getString(PushConstants.IMAGE)
+ val imageType = it.getString(PushConstants.IMAGE_TYPE, PushConstants.IMAGE_TYPE_SQUARE)
+
+ if (gcmLargeIcon != null && gcmLargeIcon != "") {
+ if (
+ gcmLargeIcon.startsWith("http://")
+ || gcmLargeIcon.startsWith("https://")
+ ) {
+ val bitmap = getBitmapFromURL(gcmLargeIcon)
+
+ if (PushConstants.IMAGE_TYPE_SQUARE.equals(imageType, ignoreCase = true)) {
+ mBuilder.setLargeIcon(bitmap)
+ } else {
+ val bm = getCircleBitmap(bitmap)
+ mBuilder.setLargeIcon(bm)
+ }
+
+ Log.d(TAG, "Using remote large-icon from GCM")
+ } else {
+ try {
+ val inputStream: InputStream = context.assets.open(gcmLargeIcon)
+
+ val bitmap = BitmapFactory.decodeStream(inputStream)
+
+ if (PushConstants.IMAGE_TYPE_SQUARE.equals(imageType, ignoreCase = true)) {
+ mBuilder.setLargeIcon(bitmap)
+ } else {
+ val bm = getCircleBitmap(bitmap)
+ mBuilder.setLargeIcon(bm)
+ }
+
+ Log.d(TAG, "Using assets large-icon from GCM")
+ } catch (e: IOException) {
+ val largeIconId: Int = getImageId(context, gcmLargeIcon)
+
+ if (largeIconId != 0) {
+ val largeIconBitmap = BitmapFactory.decodeResource(context.resources, largeIconId)
+ mBuilder.setLargeIcon(largeIconBitmap)
+ Log.d(TAG, "Using resources large-icon from GCM")
+ } else {
+ Log.d(TAG, "Not large icon settings")
+ }
+ }
+ }
+ }
+ }
+ }
+
+ fun getImageId(context: Context, icon: String): Int {
+ var iconId = context.resources.getIdentifier(icon, PushConstants.DRAWABLE, context.packageName)
+ if (iconId == 0) {
+ iconId = context.resources.getIdentifier(icon, "mipmap", context.packageName)
+ }
+ return iconId
+ }
+
+ fun setNotificationSmallIcon(
+ context: Context,
+ extras: Bundle?,
+ mBuilder: NotificationCompat.Builder,
+ localIcon: String?,
+ ) {
+ extras?.let {
+ val icon = it.getString(PushConstants.ICON)
+
+ val iconId = when {
+ !icon.isNullOrEmpty() -> {
+ getImageId(context, icon)
+ }
+
+ !localIcon.isNullOrEmpty() -> {
+ getImageId(context, localIcon)
+ }
+
+ else -> {
+ Log.d(TAG, "No icon resource found from settings, using application icon")
+ context.applicationInfo.icon
+ }
+ }
+
+ mBuilder.setSmallIcon(iconId)
+ }
+ }
+
+ fun setNotificationIconColor(
+ color: String?,
+ mBuilder: NotificationCompat.Builder,
+ localIconColor: String?,
+ ) {
+ val iconColor = when {
+ color != null && color != "" -> {
+ try {
+ Color.parseColor(color)
+ } catch (e: IllegalArgumentException) {
+ Log.e(TAG, "Couldn't parse color from Android options")
+ }
+ }
+
+ localIconColor != null && localIconColor != "" -> {
+ try {
+ Color.parseColor(localIconColor)
+ } catch (e: IllegalArgumentException) {
+ Log.e(TAG, "Couldn't parse color from android options")
+ }
+ }
+
+ else -> {
+ Log.d(TAG, "No icon color settings found")
+ 0
+ }
+ }
+
+ if (iconColor != 0) {
+ mBuilder.color = iconColor
+ }
+ }
+
+ fun getBitmapFromURL(strURL: String?): Bitmap? {
+ return try {
+ val url = URL(strURL)
+ val connection = (url.openConnection() as HttpURLConnection).apply {
+ connectTimeout = 15000
+ doInput = true
+ connect()
+ }
+ val input = connection.inputStream
+ BitmapFactory.decodeStream(input)
+ } catch (e: IOException) {
+ e.printStackTrace()
+ null
+ }
+ }
+
+ fun parseNotificationIdToInt(extras: Bundle?): Int {
+ var returnVal = 0
+
+ try {
+ returnVal = extras?.getString(PushConstants.NOT_ID)?.toInt() ?: 0
+ } catch (e: NumberFormatException) {
+ Log.e(TAG, "NumberFormatException occurred: ${PushConstants.NOT_ID}: ${e.message}")
+ } catch (e: Exception) {
+ Log.e(TAG, "Exception occurred when parsing ${PushConstants.NOT_ID}: ${e.message}")
+ }
+
+ return returnVal
+ }
+
+ fun isAvailableSender(pushSharedPref: SharedPreferences, from: String?): Boolean {
+ val savedSenderID = pushSharedPref.getString(PushConstants.SENDER_ID, "")
+ Log.d(TAG, "sender id = $savedSenderID")
+ return from == savedSenderID || from!!.startsWith("/topics/")
+ }
+
+ fun getNotificationVisibility(value: String): Int {
+ return when (value) {
+ VISIBILITY_PUBLIC_STR -> NotificationCompat.VISIBILITY_PUBLIC
+ VISIBILITY_PRIVATE_STR -> NotificationCompat.VISIBILITY_PRIVATE
+ VISIBILITY_SECRET_STR -> NotificationCompat.VISIBILITY_SECRET
+ else -> { NotificationCompat.VISIBILITY_PRIVATE }
+ }
+ }
+
+
+}
diff --git a/src/android/com/adobe/phonegap/push/StringExtensions.kt b/src/android/com/adobe/phonegap/push/StringExtensions.kt
new file mode 100755
index 000000000..3a1c8b40a
--- /dev/null
+++ b/src/android/com/adobe/phonegap/push/StringExtensions.kt
@@ -0,0 +1,15 @@
+package com.adobe.phonegap.push
+
+import android.text.Spanned
+import androidx.core.text.HtmlCompat
+
+fun String.fromHtml(): Spanned {
+ return HtmlCompat.fromHtml(this, HtmlCompat.FROM_HTML_MODE_LEGACY)
+}
+
+fun String.convertToTypedArray(): Array {
+ return this.replace("\\[".toRegex(), "")
+ .replace("]".toRegex(), "")
+ .split(",")
+ .toTypedArray()
+}
diff --git a/src/android/res/drawable/circle_animation_avd.xml b/src/android/res/drawable/circle_animation_avd.xml
new file mode 100644
index 000000000..da36f501a
--- /dev/null
+++ b/src/android/res/drawable/circle_animation_avd.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/android/res/drawable/ic_accept.xml b/src/android/res/drawable/ic_accept.xml
new file mode 100644
index 000000000..ee326aec2
--- /dev/null
+++ b/src/android/res/drawable/ic_accept.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
diff --git a/src/android/res/drawable/ic_brand_logo.xml b/src/android/res/drawable/ic_brand_logo.xml
new file mode 100644
index 000000000..005529d07
--- /dev/null
+++ b/src/android/res/drawable/ic_brand_logo.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
diff --git a/src/android/res/drawable/ic_decline.xml b/src/android/res/drawable/ic_decline.xml
new file mode 100644
index 000000000..3c8366381
--- /dev/null
+++ b/src/android/res/drawable/ic_decline.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
diff --git a/src/android/res/font/nunito_regular.ttf b/src/android/res/font/nunito_regular.ttf
new file mode 100644
index 000000000..c8c90b7c2
Binary files /dev/null and b/src/android/res/font/nunito_regular.ttf differ
diff --git a/src/android/res/layout/activity_incoming_call.xml b/src/android/res/layout/activity_incoming_call.xml
new file mode 100755
index 000000000..b1ee64f85
--- /dev/null
+++ b/src/android/res/layout/activity_incoming_call.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/android/res/values/push_dimens.xml b/src/android/res/values/push_dimens.xml
new file mode 100644
index 000000000..cfd9f25e8
--- /dev/null
+++ b/src/android/res/values/push_dimens.xml
@@ -0,0 +1,5 @@
+
+
+ 240dp
+ 200dp
+
diff --git a/src/android/res/values/push_styles.xml b/src/android/res/values/push_styles.xml
new file mode 100644
index 000000000..bcb1ff53b
--- /dev/null
+++ b/src/android/res/values/push_styles.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+