diff --git a/plugin.xml b/plugin.xml index 5aeb3d5f8..954a71bbd 100755 --- a/plugin.xml +++ b/plugin.xml @@ -37,13 +37,12 @@ - - - - - - - + + + + + + @@ -75,10 +74,14 @@ - + + + + + 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 deleted file mode 100644 index fb805731e..000000000 --- a/src/android/com/adobe/phonegap/push/BackgroundHandlerActivity.kt +++ /dev/null @@ -1,132 +0,0 @@ -package com.adobe.phonegap.push - -import android.annotation.SuppressLint -import android.app.Activity -import android.app.NotificationManager -import android.content.Intent -import android.os.Bundle -import android.util.Log -import androidx.core.app.RemoteInput - -/** - * Background Handler Activity - */ -@Suppress("HardCodedStringLiteral") -@SuppressLint("LongLogTag", "LogConditional") -class BackgroundHandlerActivity : Activity() { - companion object { - private const val TAG: String = "${PushPlugin.PREFIX_TAG} (BackgroundHandlerActivity)" - } - - /** - * This activity will be started if the user touches a notification that we own. - * We send it's data off to the push plugin for processing. - * If needed, we boot up the main activity to kickstart the application. - * - * @param savedInstanceState - * - * @see android.app.Activity#onCreate(android.os.Bundle) - */ - public override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - Log.v(TAG, "onCreate") - - intent.extras?.let { extras -> - val notId = extras.getInt(PushConstants.NOT_ID, 0) - val callback = extras.getString(PushConstants.CALLBACK) - val startOnBackground = extras.getBoolean(PushConstants.START_IN_BACKGROUND, false) - val dismissed = extras.getBoolean(PushConstants.DISMISSED, false) - - Log.d(TAG, "Not ID: $notId") - Log.d(TAG, "Callback: $callback") - Log.d(TAG, "Start In Background: $startOnBackground") - Log.d(TAG, "Dismissed: $dismissed") - - FCMService().setNotification(notId, "") - - if (!startOnBackground) { - val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager - notificationManager.cancel(FCMService.getAppName(this), notId) - } - - processPushBundle() - finish() - - if (!dismissed) { - // Tap the notification, app should start. - if (!PushPlugin.isActive) { - forceMainActivityReload(false) - } else { - forceMainActivityReload(true) - } - } - } - } - - private fun processPushBundle() { - /* - * Takes the pushBundle extras from the intent, - * and sends it through to the PushPlugin for processing. - */ - intent.extras?.let { extras -> - var originalExtras = extras.getBundle(PushConstants.PUSH_BUNDLE) - - if (originalExtras == null) { - originalExtras = extras - originalExtras.remove(PushConstants.FROM) - originalExtras.remove(PushConstants.MESSAGE_ID) - originalExtras.remove(PushConstants.COLLAPSE_KEY) - } - - originalExtras.putBoolean(PushConstants.FOREGROUND, false) - originalExtras.putBoolean(PushConstants.COLDSTART, !PushPlugin.isActive) - originalExtras.putBoolean(PushConstants.DISMISSED, extras.getBoolean(PushConstants.DISMISSED)) - originalExtras.putString( - PushConstants.ACTION_CALLBACK, - extras.getString(PushConstants.CALLBACK) - ) - originalExtras.remove(PushConstants.NO_CACHE) - - RemoteInput.getResultsFromIntent(intent)?.apply { - val reply = getCharSequence(PushConstants.INLINE_REPLY).toString() - Log.d(TAG, "Inline Reply: $reply") - - originalExtras.putString(PushConstants.INLINE_REPLY, reply) - } - - PushPlugin.sendExtras(originalExtras) - } - } - - private fun forceMainActivityReload(startOnBackground: Boolean) { - /* - * Forces the main activity to re-launch if it's unloaded. - */ - val launchIntent = packageManager.getLaunchIntentForPackage(applicationContext.packageName) - - intent.extras?.let { extras -> - launchIntent?.apply { - extras.getBundle(PushConstants.PUSH_BUNDLE)?.let { originalExtras -> - putExtras(originalExtras) - } - - addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - addFlags(Intent.FLAG_FROM_BACKGROUND) - putExtra(PushConstants.START_IN_BACKGROUND, startOnBackground) - } - } - - startActivity(launchIntent) - } - - /** - * - */ - override fun onResume() { - super.onResume() - - val notificationManager = this.getSystemService(NOTIFICATION_SERVICE) as NotificationManager - notificationManager.cancelAll() - } -} diff --git a/src/android/com/adobe/phonegap/push/FCMService.kt b/src/android/com/adobe/phonegap/push/FCMService.kt index 890283206..1d69fd902 100644 --- a/src/android/com/adobe/phonegap/push/FCMService.kt +++ b/src/android/com/adobe/phonegap/push/FCMService.kt @@ -1,38 +1,23 @@ package com.adobe.phonegap.push 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.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.NotificationCompat -import androidx.core.app.RemoteInput -import androidx.core.text.HtmlCompat 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.* + +private const val TAG = "${PushPlugin.PREFIX_TAG} (FCMService)" /** * Firebase Cloud Messaging Service Class @@ -40,1159 +25,282 @@ 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 context: Context + get() = applicationContext - 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 - } + private val pushSharedPref: SharedPreferences + get() = AndroidUtils.getPushSharedPref(context) /** - * Get the Application Name from Label + * Called when a new token is generated, after app install or token changes. + * + * @param token */ - 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() + override fun onNewToken(token: String) { + super.onNewToken(token) + Log.d(TAG, "Refreshed token: $token") - 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) + // TODO: Implement this method to send any registration to your app's servers. + //sendRegistrationToServer(token); } - for ((key, value) in message.data) { - extras.putString(key, value) - } - - 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) - - // Clear Badge - val clearBadge = pushSharedPref.getBoolean(PushConstants.CLEAR_BADGE, false) - if (clearBadge) { - setApplicationIconBadgeNumber(context, 0) - } - - // 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(extras) - } else { - Log.d(TAG, "In Background") - extras.putBoolean(PushConstants.COLDSTART, isActive) - showNotificationIfPossible(extras) - } - } - } - - 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?) + 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) } - is Boolean -> newExtras.putBoolean(newKey, (value as Boolean?) ?: return) - - is Number -> { - newExtras.putDouble(newKey, value.toDouble()) + for ((key, value) in message.data) { + extras.putString(key, value) } - else -> { - newExtras.putString(newKey, value.toString()) - } - } - } - } + if (PushUtils.isAvailableSender(pushSharedPref, from)) { + val messageKey = + pushSharedPref.getString(PushConstants.MESSAGE_KEY, PushConstants.MESSAGE) + val titleKey = pushSharedPref.getString(PushConstants.TITLE_KEY, PushConstants.TITLE) - 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 (!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) + // 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(extras) + } else { + Log.d(TAG, "In Background") + extras.putBoolean(PushConstants.COLDSTART, isActive) + showNotificationIfPossible(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) - } - - 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) - } + 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 = PushUtils.extractBadgeCount(extras) - 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( - 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 - ) - } - } - - 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 - ) + if (badgeCount >= 0) { + setApplicationIconBadgeNumber(context, badgeCount) } - 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 - ) + if (badgeCount == 0) { + val mNotificationManager = + getSystemService(NOTIFICATION_SERVICE) as NotificationManager + mNotificationManager.cancelAll() } - } - 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)) + 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") - messageMap[notId]?.let { messageList -> - val sizeList = messageList.size + val hasMessage = !message.isNullOrEmpty() + val hasTitle = !title.isNullOrEmpty() - if (sizeList > 1) { - val sizeListMessage = sizeList.toString() - var stacking: String? = "$sizeList more" + if (hasMessage || hasTitle) { + Log.d(TAG, "Create Notification") - 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))) + if (!hasTitle) { + extras.putString(PushConstants.TITLE, AndroidUtils.getAppName(this)) } - mBuilder.setStyle(bigText) - } + createNotification(extras) } - } - } - - 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))) - } - - mBuilder.apply { - setContentTitle(fromHtml(it.getString(PushConstants.TITLE))) - setContentText(fromHtml(message)) - setStyle(bigPicture) - } - } - - else -> { - setNotification(notId, "") - - message?.let { messageStr -> - val bigText = NotificationCompat.BigTextStyle().run { - bigText(fromHtml(messageStr)) - setBigContentTitle(fromHtml(it.getString(PushConstants.TITLE))) - - it.getString(PushConstants.SUMMARY_TEXT)?.let { summaryText -> - setSummaryText(fromHtml(summaryText)) - } - } - - mBuilder.setContentText(fromHtml(messageStr)) - mBuilder.setStyle(bigText) - } - } - } - } - } - private fun setNotificationSound(extras: Bundle?, mBuilder: NotificationCompat.Builder) { - extras?.let { - val soundName = it.getString(PushConstants.SOUNDNAME) ?: it.getString(PushConstants.SOUND) + if (!isActive && forceStart == "1") { + Log.d(TAG, "The app is not running, attempting to start in the background") - 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) - } - } - } - } - - private fun convertToTypedArray(item: String): Array { - return item.replace("\\[".toRegex(), "") - .replace("]".toRegex(), "") - .split(",") - .toTypedArray() - } - - 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) - - 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)") - } - } - } - } - - private 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() - } - } - } - } - - 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) + 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) + } - val bitmap = BitmapFactory.decodeStream(inputStream) + startActivity(intent) + } else if (contentAvailable == "1") { + Log.d( + TAG, + "The app is not running and content available is true, sending notification event" + ) - if (PushConstants.IMAGE_TYPE_SQUARE.equals(imageType, ignoreCase = true)) { - mBuilder.setLargeIcon(bitmap) - } else { - val bm = getCircleBitmap(bitmap) - mBuilder.setLargeIcon(bm) + sendExtras(extras) } - - 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") - } - } } - } - } - } - - 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 - } - - private fun setNotificationSmallIcon( - extras: Bundle?, - mBuilder: NotificationCompat.Builder, - localIcon: String?, - ) { - extras?.let { - val icon = it.getString(PushConstants.ICON) - val iconId = when { - icon != null && icon != "" -> { - getImageId(icon) + private fun createNotification(extras: Bundle?) { + val mNotificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + val appName = AndroidUtils.getAppName(this) + val notId = PushUtils.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) } - - localIcon != null && localIcon != "" -> { - getImageId(localIcon) + val random = SecureRandom() + var requestCode = random.nextInt() + val contentIntent = PendingIntent.getActivity( + this, + requestCode, + notificationIntent, + PendingIntent.FLAG_UPDATE_CURRENT or NotificationUtils.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 } - else -> { - Log.d(TAG, "No icon resource found from settings, using application icon") - context.applicationInfo.icon - } - } + requestCode = random.nextInt() - mBuilder.setSmallIcon(iconId) - } - } + val deleteIntent = PendingIntent.getBroadcast( + this, + requestCode, + dismissedNotificationIntent, + PendingIntent.FLAG_CANCEL_CURRENT or NotificationUtils.FLAG_IMMUTABLE + ) - 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 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 + ) - localIconColor != null && localIconColor != "" -> { - try { - Color.parseColor(localIconColor) - } catch (e: IllegalArgumentException) { - Log.e(TAG, "Couldn't parse color from android options") + /* + * 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) } - } - - else -> { - Log.d(TAG, "No icon color settings found") - 0 - } - } - if (iconColor != 0) { - mBuilder.color = iconColor + /* + * LED Notification + */ + NotificationUtils.setNotificationLedColor(extras, mBuilder) + + /* + * Priority Notification + */ + NotificationUtils.setNotificationPriority(extras, mBuilder) + + /* + * Notification message + */ + NotificationUtils.setNotificationMessage(notId, extras, mBuilder) + + /* + * Notification count + */ + PushUtils.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 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/MessagesStore.kt b/src/android/com/adobe/phonegap/push/MessagesStore.kt new file mode 100644 index 000000000..e1f742995 --- /dev/null +++ b/src/android/com/adobe/phonegap/push/MessagesStore.kt @@ -0,0 +1,41 @@ +package com.adobe.phonegap.push + +import java.util.ArrayList +import java.util.HashMap + +object MessagesStore { + + private val messageMap = HashMap>() + + /** + * Set Notification + * If message is empty or null, the message list is cleared. + * + * @param notId + * @param message + */ + fun set(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) + } + } + + /** + * Get Notification + * If no message found by @notId, returns empty message list. + * + * @param notId + */ + fun get(notId: Int): ArrayList { + return messageMap.getOrDefault(notId, ArrayList()) + } +} \ No newline at end of file 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..fdee8d4e3 --- /dev/null +++ b/src/android/com/adobe/phonegap/push/NotificationUtils.kt @@ -0,0 +1,424 @@ +package com.adobe.phonegap.push + +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.graphics.Color +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 org.json.JSONArray +import org.json.JSONException +import java.security.SecureRandom +import java.util.ArrayList + +object NotificationUtils { + private const val TAG = "${PushPlugin.PREFIX_TAG} (NotificationUtils)" + + + 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 + } + + 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 FLAG_MUTABLE + ) + } else { + Log.d(TAG, "push receiver for notId $notId") + + PendingIntent.getBroadcast( + context, + uniquePendingIntentRequestCode, + intent, + PendingIntent.FLAG_ONE_SHOT or FLAG_MUTABLE + ) + } + } + + foreground -> { + intent = Intent(context, PushHandlerActivity::class.java) + PushUtils.updateIntent(intent, callback, extras, foreground, notId) + pIntent = PendingIntent.getActivity( + context, uniquePendingIntentRequestCode, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or 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 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 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() + } + } + } + + fun setNotificationOngoing(extras: Bundle?, mBuilder: NotificationCompat.Builder) { + extras?.getString(PushConstants.ONGOING, "false")?.let { + mBuilder.setOngoing(it.toBoolean()) + } + } + + 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) { + } + } + mBuilder.setVibrate(results) + } else { + if (vibrateOption) { + mBuilder.setDefaults(Notification.DEFAULT_VIBRATE) + } + } + } + + 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 -> { + MessagesStore.set(notId, message) + mBuilder.setContentText(message?.fromHtml()) + + MessagesStore.get(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 -> { + MessagesStore.set(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 -> { + MessagesStore.set(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/PushDismissedHandler.kt b/src/android/com/adobe/phonegap/push/PushDismissedHandler.kt index 92c19db4f..b333a565c 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, "") + MessagesStore.set(notID, "") } } } diff --git a/src/android/com/adobe/phonegap/push/PushHandlerActivity.kt b/src/android/com/adobe/phonegap/push/PushHandlerActivity.kt index 1a60c078f..b2d970fdd 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, "") + MessagesStore.set(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..d1bbffaf9 100644 --- a/src/android/com/adobe/phonegap/push/PushPlugin.kt +++ b/src/android/com/adobe/phonegap/push/PushPlugin.kt @@ -901,12 +901,12 @@ class PushPlugin : CordovaPlugin() { override fun onRequestPermissionResult( requestCode: Int, - permissions: Array?, - grantResults: IntArray? + permissions: Array, + grantResults: IntArray ) { super.onRequestPermissionResult(requestCode, permissions, grantResults) - for (r in grantResults!!) { + for (r in grantResults) { if (r == PackageManager.PERMISSION_DENIED) { pushContext?.sendPluginResult( PluginResult( 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..6d0fa6573 --- /dev/null +++ b/src/android/com/adobe/phonegap/push/PushUtils.kt @@ -0,0 +1,494 @@ +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 + */ + 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 setNotificationCount(extras: Bundle?, mBuilder: NotificationCompat.Builder) { + val count = extractBadgeCount(extras) + if (count >= 0) { + Log.d(TAG, "count =[$count]") + mBuilder.setNumber(count) + } + } + + 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() +}