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 @@ + + + + + + + + + + + + + +