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" + } +}