diff --git a/README.md b/README.md index 4f2ea2ae9..78dea6ca8 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ This plugin does not provide a way to determine which platform you are running o * [Examples](docs/EXAMPLES.md) * [Platform support](docs/PLATFORM_SUPPORT.md) * [Cloud build support (PG Build, IntelXDK)](docs/PHONEGAP_BUILD.md) +* [VoIP Videocall Callscreen Support](docs/VoIP_push_videocalls.md) * [Push notification payload details](docs/PAYLOAD.md) * [Contributing](.github/CONTRIBUTING.md) * [License (MIT)](MIT-LICENSE) diff --git a/docs/VoIP_push_videocalls.md b/docs/VoIP_push_videocalls.md new file mode 100644 index 000000000..850ab15c1 --- /dev/null +++ b/docs/VoIP_push_videocalls.md @@ -0,0 +1,86 @@ +# VoIP Videocall Callscreen Support + +Enable Videocalls to be triggered on remote devices by sending a push notification. + +This plugin allows for the displaying of a calling screen and then the activation of the app if the +user "picks up" the phone using Push Notifications to trigger the call. + +The user can also choose to decline the call. + +### Note 1: This plugin, out of the box, supports only **Android** VoIP Pushes. + +You can use another plugin, [Customized iOS Callkit Plugin](cordova-plugin-callkit@https://github.com/trusted-care/cordova-plugin-callkit.git#1906e36e900fa3dd500d9a7cc174332d6dfa6caa) to add the same functionality to your iOS app as well. + +### Note 2: Additional help is available + +If you want to integrate this in your app, we can share additional code & information on the implementation on the +client / Javascript side too, please get in touch with us -> daniel at dornhardt dot com & we can go from there. + +## How to do it? + + - Add additional information to the push payload + - Customize the call screen for use with your app + +## Push Payload: + +In order to activate the calling screen in the app, you need to add the following to your push & the push plugin will show a calling screen: + +```js + var content = { + priority: 'normal', // Valid values are "normal" and "high." + data: { + from: "Your Appname", + title: '', // title isn't necessary because the PN isn't visible + body: 'Incoming Call', // the text that is displayed in the notification + sound: 'default', + vibrate: true, + userId: our_user_id, // add additional information you might need in the cordova app (?) + voip: true, // THIS activates the callscreen + caller: `${currentUser.profile.firstname} ${currentUser.profile.lastname}`, // construct a nice caller name for the call screen + isCancelPush: `${isCancel}`, // set this to true if you sent a call push before to _hang up_ the + // call in case the caller gives up before the recipient accepts or cancels the call + callId: 'individual_call_identifier', // we use this to create two webhook URLs which allow the plugin (see below) + // to update the call status on the calling server / app + callbackUrl: `${getExternalRootUrl()}updateStatus`, // this will be used to construct two callback URLs like this: + // + // The moment the device receives the push, this URL will be constructed & called in order to be + // able to show a "ringing" status on the calling site: + // + // `${getExternalRootUrl()}updateStatus?input=connected&id=` + // + // If the callee declines the call, the following URL will be constructed & called from the plugin: + // + // `${getExternalRootUrl()}updateStatus?input=declined_callee&id=` + + } +}; +``` + +### How to customize the call screen: + + +To customize view of call screen, you can replace/edit the following resources: +`res/drawable/ic_brand_logo.xml` - can be replaced to set new screen logo. + +To modify other elements of the screen you can change the following resource files: +`res/values/push_strings.xml` - to replace call screen texts, +`res/values/push_dimens.xml` - contains dimensions to change call screen margins, buttons sizes, +`res/values/push_styles.xml` - contains styles of call screen buttons, fonts, that can be changed. + + +For eg. `Meteor.js` users, you can use the `cordova-build-override` - folder to customize the callscreen: + +![Pasted image 20240227150303.png](./assets/cordova-build-override.png) + +[Meteor.js: Cordova: Advanced build customization ](https://guide.meteor.com/cordova#advanced-build) + +### Additional useful cordova packages: + +`cordova-plugin-insomnia@4.3.0` - make sure the phone doesn't lock itself whilst you are in a video call +`cordova-plugin-advanced-background-mode@https://github.com/brunochikuji/cordova-plugin-background-mode.git#5df0632fdd40d4e1f35fff3a632202824b70929d` - keep calls & connections open while the app is in the background + + +## Documentation ToDos: + +- add infos to `PAYLOAD.md` +- Document iOS VoIP Push configuration diff --git a/docs/assets/cordova-build-override.png b/docs/assets/cordova-build-override.png new file mode 100644 index 000000000..ef22df7d4 Binary files /dev/null and b/docs/assets/cordova-build-override.png differ diff --git a/plugin.xml b/plugin.xml index 5aeb3d5f8..36f4da404 100755 --- a/plugin.xml +++ b/plugin.xml @@ -33,11 +33,17 @@ + + + + + + @@ -71,14 +77,39 @@ - + + + + + + + + + + + + + + + + + #00054b + + + + + + + + + diff --git a/src/android/com/adobe/phonegap/push/FCMService.kt b/src/android/com/adobe/phonegap/push/FCMService.kt old mode 100644 new mode 100755 index 890283206..32ce6a421 --- a/src/android/com/adobe/phonegap/push/FCMService.kt +++ b/src/android/com/adobe/phonegap/push/FCMService.kt @@ -1,22 +1,30 @@ package com.adobe.phonegap.push +import android.Manifest import android.annotation.SuppressLint import android.app.Notification +import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent +import android.content.BroadcastReceiver 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.media.AudioAttributes 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.NotificationManagerCompat import androidx.core.app.RemoteInput +import androidx.core.app.TaskStackBuilder import androidx.core.text.HtmlCompat import com.adobe.phonegap.push.PushPlugin.Companion.isActive import com.adobe.phonegap.push.PushPlugin.Companion.isInForeground @@ -24,6 +32,7 @@ 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 @@ -56,6 +65,12 @@ class FCMService : FirebaseMessagingService() { 0 } + // VoIP + private const val CHANNEL_VOIP = "Voip" + private const val CHANNEL_NAME = "TCVoip" + private var voipNotificationActionBR: BroadcastReceiver? = null + const val VOIP_NOTIFICATION_ID = 168697 + /** * Get the Application Name from Label */ @@ -101,7 +116,7 @@ class FCMService : FirebaseMessagingService() { messageMap[notId] = messageList } - if (message == null || message.isEmpty()) { + if (message.isNullOrEmpty()) { messageList.clear() } else { messageList.add(message) @@ -141,27 +156,177 @@ class FCMService : FirebaseMessagingService() { 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) + // Detect if push message is VOIP call message + if ("true" == message.data[PushConstants.VOIP_CALL_KEY]) { // if this flag is true, then process as VOIP call event + if ("true" == message.data[PushConstants.VOIP_IS_CANCEL_PUSH_KEY]) { // if true, then this is cancel VOIP call event + IncomingCallHelper.dismissVOIPNotification(context, true) + IncomingCallActivity.dismissUnlockScreenNotification(this.applicationContext) + } else { // else start VOIP call, show incoming call screen + showVOIPNotification(message.data) + } + } else { // else process as push message event + // 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) + } } } } + // VoIP implementation + + private fun createNotificationChannel() { + // 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) + val voipCallChannelDescriptionRes = ResourcesMapper.getString(applicationContext, + ResourcesKeys.RES_STR_VOIP_CALL_CHANNEL_DESCRIPTION) + channel.description = getString(voipCallChannelDescriptionRes) + + // Set ringtone to notification (>= Android O) + val audioAttributes = AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .setUsage(AudioAttributes.USAGE_NOTIFICATION) + .build() + channel.setSound(IncomingCallHelper.defaultRingtoneUri(), audioAttributes) + + // Register the channel with the system; you can't change the importance + // or other notification behaviors after this + val notificationManager: NotificationManager = getSystemService(NotificationManager::class.java) + notificationManager.createNotificationChannel(channel) + } + } + + private fun showVOIPNotification(messageData: Map) { + createNotificationChannel() + val incomingCallCallerNameDefRes = ResourcesMapper.getString(applicationContext, + ResourcesKeys.RES_STR_INCOMING_CALL_CALLER_NAME_DEF) + val incomingCallTitleRes = ResourcesMapper.getString(applicationContext, + ResourcesKeys.RES_STR_INCOMING_CALL_TITLE) + val incomingCallBtnAcceptRes = ResourcesMapper.getString(applicationContext, + ResourcesKeys.RES_STR_INCOMING_CALL_BTN_ACCEPT) + val incomingCallBtnDeclineRes = ResourcesMapper.getString(applicationContext, + ResourcesKeys.RES_STR_INCOMING_CALL_BTN_DECLINE) + + // Prepare data from messageData + var caller: String? = getString(incomingCallCallerNameDefRes) + if (messageData.containsKey(PushConstants.VOIP_CALLER_NAME_KEY)) { + caller = messageData[PushConstants.VOIP_CALLER_NAME_KEY] + } + + val callId = messageData[PushConstants.VOIP_CALL_ID_KEY] + val callbackUrl = messageData[PushConstants.VOIP_CALLBACK_URL_KEY] + + // Read the message title from messageData + var title: String? = getString(incomingCallTitleRes) + if (messageData.containsKey(PushConstants.VOIP_MESSAGE_BODY_KEY)) { + title = messageData[PushConstants.VOIP_MESSAGE_BODY_KEY] + } + + // Update Webhook status to CONNECTED + IncomingCallHelper.updateWebhookVOIPStatus(callbackUrl, callId, PushConstants.VOIP_CONNECTED_KEY) + + // Intent for LockScreen or tapping on notification + val fullScreenIntent = Intent(this, IncomingCallActivity::class.java) + fullScreenIntent.putExtra(PushConstants.VOIP_CALLER_NAME_KEY, caller) + fullScreenIntent.putExtra(PushConstants.VOIP_EXTRA_CALLBACK_URL, callbackUrl) + fullScreenIntent.putExtra(PushConstants.VOIP_EXTRA_CALL_ID, callId) + + val fullScreenPendingIntent = PendingIntent.getActivity(context, 0, fullScreenIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + + // Intent for tapping on Answer + val acceptIntent = Intent(context, IncomingCallActionHandlerActivity::class.java) + acceptIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + acceptIntent.putExtra(PushConstants.VOIP_EXTRA_BUTTON_ACTION, PushConstants.VOIP_ACCEPT_KEY) + acceptIntent.putExtra(PushConstants.VOIP_EXTRA_CALLBACK_URL, callbackUrl) + acceptIntent.putExtra(PushConstants.VOIP_EXTRA_CALL_ID, callId) + + // Intent for tapping on Reject + val declineIntent = Intent(context, IncomingCallActionHandlerActivity::class.java) + declineIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + declineIntent.putExtra(PushConstants.VOIP_EXTRA_BUTTON_ACTION, PushConstants.VOIP_DECLINE_KEY) + declineIntent.putExtra(PushConstants.VOIP_EXTRA_CALLBACK_URL, callbackUrl) + declineIntent.putExtra(PushConstants.VOIP_EXTRA_CALL_ID, callId) + + val acceptPendingIntent = PendingIntent.getActivity( + this@FCMService, 10, + acceptIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + val declinePendingIntent = PendingIntent.getActivity( + this@FCMService, 20, + declineIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + val pushiconRes = ResourcesMapper.getDrawable(applicationContext, + ResourcesKeys.RES_DRAWABLE_PUSHICON) + + val acceptColor = ResourcesMapper.getColor(applicationContext, ResourcesKeys.RES_COLOR_ACCEPT_BTN) + val declineColor = ResourcesMapper.getColor(applicationContext, ResourcesKeys.RES_COLOR_DECLINE_BTN) + + val notificationBuilder = NotificationCompat.Builder(this, CHANNEL_VOIP) + .setSmallIcon(pushiconRes) + .setContentTitle(title) + .setContentText(caller) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setCategory(NotificationCompat.CATEGORY_CALL) // Show main activity on lock screen or when tapping on notification + .setFullScreenIntent(fullScreenPendingIntent, true) + .addAction(// Show decline action + NotificationCompat.Action( + 0, + ResourcesMapper.getActionText(applicationContext, incomingCallBtnDeclineRes, declineColor), + declinePendingIntent + ) + ) + .addAction( // Show Accept button + NotificationCompat.Action( + 0, + ResourcesMapper.getActionText(applicationContext, incomingCallBtnAcceptRes, acceptColor), + acceptPendingIntent + ) + ) + // Make notification dismiss on user input action + .setAutoCancel(true) // Cannot be swiped by user + .setOngoing(true) // Set ringtone to notification (< Android O) + .setSound(IncomingCallHelper.defaultRingtoneUri()) + + val incomingCallNotification: Notification = notificationBuilder.build() + val notificationManager = NotificationManagerCompat.from(this) + + // Display notification + if (ActivityCompat.checkSelfPermission(this, 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(VOIP_NOTIFICATION_ID, incomingCallNotification) + } + + // END of VoIP implementation + private fun replaceKey(oldKey: String, newKey: String, extras: Bundle, newExtras: Bundle) { /* * Change a values key in the extras bundle @@ -422,8 +587,8 @@ class FCMService : FirebaseMessagingService() { Log.d(TAG, "forceStart=$forceStart") Log.d(TAG, "badgeCount=$badgeCount") - val hasMessage = message != null && message.isNotEmpty() - val hasTitle = title != null && title.isNotEmpty() + val hasMessage = !message.isNullOrEmpty() + val hasTitle = !title.isNullOrEmpty() if (hasMessage || hasTitle) { Log.d(TAG, "Create Notification") @@ -468,12 +633,26 @@ class FCMService : FirebaseMessagingService() { } val random = SecureRandom() var requestCode = random.nextInt() - val contentIntent = PendingIntent.getActivity( - this, - requestCode, - notificationIntent, - PendingIntent.FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE - ) + + val contentIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + TaskStackBuilder.create(context).run { + addNextIntentWithParentStack(notificationIntent) + PendingIntent.getActivity( + this@FCMService, + requestCode, + notificationIntent, + PendingIntent.FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE + ) + } + } else { + PendingIntent.getActivity( + this@FCMService, + requestCode, + notificationIntent, + PendingIntent.FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE + ) + } + val dismissedNotificationIntent = Intent( this, PushDismissedHandler::class.java @@ -675,7 +854,7 @@ class FCMService : FirebaseMessagingService() { val foreground = action.optBoolean(PushConstants.FOREGROUND, true) val inline = action.optBoolean("inline", false) - var intent: Intent? + var intent: Intent var pIntent: PendingIntent? val callback = action.getString(PushConstants.CALLBACK) @@ -697,33 +876,64 @@ class FCMService : FirebaseMessagingService() { Log.d(TAG, "push activity for notId $notId") PendingIntent.getActivity( - this, - uniquePendingIntentRequestCode, - intent, - PendingIntent.FLAG_ONE_SHOT or FLAG_MUTABLE + this@FCMService, + uniquePendingIntentRequestCode, + intent, + PendingIntent.FLAG_ONE_SHOT or FLAG_MUTABLE ) - } else { - Log.d(TAG, "push receiver for notId $notId") + } else if (foreground) { + Log.d(TAG, "push receiver for notId $notId") PendingIntent.getBroadcast( this, uniquePendingIntentRequestCode, intent, PendingIntent.FLAG_ONE_SHOT or 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 (applicationInfo.targetSdkVersion >= Build.VERSION_CODES.S && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + intent = Intent(this, OnNotificationReceiverActivity::class.java) + 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(this, BackgroundActionButtonHandler::class.java) + updateIntent(intent, action.getString(PushConstants.CALLBACK), extras, foreground, notId) + PendingIntent.getBroadcast(this, uniquePendingIntentRequestCode, intent, flag) + } } } 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 - ) - } + 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 FLAG_IMMUTABLE + ) + } + } else { + PendingIntent.getActivity( + context, uniquePendingIntentRequestCode, + intent, PendingIntent.FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE + ) + } + } else -> { intent = Intent(this, BackgroundActionButtonHandler::class.java) updateIntent(intent, callback, extras, foreground, notId) diff --git a/src/android/com/adobe/phonegap/push/IncomingCallActionHandlerActivity.kt b/src/android/com/adobe/phonegap/push/IncomingCallActionHandlerActivity.kt new file mode 100644 index 000000000..d356623e4 --- /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(PushConstants.VOIP_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..d4de579c9 --- /dev/null +++ b/src/android/com/adobe/phonegap/push/IncomingCallActivity.kt @@ -0,0 +1,235 @@ +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 +import java.lang.ref.WeakReference + +private const val POST_NOTIFICATIONS_REQUEST_CODE = 8234 + +class IncomingCallActivity : Activity() { + + override fun onCreate(savedInstanceState: Bundle?) { + showWhenLockedAndTurnScreenOn() + super.onCreate(savedInstanceState) + + Log.d("", "IncomingCallActivity.onCreate()") + val activityIncomingCallRes = ResourcesMapper.getLayout(applicationContext, ResourcesKeys.RES_LAYOUT_ACTIVITY_INCOMING_CALL) + setContentView(activityIncomingCallRes) + + window.setFlags( + WindowManager.LayoutParams.FLAG_FULLSCREEN, + WindowManager.LayoutParams.FLAG_FULLSCREEN + ) + + instance = WeakReference(this) + + val tvCallerRes = ResourcesMapper.getId(applicationContext, ResourcesKeys.RES_TV_CALLER) + val btnAcceptRes = ResourcesMapper.getId(applicationContext, ResourcesKeys.RES_BTN_ACCEPT) + val btnDeclineRes = ResourcesMapper.getId(applicationContext, ResourcesKeys.RES_BTN_DECLINE) + val ivAnimatedCircleRes = ResourcesMapper.getId(applicationContext, ResourcesKeys.RES_ANIMATED_CIRCLE) + val circleAnimationAvdRes = ResourcesMapper.getDrawable(applicationContext, ResourcesKeys.RES_CIRCLE_ANIMATION_AVD) + + caller = intent?.extras?.getString(PushConstants.VOIP_CALLER_NAME_KEY) ?: "" + (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, PushConstants.VOIP_ACCEPT_KEY) + } + + private fun declineIncomingVoIP() { + Log.d("IC", "declineIncomingVoIP") + IncomingCallHelper.handleActionCall(applicationContext, intent, PushConstants.VOIP_DECLINE_KEY) + } + + @SuppressLint("MissingPermission") + private fun showUnlockScreenNotification() { + val pushiconRes = ResourcesMapper.getDrawable(applicationContext, + ResourcesKeys.RES_DRAWABLE_PUSHICON) + + val unlockDeviceTextRes = ResourcesMapper.getString(applicationContext, + ResourcesKeys.RES_STR_INCOMING_CALL_UNLOCK_DEVICE_TEXT) + val ongoingCallTitleRes = ResourcesMapper.getString(applicationContext, + ResourcesKeys.RES_STR_INCOMING_CALL_ONGOING_CALL_TITLE) + + val ongoingCallTitle = getString(ongoingCallTitleRes) + val unlockDeviceText = getString(unlockDeviceTextRes) + + val notificationBuilder = NotificationCompat.Builder(this, PushConstants.DEFAULT_CHANNEL_ID) + .setSmallIcon(pushiconRes) + .setContentTitle("$ongoingCallTitle $caller") + .setContentText(unlockDeviceText) + .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() + instance?.clear() + } + + class PhoneUnlockBroadcastReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action.equals(Intent.ACTION_USER_PRESENT)) { + dismissUnlockScreenNotification(context.applicationContext) + } + } + } + + companion object { + + private const val NOTIFICATION_MESSAGE_ID = 1337 + + var instance: WeakReference? = null + + var caller: String = "" + + 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 100644 index 000000000..952471b17 --- /dev/null +++ b/src/android/com/adobe/phonegap/push/IncomingCallHelper.kt @@ -0,0 +1,90 @@ +package com.adobe.phonegap.push + +import android.content.Context +import android.content.Intent +import android.media.RingtoneManager +import android.net.Uri +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 { + + 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 finishApp() { + IncomingCallActivity.instance?.get()?.finishAndRemoveTask() + } + + fun finishCallScreen() { + IncomingCallActivity.instance?.get()?.finish() + } + + fun dismissVOIPNotification(context: Context, finishCallScreen: Boolean = false) { + NotificationManagerCompat.from(context).cancel(FCMService.VOIP_NOTIFICATION_ID) + if (finishCallScreen) { + finishCallScreen() + } + } + + fun handleActionCall(context: Context, intent: Intent, voipStatus: String) { + val callbackUrl = intent.getStringExtra(PushConstants.VOIP_EXTRA_CALLBACK_URL) + val callId = intent.getStringExtra(PushConstants.VOIP_EXTRA_CALL_ID) + + // Handle actiontest + dismissVOIPNotification(context) + if (voipStatus == PushConstants.VOIP_ACCEPT_KEY) { + finishCallScreen() + } + + // 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 == PushConstants.VOIP_ACCEPT_KEY) { + context.startActivity(intentForLaunchActivity(context)) + } else { + finishApp() + } + } + + fun intentForLaunchActivity(context: Context): Intent? { + val pm = context.packageManager + val packageName = context.packageName + return pm?.getLaunchIntentForPackage(packageName) + } + + fun defaultRingtoneUri(): Uri { + return RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE) + } +} 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..db8e9b75a --- /dev/null +++ b/src/android/com/adobe/phonegap/push/OnNotificationReceiverActivity.kt @@ -0,0 +1,53 @@ +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, intent) + 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.packageName) + launchIntent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) + val data = intent.extras + if (data?.containsKey(PushConstants.NOTIFY_MESSAGE_TYPE_KEY) == false) { + data.putString(PushConstants.NOTIFY_MESSAGE_TYPE_KEY, PushConstants.NOTIFY_NOTIFICATION_VALUE) + } + data?.putString( + PushConstants.NOTIFY_TAP_KEY, + if (PushPlugin.isInBackground) PushConstants.NOTIFY_BACKGROUND_VALUE else PushConstants.NOTIFY_FOREGROUND_VALUE + ) + 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/PushConstants.kt b/src/android/com/adobe/phonegap/push/PushConstants.kt index e403e3677..489ac5182 100644 --- a/src/android/com/adobe/phonegap/push/PushConstants.kt +++ b/src/android/com/adobe/phonegap/push/PushConstants.kt @@ -7,107 +7,128 @@ package com.adobe.phonegap.push */ @Suppress("HardCodedStringLiteral") object PushConstants { - const val COM_ADOBE_PHONEGAP_PUSH: String = "com.adobe.phonegap.push" - const val REGISTRATION_ID: String = "registrationId" - const val REGISTRATION_TYPE: String = "registrationType" - const val FOREGROUND: String = "foreground" - const val TITLE: String = "title" - const val NOT_ID: String = "notId" - const val PUSH_BUNDLE: String = "pushBundle" - const val ICON: String = "icon" - const val ICON_COLOR: String = "iconColor" - const val SOUND: String = "sound" - const val SOUND_DEFAULT: String = "default" - const val SOUND_RINGTONE: String = "ringtone" - const val VIBRATE: String = "vibrate" - const val ACTIONS: String = "actions" - const val CALLBACK: String = "callback" - const val ACTION_CALLBACK: String = "actionCallback" - const val DRAWABLE: String = "drawable" - const val MSGCNT: String = "msgcnt" - const val VIBRATION_PATTERN: String = "vibrationPattern" - const val STYLE: String = "style" - const val SUMMARY_TEXT: String = "summaryText" - const val PICTURE: String = "picture" - const val GCM_N: String = "gcm.n." - const val GCM_NOTIFICATION: String = "gcm.notification" - const val GCM_NOTIFICATION_BODY: String = "gcm.notification.body" - const val UA_PREFIX: String = "com.urbanairship.push" - const val PARSE_COM_DATA: String = "data" - const val ALERT: String = "alert" - const val MESSAGE: String = "message" - const val BODY: String = "body" - const val SOUNDNAME: String = "soundname" - const val COLOR: String = "color" - const val LED_COLOR: String = "ledColor" - const val PRIORITY: String = "priority" - const val IMAGE: String = "image" - const val STYLE_INBOX: String = "inbox" - const val STYLE_PICTURE: String = "picture" - const val STYLE_TEXT: String = "text" - const val BADGE: String = "badge" - const val INITIALIZE: String = "init" - const val SUBSCRIBE: String = "subscribe" - const val UNSUBSCRIBE: String = "unsubscribe" - const val UNREGISTER: String = "unregister" - const val EXIT: String = "exit" - const val FINISH: String = "finish" - const val HAS_PERMISSION: String = "hasPermission" - const val ANDROID: String = "android" - const val SENDER_ID: String = "senderID" - const val CLEAR_BADGE: String = "clearBadge" - const val CLEAR_NOTIFICATIONS: String = "clearNotifications" - const val COLDSTART: String = "coldstart" - const val ADDITIONAL_DATA: String = "additionalData" - const val COUNT: String = "count" - const val FROM: String = "from" - const val COLLAPSE_KEY: String = "collapse_key" - const val FORCE_SHOW: String = "forceShow" - const val FCM: String = "FCM" - const val CONTENT_AVAILABLE: String = "content-available" - const val TOPICS: String = "topics" - const val SET_APPLICATION_ICON_BADGE_NUMBER: String = "setApplicationIconBadgeNumber" - const val GET_APPLICATION_ICON_BADGE_NUMBER: String = "getApplicationIconBadgeNumber" - const val CLEAR_ALL_NOTIFICATIONS: String = "clearAllNotifications" - const val VISIBILITY: String = "visibility" - const val INLINE_REPLY: String = "inlineReply" - const val INLINE_REPLY_LABEL: String = "replyLabel" - const val LOC_KEY: String = "locKey" - const val LOC_DATA: String = "locData" - const val TWILIO_BODY: String = "twi_body" - const val TWILIO_TITLE: String = "twi_title" - const val TWILIO_SOUND: String = "twi_sound" - const val AWS_PINPOINT_BODY: String = "pinpoint.notification.body" - const val AWS_PINPOINT_PICTURE: String = "pinpoint.notification.imageUrl" - const val AWS_PINPOINT_PREFIX: String = "pinpoint.notification" - const val MP_MESSAGE: String = "mp_message" - const val START_IN_BACKGROUND: String = "cdvStartInBackground" - const val FORCE_START: String = "force-start" - const val MESSAGE_KEY: String = "messageKey" - const val TITLE_KEY: String = "titleKey" - const val NO_CACHE: String = "no-cache" - const val DISMISSED: String = "dismissed" - const val IMAGE_TYPE: String = "image-type" - const val IMAGE_TYPE_SQUARE: String = "square" - const val IMAGE_TYPE_CIRCLE: String = "circle" - const val SUBJECT: String = "subject" - const val GOOGLE_APP_ID: String = "google_app_id" - const val GCM_DEFAULT_SENDER_ID: String = "gcm_defaultSenderId" - const val PUSH_DISMISSED: String = "push_dismissed" - const val DEFAULT_CHANNEL_ID: String = "PushPluginChannel" - const val CHANNELS: String = "channels" - const val CHANNEL_ID: String = "id" - const val CHANNEL_DESCRIPTION: String = "description" - const val CHANNEL_IMPORTANCE: String = "importance" - const val CHANNEL_LIGHT_COLOR: String = "lightColor" - const val CHANNEL_VIBRATION: String = "vibration" - const val ANDROID_CHANNEL_ID: String = "android_channel_id" - const val CHANNEL_STATE: String = "state" - const val CREATE_CHANNEL: String = "createChannel" - const val DELETE_CHANNEL: String = "deleteChannel" - const val ONGOING: String = "ongoing" - const val LIST_CHANNELS: String = "listChannels" - const val CLEAR_NOTIFICATION: String = "clearNotification" - const val MESSAGE_ID: String = "google.message_id" - const val IS_ENABLED: String = "isEnabled" + const val COM_ADOBE_PHONEGAP_PUSH: String = "com.adobe.phonegap.push" + const val REGISTRATION_ID: String = "registrationId" + const val REGISTRATION_TYPE: String = "registrationType" + const val FOREGROUND: String = "foreground" + const val TITLE: String = "title" + const val NOT_ID: String = "notId" + const val PUSH_BUNDLE: String = "pushBundle" + const val ICON: String = "icon" + const val ICON_COLOR: String = "iconColor" + const val SOUND: String = "sound" + const val SOUND_DEFAULT: String = "default" + const val SOUND_RINGTONE: String = "ringtone" + const val VIBRATE: String = "vibrate" + const val ACTIONS: String = "actions" + const val CALLBACK: String = "callback" + const val ACTION_CALLBACK: String = "actionCallback" + const val DRAWABLE: String = "drawable" + const val MSGCNT: String = "msgcnt" + const val VIBRATION_PATTERN: String = "vibrationPattern" + const val STYLE: String = "style" + const val SUMMARY_TEXT: String = "summaryText" + const val PICTURE: String = "picture" + const val GCM_N: String = "gcm.n." + const val GCM_NOTIFICATION: String = "gcm.notification" + const val GCM_NOTIFICATION_BODY: String = "gcm.notification.body" + const val UA_PREFIX: String = "com.urbanairship.push" + const val PARSE_COM_DATA: String = "data" + const val ALERT: String = "alert" + const val MESSAGE: String = "message" + const val BODY: String = "body" + const val SOUNDNAME: String = "soundname" + const val COLOR: String = "color" + const val LED_COLOR: String = "ledColor" + const val PRIORITY: String = "priority" + const val IMAGE: String = "image" + const val STYLE_INBOX: String = "inbox" + const val STYLE_PICTURE: String = "picture" + const val STYLE_TEXT: String = "text" + const val BADGE: String = "badge" + const val INITIALIZE: String = "init" + const val SUBSCRIBE: String = "subscribe" + const val UNSUBSCRIBE: String = "unsubscribe" + const val UNREGISTER: String = "unregister" + const val EXIT: String = "exit" + const val FINISH: String = "finish" + const val HAS_PERMISSION: String = "hasPermission" + const val ANDROID: String = "android" + const val SENDER_ID: String = "senderID" + const val CLEAR_BADGE: String = "clearBadge" + const val CLEAR_NOTIFICATIONS: String = "clearNotifications" + const val COLDSTART: String = "coldstart" + const val ADDITIONAL_DATA: String = "additionalData" + const val COUNT: String = "count" + const val FROM: String = "from" + const val COLLAPSE_KEY: String = "collapse_key" + const val FORCE_SHOW: String = "forceShow" + const val FCM: String = "FCM" + const val CONTENT_AVAILABLE: String = "content-available" + const val TOPICS: String = "topics" + const val SET_APPLICATION_ICON_BADGE_NUMBER: String = "setApplicationIconBadgeNumber" + const val GET_APPLICATION_ICON_BADGE_NUMBER: String = "getApplicationIconBadgeNumber" + const val CLEAR_ALL_NOTIFICATIONS: String = "clearAllNotifications" + const val VISIBILITY: String = "visibility" + const val INLINE_REPLY: String = "inlineReply" + const val INLINE_REPLY_LABEL: String = "replyLabel" + const val LOC_KEY: String = "locKey" + const val LOC_DATA: String = "locData" + const val TWILIO_BODY: String = "twi_body" + const val TWILIO_TITLE: String = "twi_title" + const val TWILIO_SOUND: String = "twi_sound" + const val AWS_PINPOINT_BODY: String = "pinpoint.notification.body" + const val AWS_PINPOINT_PICTURE: String = "pinpoint.notification.imageUrl" + const val AWS_PINPOINT_PREFIX: String = "pinpoint.notification" + const val MP_MESSAGE: String = "mp_message" + const val START_IN_BACKGROUND: String = "cdvStartInBackground" + const val FORCE_START: String = "force-start" + const val MESSAGE_KEY: String = "messageKey" + const val TITLE_KEY: String = "titleKey" + const val NO_CACHE: String = "no-cache" + const val DISMISSED: String = "dismissed" + const val IMAGE_TYPE: String = "image-type" + const val IMAGE_TYPE_SQUARE: String = "square" + const val IMAGE_TYPE_CIRCLE: String = "circle" + const val SUBJECT: String = "subject" + const val GOOGLE_APP_ID: String = "google_app_id" + const val GCM_DEFAULT_SENDER_ID: String = "gcm_defaultSenderId" + const val PUSH_DISMISSED: String = "push_dismissed" + const val DEFAULT_CHANNEL_ID: String = "PushPluginChannel" + const val CHANNELS: String = "channels" + const val CHANNEL_ID: String = "id" + const val CHANNEL_DESCRIPTION: String = "description" + const val CHANNEL_IMPORTANCE: String = "importance" + const val CHANNEL_LIGHT_COLOR: String = "lightColor" + const val CHANNEL_VIBRATION: String = "vibration" + const val ANDROID_CHANNEL_ID: String = "android_channel_id" + const val CHANNEL_STATE: String = "state" + const val CREATE_CHANNEL: String = "createChannel" + const val DELETE_CHANNEL: String = "deleteChannel" + const val ONGOING: String = "ongoing" + const val LIST_CHANNELS: String = "listChannels" + const val CLEAR_NOTIFICATION: String = "clearNotification" + const val MESSAGE_ID: String = "google.message_id" + const val IS_ENABLED: String = "isEnabled" + + const val NOTIFY_MESSAGE_TYPE_KEY = "messageType" + const val NOTIFY_TAP_KEY = "tap" + const val NOTIFY_NOTIFICATION_VALUE = "notification" + const val NOTIFY_BACKGROUND_VALUE = "background" + const val NOTIFY_FOREGROUND_VALUE = "foreground" + + const val VOIP_CALL_ID_KEY = "callId" + const val VOIP_CALLBACK_URL_KEY = "callbackUrl" + const val VOIP_MESSAGE_BODY_KEY = "body" + const val VOIP_CALL_KEY = "voip" + const val VOIP_IS_CANCEL_PUSH_KEY = "isCancelPush" + const val VOIP_CALLER_NAME_KEY = "caller" + + const val VOIP_EXTRA_BUTTON_ACTION = "extra_button_action" + const val VOIP_EXTRA_CALLBACK_URL = "extra_callback_url" + const val VOIP_EXTRA_CALL_ID = "extra_call_id" + + const val VOIP_CONNECTED_KEY = "connected" + const val VOIP_ACCEPT_KEY = "pickup" + const val VOIP_DECLINE_KEY = "declined_callee" } diff --git a/src/android/com/adobe/phonegap/push/PushPlugin.kt b/src/android/com/adobe/phonegap/push/PushPlugin.kt index 904c4262b..073ef7e6d 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/ResourcesKeys.kt b/src/android/com/adobe/phonegap/push/ResourcesKeys.kt new file mode 100644 index 000000000..bf597490e --- /dev/null +++ b/src/android/com/adobe/phonegap/push/ResourcesKeys.kt @@ -0,0 +1,35 @@ +package com.adobe.phonegap.push + +import android.content.Context + +/* +* Resources keys class to store required resources id names +* */ +object ResourcesKeys { + // Resources types names + const val RES_TYPE_ID = "id" + const val RES_TYPE_DRAWABLE = "drawable" + const val RES_TYPE_LAYOUT = "layout" + const val RES_TYPE_STRING = "string" + const val RES_TYPE_COLOR = "color" + + // IncomingCallActivity resources names + const val RES_LAYOUT_ACTIVITY_INCOMING_CALL = "activity_incoming_call" + const val RES_TV_CALLER = "tvCaller" + const val RES_BTN_ACCEPT = "btnAccept" + const val RES_BTN_DECLINE = "btnDecline" + const val RES_ANIMATED_CIRCLE = "ivAnimatedCircle" + const val RES_CIRCLE_ANIMATION_AVD = "circle_animation_avd" + + // VOIP Push resources names + const val RES_DRAWABLE_PUSHICON = "pushicon" + const val RES_STR_VOIP_CALL_CHANNEL_DESCRIPTION = "voip_call_channel_description" + const val RES_STR_INCOMING_CALL_CALLER_NAME_DEF = "incoming_call_caller_name_def" + const val RES_STR_INCOMING_CALL_TITLE = "incoming_call_title" + const val RES_STR_INCOMING_CALL_BTN_ACCEPT = "incoming_call_btn_accept" + const val RES_STR_INCOMING_CALL_BTN_DECLINE = "incoming_call_btn_decline" + const val RES_STR_INCOMING_CALL_ONGOING_CALL_TITLE = "incoming_call_ongoing_call_title" + const val RES_STR_INCOMING_CALL_UNLOCK_DEVICE_TEXT = "incoming_call_unlock_device_text" + const val RES_COLOR_ACCEPT_BTN = "accept_btn_color" + const val RES_COLOR_DECLINE_BTN = "decline_btn_color" +} diff --git a/src/android/com/adobe/phonegap/push/ResourcesMapper.kt b/src/android/com/adobe/phonegap/push/ResourcesMapper.kt new file mode 100644 index 000000000..82d62c2db --- /dev/null +++ b/src/android/com/adobe/phonegap/push/ResourcesMapper.kt @@ -0,0 +1,50 @@ +package com.adobe.phonegap.push + +import android.content.Context +import android.os.Build +import android.text.Spannable +import android.text.SpannableString +import android.text.style.ForegroundColorSpan +import androidx.annotation.ColorRes +import androidx.annotation.StringRes + +/* +* Resources mapper class to get resources id with variable project package name +* */ +object ResourcesMapper { + + fun getId(context: Context, name: String): Int { + return getResId(context, name, ResourcesKeys.RES_TYPE_ID) + } + + fun getLayout(context: Context, name: String): Int { + return getResId(context, name, ResourcesKeys.RES_TYPE_LAYOUT) + } + + fun getString(context: Context, name: String): Int { + return getResId(context, name, ResourcesKeys.RES_TYPE_STRING) + } + + fun getDrawable(context: Context, name: String): Int { + return getResId(context, name, ResourcesKeys.RES_TYPE_DRAWABLE) + } + + fun getColor(context: Context, name: String): Int { + return getResId(context, name, ResourcesKeys.RES_TYPE_COLOR) + } + + fun getResId(context: Context, name: String, type: String): Int { + val packageName = context.packageName + return context.resources.getIdentifier(name, type, packageName) + } + + fun getActionText(context: Context, @StringRes stringRes: Int, @ColorRes colorRes: Int): Spannable? { + val spannable: Spannable = SpannableString(context.getText(stringRes)) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + spannable.setSpan( + ForegroundColorSpan(context.getColor(colorRes)), 0, spannable.length, 0 + ) + } + return spannable + } +} 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..15388f6e1 --- /dev/null +++ b/src/android/res/drawable/ic_brand_logo.xml @@ -0,0 +1,5 @@ + + + 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..58b1f7f0f --- /dev/null +++ b/src/android/res/layout/activity_incoming_call.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + +