diff --git a/app/build.gradle b/app/build.gradle
index 40f62082498..131caa85a36 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -62,6 +62,10 @@ android {
applicationId 'com.nextcloud.talk2'
dimension "default"
}
+ unifiedpush {
+ applicationId 'com.nextcloud.talk2'
+ dimension "default"
+ }
qa {
applicationId "com.nextcloud.talk2.qa"
dimension "default"
@@ -336,6 +340,8 @@ dependencies {
gplayImplementation 'com.google.android.gms:play-services-base:18.0.1'
gplayImplementation "com.google.firebase:firebase-messaging:23.0.5"
+ unifiedpushImplementation 'com.github.UnifiedPush:android-connector:2.0.1'
+
// implementation 'androidx.activity:activity-ktx:1.4.0'
}
diff --git a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java
index a493dc785ea..586328da9df 100644
--- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java
+++ b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java
@@ -387,7 +387,7 @@ public static String getUrlNextcloudPush(String baseUrl) {
public static String getUrlPushProxy() {
return NextcloudTalkApplication.Companion.getSharedApplication().
- getApplicationContext().getResources().getString(R.string.nc_push_server_url) + "/devices";
+ appPreferences.getPushServerUrl() + "/devices";
}
public static String getUrlForNotificationWithId(String baseUrl, String notificationId) {
diff --git a/app/src/main/java/com/nextcloud/talk/utils/PushUtils.java b/app/src/main/java/com/nextcloud/talk/utils/PushUtils.java
index 4010ba9c49d..18835b713f1 100644
--- a/app/src/main/java/com/nextcloud/talk/utils/PushUtils.java
+++ b/app/src/main/java/com/nextcloud/talk/utils/PushUtils.java
@@ -98,8 +98,7 @@ public PushUtils() {
String keyPath = NextcloudTalkApplication.Companion.getSharedApplication().getDir("PushKeystore", Context.MODE_PRIVATE).getAbsolutePath();
publicKeyFile = new File(keyPath, "push_key.pub");
privateKeyFile = new File(keyPath, "push_key.priv");
- proxyServer = NextcloudTalkApplication.Companion.getSharedApplication().getResources().
- getString(R.string.nc_push_server_url);
+ proxyServer = appPreferences.getPushServerUrl();
}
public SignatureVerification verifySignature(byte[] signatureBytes, byte[] subjectBytes) {
diff --git a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java
index 96b53631dac..2812fa28a74 100644
--- a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java
+++ b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java
@@ -126,6 +126,17 @@ public interface AppPreferences {
@RemoveMethod
void removePushToken();
+ @KeyByString("push_server_url")
+ @DefaultValue(R.string.nc_push_server_url)
+ String getPushServerUrl();
+
+ @KeyByString("push_server_url")
+ void setPushServerUrl(String pushServerUrl);
+
+ @KeyByString("push_server_url")
+ @RemoveMethod
+ void removePushServerUrl();
+
@KeyByString("tempClientCertAlias")
String getTemporaryClientCertAlias();
diff --git a/app/src/unifiedpush/AndroidManifest.xml b/app/src/unifiedpush/AndroidManifest.xml
new file mode 100644
index 00000000000..1bbaaa7e8bb
--- /dev/null
+++ b/app/src/unifiedpush/AndroidManifest.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/unifiedpush/java/com/nextcloud/talk/services/unifiedpush/ChatAndCallMessagingService.kt b/app/src/unifiedpush/java/com/nextcloud/talk/services/unifiedpush/ChatAndCallMessagingService.kt
new file mode 100644
index 00000000000..0f503bfbb09
--- /dev/null
+++ b/app/src/unifiedpush/java/com/nextcloud/talk/services/unifiedpush/ChatAndCallMessagingService.kt
@@ -0,0 +1,339 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * @author Tim Krüger
+ * Copyright (C) 2022 Tim Krüger
+ * Copyright (C) 2017-2019 Mario Danic
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.nextcloud.talk.services.unifiedpush
+
+import android.annotation.SuppressLint
+import android.app.Notification
+import android.app.PendingIntent
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.os.Handler
+import android.util.Base64
+import android.util.Log
+import androidx.core.app.NotificationCompat
+import androidx.emoji.text.EmojiCompat
+import androidx.work.Configuration
+import androidx.work.Data
+import androidx.work.OneTimeWorkRequest
+import androidx.work.WorkManager
+import autodagger.AutoInjector
+import com.bluelinelabs.logansquare.LoganSquare
+import com.nextcloud.talk.utils.unifiedpush.UnifiedPushMessagingService
+import com.nextcloud.talk.utils.unifiedpush.RemoteMessage
+import com.nextcloud.talk.R
+import com.nextcloud.talk.activities.CallNotificationActivity
+import com.nextcloud.talk.api.NcApi
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
+import com.nextcloud.talk.events.CallNotificationClick
+import com.nextcloud.talk.jobs.NotificationWorker
+import com.nextcloud.talk.jobs.PushRegistrationWorker
+import com.nextcloud.talk.models.SignatureVerification
+import com.nextcloud.talk.models.json.participants.Participant
+import com.nextcloud.talk.models.json.participants.ParticipantsOverall
+import com.nextcloud.talk.models.json.push.DecryptedPushMessage
+import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.NotificationUtils
+import com.nextcloud.talk.utils.NotificationUtils.cancelAllNotificationsForAccount
+import com.nextcloud.talk.utils.NotificationUtils.cancelExistingNotificationWithId
+import com.nextcloud.talk.utils.NotificationUtils.getCallRingtoneUri
+import com.nextcloud.talk.utils.PushUtils
+import com.nextcloud.talk.utils.bundle.BundleKeys
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FROM_NOTIFICATION_START_CALL
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY
+import com.nextcloud.talk.utils.preferences.AppPreferences
+import io.reactivex.Observable
+import io.reactivex.Observer
+import io.reactivex.disposables.Disposable
+import io.reactivex.schedulers.Schedulers
+import okhttp3.JavaNetCookieJar
+import okhttp3.OkHttpClient
+import org.greenrobot.eventbus.EventBus
+import org.greenrobot.eventbus.Subscribe
+import org.greenrobot.eventbus.ThreadMode
+import retrofit2.Retrofit
+import java.net.CookieManager
+import java.security.InvalidKeyException
+import java.security.NoSuchAlgorithmException
+import java.security.PrivateKey
+import java.util.concurrent.TimeUnit
+import javax.crypto.Cipher
+import javax.crypto.NoSuchPaddingException
+import javax.inject.Inject
+
+@SuppressLint("LongLogTag")
+@AutoInjector(NextcloudTalkApplication::class)
+class ChatAndCallMessagingService : UnifiedPushMessagingService() {
+
+ @Inject
+ lateinit var appPreferences: AppPreferences
+
+ private var decryptedPushMessage: DecryptedPushMessage? = null
+ private var signatureVerification: SignatureVerification? = null
+ private var handler: Handler = Handler()
+
+ @Inject
+ lateinit var retrofit: Retrofit
+
+ @Inject
+ lateinit var okHttpClient: OkHttpClient
+
+ @Inject
+ lateinit var eventBus: EventBus
+
+ init {
+ Configuration.Builder().setJobSchedulerJobIdRange(0, 1000)
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ sharedApplication!!.componentApplication.inject(this)
+ eventBus.register(this)
+ }
+
+ @Subscribe(threadMode = ThreadMode.BACKGROUND)
+ fun onMessageEvent(event: CallNotificationClick) {
+ Log.d(TAG, "CallNotification was clicked")
+ isServiceInForeground = false
+ stopForeground(true)
+ }
+
+ override fun onDestroy() {
+ Log.d(TAG, "onDestroy")
+ isServiceInForeground = false
+ eventBus.unregister(this)
+ stopForeground(true)
+ handler.removeCallbacksAndMessages(null)
+ super.onDestroy()
+ }
+
+ override fun onNewToken(token: String) {
+ super.onNewToken(token)
+ sharedApplication!!.componentApplication.inject(this)
+ appPreferences.pushToken = token
+ Log.d(TAG, "onNewToken. token = $token")
+
+ val data: Data = Data.Builder().putString(PushRegistrationWorker.ORIGIN, "onNewToken").build()
+ val pushRegistrationWork = OneTimeWorkRequest.Builder(PushRegistrationWorker::class.java)
+ .setInputData(data)
+ .build()
+ WorkManager.getInstance().enqueue(pushRegistrationWork)
+ }
+
+ override fun onMessageReceived(remoteMessage: RemoteMessage) {
+ Log.d(TAG, "onMessageReceived")
+ sharedApplication!!.componentApplication.inject(this)
+ if (!remoteMessage.data["subject"].isNullOrEmpty() && !remoteMessage.data["signature"].isNullOrEmpty()) {
+ decryptMessage(remoteMessage.data["subject"]!!, remoteMessage.data["signature"]!!)
+ }
+ }
+
+ @Suppress("Detekt.TooGenericExceptionCaught")
+ private fun decryptMessage(subject: String, signature: String) {
+ try {
+ val base64DecodedSubject = Base64.decode(subject, Base64.DEFAULT)
+ val base64DecodedSignature = Base64.decode(signature, Base64.DEFAULT)
+ val pushUtils = PushUtils()
+ val privateKey = pushUtils.readKeyFromFile(false) as PrivateKey
+ try {
+ signatureVerification = pushUtils.verifySignature(
+ base64DecodedSignature,
+ base64DecodedSubject
+ )
+ if (signatureVerification!!.signatureValid) {
+ decryptMessage(privateKey, base64DecodedSubject, subject, signature)
+ }
+ } catch (e1: NoSuchAlgorithmException) {
+ Log.e(NotificationWorker.TAG, "No proper algorithm to decrypt the message.", e1)
+ } catch (e1: NoSuchPaddingException) {
+ Log.e(NotificationWorker.TAG, "No proper padding to decrypt the message.", e1)
+ } catch (e1: InvalidKeyException) {
+ Log.e(NotificationWorker.TAG, "Invalid private key.", e1)
+ }
+ } catch (exception: Exception) {
+ Log.e(NotificationWorker.TAG, "Something went very wrong!", exception)
+ }
+ }
+
+ private fun decryptMessage(
+ privateKey: PrivateKey,
+ base64DecodedSubject: ByteArray?,
+ subject: String,
+ signature: String
+ ) {
+ val cipher = Cipher.getInstance("RSA/None/PKCS1Padding")
+ cipher.init(Cipher.DECRYPT_MODE, privateKey)
+ val decryptedSubject = cipher.doFinal(base64DecodedSubject)
+ decryptedPushMessage = LoganSquare.parse(
+ String(decryptedSubject),
+ DecryptedPushMessage::class.java
+ )
+ decryptedPushMessage?.apply {
+ Log.d(TAG, this.toString())
+ timestamp = System.currentTimeMillis()
+ if (delete) {
+ cancelExistingNotificationWithId(
+ applicationContext,
+ signatureVerification!!.userEntity!!,
+ notificationId
+ )
+ } else if (deleteAll) {
+ cancelAllNotificationsForAccount(applicationContext, signatureVerification!!.userEntity!!)
+ } else if (deleteMultiple) {
+ notificationIds!!.forEach {
+ cancelExistingNotificationWithId(
+ applicationContext,
+ signatureVerification!!.userEntity!!,
+ it
+ )
+ }
+ } else if (type == "call") {
+ val fullScreenIntent = Intent(applicationContext, CallNotificationActivity::class.java)
+ val bundle = Bundle()
+ bundle.putString(BundleKeys.KEY_ROOM_ID, decryptedPushMessage!!.id)
+ bundle.putParcelable(KEY_USER_ENTITY, signatureVerification!!.userEntity)
+ bundle.putBoolean(KEY_FROM_NOTIFICATION_START_CALL, true)
+ fullScreenIntent.putExtras(bundle)
+
+ fullScreenIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
+ val fullScreenPendingIntent = PendingIntent.getActivity(
+ this@ChatAndCallMessagingService,
+ 0,
+ fullScreenIntent,
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ } else {
+ PendingIntent.FLAG_UPDATE_CURRENT
+ }
+ )
+
+ val soundUri = getCallRingtoneUri(applicationContext!!, appPreferences)
+ val notificationChannelId = NotificationUtils.NOTIFICATION_CHANNEL_CALLS_V4
+ val uri = Uri.parse(signatureVerification!!.userEntity!!.baseUrl)
+ val baseUrl = uri.host
+
+ val notification =
+ NotificationCompat.Builder(this@ChatAndCallMessagingService, notificationChannelId)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setCategory(NotificationCompat.CATEGORY_CALL)
+ .setSmallIcon(R.drawable.ic_call_black_24dp)
+ .setSubText(baseUrl)
+ .setShowWhen(true)
+ .setWhen(decryptedPushMessage!!.timestamp)
+ .setContentTitle(EmojiCompat.get().process(decryptedPushMessage!!.subject))
+ .setAutoCancel(true)
+ .setOngoing(true)
+ // .setTimeoutAfter(45000L)
+ .setContentIntent(fullScreenPendingIntent)
+ .setFullScreenIntent(fullScreenPendingIntent, true)
+ .setSound(soundUri)
+ .build()
+ notification.flags = notification.flags or Notification.FLAG_INSISTENT
+ isServiceInForeground = true
+ checkIfCallIsActive(signatureVerification!!, decryptedPushMessage!!)
+ startForeground(decryptedPushMessage!!.timestamp.toInt(), notification)
+ } else {
+ val messageData = Data.Builder()
+ .putString(BundleKeys.KEY_NOTIFICATION_SUBJECT, subject)
+ .putString(BundleKeys.KEY_NOTIFICATION_SIGNATURE, signature)
+ .build()
+ val pushNotificationWork =
+ OneTimeWorkRequest.Builder(NotificationWorker::class.java).setInputData(messageData)
+ .build()
+ WorkManager.getInstance().enqueue(pushNotificationWork)
+ }
+ }
+ }
+
+ private fun checkIfCallIsActive(
+ signatureVerification: SignatureVerification,
+ decryptedPushMessage: DecryptedPushMessage
+ ) {
+ Log.d(TAG, "checkIfCallIsActive")
+ val ncApi = retrofit.newBuilder()
+ .client(okHttpClient.newBuilder().cookieJar(JavaNetCookieJar(CookieManager())).build()).build()
+ .create(NcApi::class.java)
+ var hasParticipantsInCall = true
+ var inCallOnDifferentDevice = false
+
+ val apiVersion = ApiUtils.getConversationApiVersion(
+ signatureVerification.userEntity,
+ intArrayOf(ApiUtils.APIv4, 1)
+ )
+
+ ncApi.getPeersForCall(
+ ApiUtils.getCredentials(
+ signatureVerification.userEntity!!.username,
+ signatureVerification.userEntity!!.token
+ ),
+ ApiUtils.getUrlForCall(
+ apiVersion,
+ signatureVerification.userEntity!!.baseUrl,
+ decryptedPushMessage.id
+ )
+ )
+ .repeatWhen { completed ->
+ completed.zipWith(Observable.range(1, OBSERVABLE_COUNT), { _, i -> i })
+ .flatMap { Observable.timer(OBSERVABLE_DELAY, TimeUnit.SECONDS) }
+ .takeWhile { isServiceInForeground && hasParticipantsInCall && !inCallOnDifferentDevice }
+ }
+ .subscribeOn(Schedulers.io())
+ .subscribe(object : Observer {
+ override fun onSubscribe(d: Disposable) = Unit
+
+ override fun onNext(participantsOverall: ParticipantsOverall) {
+ val participantList: List = participantsOverall.ocs!!.data!!
+ hasParticipantsInCall = participantList.isNotEmpty()
+ if (hasParticipantsInCall) {
+ for (participant in participantList) {
+ if (participant.actorId == signatureVerification.userEntity!!.userId &&
+ participant.actorType == Participant.ActorType.USERS
+ ) {
+ inCallOnDifferentDevice = true
+ break
+ }
+ }
+ }
+ if (!hasParticipantsInCall || inCallOnDifferentDevice) {
+ Log.d(TAG, "no participants in call OR inCallOnDifferentDevice")
+ stopForeground(true)
+ handler.removeCallbacksAndMessages(null)
+ }
+ }
+
+ override fun onError(e: Throwable) = Unit
+
+ override fun onComplete() {
+ stopForeground(true)
+ handler.removeCallbacksAndMessages(null)
+ }
+ })
+ }
+
+ companion object {
+ private val TAG = ChatAndCallMessagingService::class.simpleName
+ private const val OBSERVABLE_COUNT = 12
+ private const val OBSERVABLE_DELAY: Long = 5
+ }
+}
diff --git a/app/src/unifiedpush/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.kt b/app/src/unifiedpush/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.kt
new file mode 100644
index 00000000000..1174bd9c24a
--- /dev/null
+++ b/app/src/unifiedpush/java/com/nextcloud/talk/utils/ClosedInterfaceImpl.kt
@@ -0,0 +1,79 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * @author Andy Scherzinger
+ * @author Marcel Hibbe
+ * @author Jindrich Kolman
+ * Copyright (C) 2017-2019 Mario Danic
+ * Copyright (C) 2022 Marcel Hibbe
+ * Copyright (C) 2022 Andy Scherzinger
+ * Copyright (C) 2022 Jindrich Kolman
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.nextcloud.talk.utils
+
+import android.content.Intent
+import androidx.work.Data
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.OneTimeWorkRequest
+import androidx.work.PeriodicWorkRequest
+import androidx.work.WorkManager
+import autodagger.AutoInjector
+import com.nextcloud.talk.utils.unifiedpush.ProviderInstaller
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.interfaces.ClosedInterface
+import android.util.Log
+
+@AutoInjector(NextcloudTalkApplication::class)
+class ClosedInterfaceImpl : ClosedInterface, ProviderInstaller.ProviderInstallListener {
+
+ override val isGooglePlayServicesAvailable: Boolean = isUnifiedPushAvailable()
+
+ override fun providerInstallerInstallIfNeededAsync() {
+ NextcloudTalkApplication.sharedApplication?.let {
+ ProviderInstaller.installIfNeededAsync(
+ it.applicationContext,
+ this
+ )
+ }
+ }
+
+ override fun onProviderInstalled() {
+ // unused atm
+ }
+
+ override fun onProviderInstallFailed(p0: Int, p1: Intent?) {
+ // unused atm
+ }
+
+ private fun isUnifiedPushAvailable(): Boolean {
+ val unifiedPushAvailable =
+ NextcloudTalkApplication.sharedApplication?.let {
+ ProviderInstaller.isUnifiedPushAvailable(
+ it.applicationContext
+ )
+ }
+ return unifiedPushAvailable == true
+ }
+
+ override fun setUpPushTokenRegistration() {
+ android.util.Log.d(TAG, "setUpPushTokenRegistration")
+ }
+
+ companion object {
+ const val TAG = "ClosedInterfaceImpl"
+ }
+}
diff --git a/app/src/unifiedpush/java/com/nextcloud/talk/utils/unifiedpush/ProviderInstaller.kt b/app/src/unifiedpush/java/com/nextcloud/talk/utils/unifiedpush/ProviderInstaller.kt
new file mode 100644
index 00000000000..cb7ae86ecd8
--- /dev/null
+++ b/app/src/unifiedpush/java/com/nextcloud/talk/utils/unifiedpush/ProviderInstaller.kt
@@ -0,0 +1,62 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * @author Jindrich Kolman
+ * Copyright (C) 2017-2022 Mario Danic
+ * Copyright (C) 2022 Jindrich Kolman
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.nextcloud.talk.utils.unifiedpush
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+import org.unifiedpush.android.connector.UnifiedPush
+
+@SuppressLint("LongLogTag")
+open class ProviderInstaller {
+
+ interface ProviderInstallListener {
+
+ fun onProviderInstalled() {
+ // unused atm
+ }
+
+ fun onProviderInstallFailed(p0: Int, p1: Intent?) {
+ // unused atm
+ }
+
+ companion object {
+ const val TAG = "ProviderInstaller.ProviderInstallListener"
+ }
+ }
+
+ companion object {
+ lateinit var listener: ProviderInstallListener
+ fun installIfNeededAsync(context: Context, listener: ProviderInstallListener) {
+ this.listener = listener
+ UnifiedPush.registerAppWithDialog(context)
+ }
+
+ fun isUnifiedPushAvailable(context: Context): Boolean {
+ return UnifiedPush.getDistributors(context).isNotEmpty()
+ }
+
+ const val TAG = "ProviderInstaller"
+ }
+}
diff --git a/app/src/unifiedpush/java/com/nextcloud/talk/utils/unifiedpush/RemoteMessage.kt b/app/src/unifiedpush/java/com/nextcloud/talk/utils/unifiedpush/RemoteMessage.kt
new file mode 100644
index 00000000000..36ec749e4df
--- /dev/null
+++ b/app/src/unifiedpush/java/com/nextcloud/talk/utils/unifiedpush/RemoteMessage.kt
@@ -0,0 +1,39 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * Copyright (C) 2017-2022 Mario Danic
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.nextcloud.talk.utils.unifiedpush
+
+import com.google.gson.Gson
+import com.google.gson.JsonParseException
+import com.google.gson.reflect.TypeToken
+
+inline fun Gson.fromJson(json: String): T = fromJson(json, object : TypeToken() {}.type)
+
+typealias RemoteMessageJsonParseException = com.google.gson.JsonParseException
+
+open class RemoteMessage {
+
+ var data: HashMap = HashMap()
+
+ open fun fromJson(json: String): RemoteMessage {
+ this.data = Gson().fromJson(json)
+ return this
+ }
+}
diff --git a/app/src/unifiedpush/java/com/nextcloud/talk/utils/unifiedpush/UnifiedPushMessagingReceiver.kt b/app/src/unifiedpush/java/com/nextcloud/talk/utils/unifiedpush/UnifiedPushMessagingReceiver.kt
new file mode 100644
index 00000000000..1c3c36e5040
--- /dev/null
+++ b/app/src/unifiedpush/java/com/nextcloud/talk/utils/unifiedpush/UnifiedPushMessagingReceiver.kt
@@ -0,0 +1,96 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Jindrich Kolman
+ * Copyright (C) 2022 Jindrich Kolman
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.nextcloud.talk.utils.unifiedpush
+
+import android.annotation.SuppressLint
+import android.app.job.JobInfo
+import android.app.job.JobScheduler
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.os.PersistableBundle
+import android.util.Log
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.services.unifiedpush.ChatAndCallMessagingService
+import org.unifiedpush.android.connector.EXTRA_MESSAGE
+import org.unifiedpush.android.connector.ACTION_MESSAGE
+import org.unifiedpush.android.connector.ACTION_NEW_ENDPOINT
+import org.unifiedpush.android.connector.EXTRA_ENDPOINT
+import org.unifiedpush.android.connector.MessagingReceiver
+import javax.inject.Inject
+
+@SuppressLint("LongLogTag")
+open class UnifiedPushMessagingReceiver : MessagingReceiver() {
+ @JvmField
+ @Inject
+ var context: Context = NextcloudTalkApplication.sharedApplication!!.applicationContext
+
+ override fun onMessage(context: Context, message: ByteArray, instance: String) {
+ val strMessage = String(message)
+ Log.d(TAG, "New Message: $instance $strMessage")
+ scheduleJob(
+ PersistableBundle().apply {
+ putString("action", ACTION_MESSAGE)
+ putString(EXTRA_MESSAGE, strMessage)
+ }
+ )
+ }
+
+ override fun onNewEndpoint(context: Context, endpoint: String, instance: String) {
+ scheduleJob(
+ PersistableBundle().apply {
+ putString("action", ACTION_NEW_ENDPOINT)
+ putString(EXTRA_ENDPOINT, endpoint)
+ }
+ )
+ }
+
+ override fun onRegistrationFailed(context: Context, instance: String) {
+ Log.d(TAG, "Registration Failed: $instance")
+ }
+
+ override fun onUnregistered(context: Context, instance: String) {
+ Log.d(TAG, "$instance is unregistered")
+ }
+
+ override fun onReceive(context: Context, intent: Intent) {
+ Log.d(TAG, "event received $intent")
+ super.onReceive(context, intent)
+ }
+
+ open fun scheduleJob(input: PersistableBundle) {
+ Log.d(TAG, "ScheduleJob")
+ val jobScheduler: JobScheduler = this.context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
+ jobScheduler.schedule(
+ JobInfo.Builder(
+ 0,
+ ComponentName(this.context, ChatAndCallMessagingService::class.java)
+ ).apply {
+ setOverrideDeadline((120 * 1000).toLong()) // Maximum delay 120s
+ setExtras(input)
+ }.build()
+ )
+ }
+
+ companion object {
+ const val TAG = "UnifiedPushMessagingReceiver"
+ }
+}
diff --git a/app/src/unifiedpush/java/com/nextcloud/talk/utils/unifiedpush/UnifiedPushMessagingService.kt b/app/src/unifiedpush/java/com/nextcloud/talk/utils/unifiedpush/UnifiedPushMessagingService.kt
new file mode 100644
index 00000000000..c4a0d77e082
--- /dev/null
+++ b/app/src/unifiedpush/java/com/nextcloud/talk/utils/unifiedpush/UnifiedPushMessagingService.kt
@@ -0,0 +1,122 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Jindrich Kolman
+ * Copyright (C) 2022 Jindrich Kolman
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.nextcloud.talk.utils.unifiedpush
+
+import android.annotation.SuppressLint
+import android.app.job.JobParameters
+import android.app.job.JobService
+import android.net.Uri
+import android.os.*
+import android.util.Log
+import androidx.work.Configuration
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import org.unifiedpush.android.connector.ACTION_MESSAGE
+import org.unifiedpush.android.connector.ACTION_NEW_ENDPOINT
+import org.unifiedpush.android.connector.EXTRA_ENDPOINT
+import org.unifiedpush.android.connector.EXTRA_MESSAGE
+
+@SuppressLint("LongLogTag")
+open class UnifiedPushMessagingService : JobService() {
+ var isServiceInForeground: Boolean = false
+ private var params: JobParameters? = null
+
+ open fun onNewToken(token: String) {}
+ open fun onMessageReceived(remoteMessage: RemoteMessage) {}
+
+ override fun onStartJob(params: JobParameters?): Boolean {
+ Log.d(TAG, "onStartJob")
+ this.params = params
+ params?.let {
+ handleMessage(
+ it.extras.getString("action")!!,
+ it.extras.getString(EXTRA_MESSAGE),
+ it.extras.getString(EXTRA_ENDPOINT)
+ )
+ }
+ return isServiceInForeground
+ }
+
+ private fun handleMessage(action: String, message: String?, endpoint: String?) {
+ when (action) {
+ ACTION_NEW_ENDPOINT -> {
+ onNewEndpoint(endpoint!!)
+ }
+ ACTION_MESSAGE -> {
+ onMessage(message!!)
+ }
+ }
+ }
+
+ private fun onMessage(message: String) {
+ try {
+ onMessageReceived(
+ RemoteMessage().fromJson(message)
+ )
+ } catch (e: RemoteMessageJsonParseException) {
+ Log.d(TAG, "while parsing received message as JSON: $e")
+ }
+ checkForeground()
+ }
+
+ private fun checkForeground() {
+ Log.d(TAG, "checkForeground")
+ val handler = Handler()
+
+ if (isServiceInForeground) {
+ handler.postDelayed({ checkForeground() }, 10000)
+ } else {
+ jobFinished(this.params, false)
+ }
+ }
+
+ override fun onStopJob(params: JobParameters?): Boolean {
+ Log.d(TAG, "Not yet implemented")
+ stopForeground(true)
+ isServiceInForeground = false
+ return true
+ }
+
+ open fun onNewEndpoint(endpoint: String) {
+ Log.d(TAG, "New Endpoint: $endpoint")
+ val uri: Uri = Uri.parse(endpoint)
+ val protocol: String? = uri.scheme
+ val server: String? = uri.authority
+ val path: String? = uri.path
+ val args: Set = uri.queryParameterNames
+ Log.d(UnifiedPushMessagingReceiver.TAG, "Parsed Endpoint $protocol $server $path")
+ var token: String? = null
+ if (path == "/UP") {
+ setPushServerUrl("$protocol://$server$path")
+ token = uri.getQueryParameter("token")
+ }
+ if (token is String) {
+ onNewToken(token)
+ }
+ }
+
+ private fun setPushServerUrl(url: String) {
+ NextcloudTalkApplication.sharedApplication!!.appPreferences.pushServerUrl = url
+ }
+
+ companion object {
+ const val TAG = "UnifiedPushMessagingService"
+ }
+}