Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/talk-android.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarcel Hibbe <dev@mhibbe.de>2022-11-11 14:48:50 +0300
committerGitHub <noreply@github.com>2022-11-11 14:48:50 +0300
commit263edbc1d0fa4329fb02775d26f2ca8544a8b181 (patch)
treeae0d2e2867caafe9f31d4c0596252baf7af3c6a0
parentc212a86fe9691d6dab9d32390073bb04075dda66 (diff)
parent3729f1130a32e83d5ab674326858beb325e57284 (diff)
Merge pull request #2415 from nextcloud/feature/1724/missedCallNotification
Feature/1724/missed call notification
-rw-r--r--app/src/gplay/AndroidManifest.xml2
-rw-r--r--app/src/gplay/java/com/nextcloud/talk/services/firebase/ChatAndCallMessagingService.kt327
-rw-r--r--app/src/gplay/java/com/nextcloud/talk/services/firebase/NCFirebaseMessagingService.kt97
-rw-r--r--app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.java451
-rw-r--r--app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.kt476
-rw-r--r--app/src/main/java/com/nextcloud/talk/events/CallNotificationClick.kt23
-rw-r--r--app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.java695
-rw-r--r--app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt849
-rw-r--r--app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt2
-rw-r--r--app/src/main/java/com/nextcloud/talk/models/json/notifications/Notification.kt2
-rw-r--r--app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationOverall.kt2
-rw-r--r--app/src/main/java/com/nextcloud/talk/models/json/push/DecryptedPushMessage.kt1
-rw-r--r--app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java1
-rw-r--r--app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt3
-rw-r--r--app/src/main/res/drawable/ic_baseline_phone_missed_24.xml5
-rw-r--r--app/src/main/res/values/strings.xml2
16 files changed, 1437 insertions, 1501 deletions
diff --git a/app/src/gplay/AndroidManifest.xml b/app/src/gplay/AndroidManifest.xml
index 7da9db5a5..7661d02bd 100644
--- a/app/src/gplay/AndroidManifest.xml
+++ b/app/src/gplay/AndroidManifest.xml
@@ -39,7 +39,7 @@
<meta-data android:name="google_analytics_adid_collection_enabled" android:value="false" />
<service
- android:name=".services.firebase.ChatAndCallMessagingService"
+ android:name=".services.firebase.NCFirebaseMessagingService"
android:exported="false"
android:foregroundServiceType="phoneCall">
<intent-filter>
diff --git a/app/src/gplay/java/com/nextcloud/talk/services/firebase/ChatAndCallMessagingService.kt b/app/src/gplay/java/com/nextcloud/talk/services/firebase/ChatAndCallMessagingService.kt
deleted file mode 100644
index 26086c0aa..000000000
--- a/app/src/gplay/java/com/nextcloud/talk/services/firebase/ChatAndCallMessagingService.kt
+++ /dev/null
@@ -1,327 +0,0 @@
-/*
- * Nextcloud Talk application
- *
- * @author Mario Danic
- * @author Tim Krüger
- * Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
- * Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-package com.nextcloud.talk.services.firebase
-
-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.Data
-import androidx.work.OneTimeWorkRequest
-import androidx.work.WorkManager
-import autodagger.AutoInjector
-import com.bluelinelabs.logansquare.LoganSquare
-import com.google.firebase.messaging.FirebaseMessagingService
-import com.google.firebase.messaging.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 : FirebaseMessagingService() {
-
- @Inject
- lateinit var appPreferences: AppPreferences
-
- private var isServiceInForeground: Boolean = false
- 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
-
- 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!!.user!!, notificationId)
- } else if (deleteAll) {
- cancelAllNotificationsForAccount(applicationContext, signatureVerification!!.user!!)
- } else if (deleteMultiple) {
- notificationIds!!.forEach {
- cancelExistingNotificationWithId(applicationContext, signatureVerification!!.user!!, 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!!.user)
- 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.NotificationChannels.NOTIFICATION_CHANNEL_CALLS_V4.name
- val uri = Uri.parse(signatureVerification!!.user!!.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.user,
- intArrayOf(ApiUtils.APIv4, 1)
- )
-
- ncApi.getPeersForCall(
- ApiUtils.getCredentials(
- signatureVerification.user!!.username,
- signatureVerification.user!!.token
- ),
- ApiUtils.getUrlForCall(
- apiVersion,
- signatureVerification.user!!.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<ParticipantsOverall> {
- override fun onSubscribe(d: Disposable) = Unit
-
- override fun onNext(participantsOverall: ParticipantsOverall) {
- val participantList: List<Participant> = participantsOverall.ocs!!.data!!
- hasParticipantsInCall = participantList.isNotEmpty()
- if (hasParticipantsInCall) {
- for (participant in participantList) {
- if (participant.actorId == signatureVerification.user!!.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/gplay/java/com/nextcloud/talk/services/firebase/NCFirebaseMessagingService.kt b/app/src/gplay/java/com/nextcloud/talk/services/firebase/NCFirebaseMessagingService.kt
new file mode 100644
index 000000000..04f61b052
--- /dev/null
+++ b/app/src/gplay/java/com/nextcloud/talk/services/firebase/NCFirebaseMessagingService.kt
@@ -0,0 +1,97 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * @author Tim Krüger
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ * Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
+ * Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.services.firebase
+
+import android.util.Log
+import androidx.work.Data
+import androidx.work.OneTimeWorkRequest
+import androidx.work.WorkManager
+import autodagger.AutoInjector
+import com.google.firebase.messaging.FirebaseMessagingService
+import com.google.firebase.messaging.RemoteMessage
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
+import com.nextcloud.talk.jobs.NotificationWorker
+import com.nextcloud.talk.jobs.PushRegistrationWorker
+import com.nextcloud.talk.utils.bundle.BundleKeys
+import com.nextcloud.talk.utils.preferences.AppPreferences
+import javax.inject.Inject
+
+@AutoInjector(NextcloudTalkApplication::class)
+class NCFirebaseMessagingService : FirebaseMessagingService() {
+
+ @Inject
+ lateinit var appPreferences: AppPreferences
+
+ override fun onCreate() {
+ Log.d(TAG, "onCreate")
+ super.onCreate()
+ sharedApplication!!.componentApplication.inject(this)
+ }
+
+ override fun onMessageReceived(remoteMessage: RemoteMessage) {
+ Log.d(TAG, "onMessageReceived")
+ sharedApplication!!.componentApplication.inject(this)
+
+ Log.d(TAG, "remoteMessage.priority: " + remoteMessage.priority)
+ Log.d(TAG, "remoteMessage.originalPriority: " + remoteMessage.originalPriority)
+
+ val data = remoteMessage.data
+ val subject = data[KEY_NOTIFICATION_SUBJECT]
+ val signature = data[KEY_NOTIFICATION_SIGNATURE]
+
+ if (!subject.isNullOrEmpty() && !signature.isNullOrEmpty()) {
+ val messageData = Data.Builder()
+ .putString(BundleKeys.KEY_NOTIFICATION_SUBJECT, subject)
+ .putString(BundleKeys.KEY_NOTIFICATION_SIGNATURE, signature)
+ .build()
+ val notificationWork =
+ OneTimeWorkRequest.Builder(NotificationWorker::class.java).setInputData(messageData)
+ .build()
+ WorkManager.getInstance().enqueue(notificationWork)
+ }
+ }
+
+ override fun onNewToken(token: String) {
+ super.onNewToken(token)
+ Log.d(TAG, "onNewToken. token = $token")
+
+ appPreferences.pushToken = token
+
+ val data: Data = Data.Builder().putString(
+ PushRegistrationWorker.ORIGIN,
+ "NCFirebaseMessagingService#onNewToken"
+ ).build()
+ val pushRegistrationWork = OneTimeWorkRequest.Builder(PushRegistrationWorker::class.java)
+ .setInputData(data)
+ .build()
+ WorkManager.getInstance().enqueue(pushRegistrationWork)
+ }
+
+ companion object {
+ private val TAG = NCFirebaseMessagingService::class.simpleName
+ const val KEY_NOTIFICATION_SUBJECT = "subject"
+ const val KEY_NOTIFICATION_SIGNATURE = "signature"
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.java b/app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.java
deleted file mode 100644
index e07491dc1..000000000
--- a/app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.java
+++ /dev/null
@@ -1,451 +0,0 @@
-/*
- * Nextcloud Talk application
- *
- * @author Mario Danic
- * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-package com.nextcloud.talk.activities;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Configuration;
-import android.graphics.Bitmap;
-import android.graphics.drawable.BitmapDrawable;
-import android.media.AudioAttributes;
-import android.media.MediaPlayer;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Handler;
-import android.util.Log;
-import android.view.View;
-
-import com.facebook.common.executors.UiThreadImmediateExecutorService;
-import com.facebook.common.references.CloseableReference;
-import com.facebook.datasource.DataSource;
-import com.facebook.drawee.backends.pipeline.Fresco;
-import com.facebook.imagepipeline.core.ImagePipeline;
-import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber;
-import com.facebook.imagepipeline.image.CloseableImage;
-import com.facebook.imagepipeline.request.ImageRequest;
-import com.nextcloud.talk.R;
-import com.nextcloud.talk.api.NcApi;
-import com.nextcloud.talk.application.NextcloudTalkApplication;
-import com.nextcloud.talk.data.user.model.User;
-import com.nextcloud.talk.databinding.CallNotificationActivityBinding;
-import com.nextcloud.talk.events.CallNotificationClick;
-import com.nextcloud.talk.models.json.conversations.Conversation;
-import com.nextcloud.talk.models.json.conversations.RoomOverall;
-import com.nextcloud.talk.models.json.participants.Participant;
-import com.nextcloud.talk.models.json.participants.ParticipantsOverall;
-import com.nextcloud.talk.utils.ApiUtils;
-import com.nextcloud.talk.utils.DisplayUtils;
-import com.nextcloud.talk.utils.DoNotDisturbUtils;
-import com.nextcloud.talk.utils.NotificationUtils;
-import com.nextcloud.talk.utils.ParticipantPermissions;
-import com.nextcloud.talk.utils.bundle.BundleKeys;
-import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew;
-import com.nextcloud.talk.utils.preferences.AppPreferences;
-
-import org.greenrobot.eventbus.EventBus;
-import org.parceler.Parcels;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-import javax.inject.Inject;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import autodagger.AutoInjector;
-import io.reactivex.Observable;
-import io.reactivex.Observer;
-import io.reactivex.android.schedulers.AndroidSchedulers;
-import io.reactivex.annotations.NonNull;
-import io.reactivex.disposables.Disposable;
-import io.reactivex.schedulers.Schedulers;
-import okhttp3.Cache;
-
-@SuppressLint("LongLogTag")
-@AutoInjector(NextcloudTalkApplication.class)
-public class CallNotificationActivity extends CallBaseActivity {
-
- public static final String TAG = "CallNotificationActivity";
-
- @Inject
- NcApi ncApi;
-
- @Inject
- AppPreferences appPreferences;
-
- @Inject
- Cache cache;
-
- @Inject
- EventBus eventBus;
-
- @Inject
- Context context;
-
- private List<Disposable> disposablesList = new ArrayList<>();
- private Bundle originalBundle;
- private String roomId;
- private User userBeingCalled;
- private String credentials;
- private Conversation currentConversation;
- private MediaPlayer mediaPlayer;
- private boolean leavingScreen = false;
- private Handler handler;
- private CallNotificationActivityBinding binding;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- Log.d(TAG, "onCreate");
- super.onCreate(savedInstanceState);
-
- NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
-
- binding = CallNotificationActivityBinding.inflate(getLayoutInflater());
- setContentView(binding.getRoot());
-
- hideNavigationIfNoPipAvailable();
-
- eventBus.post(new CallNotificationClick());
-
- Bundle extras = getIntent().getExtras();
- this.roomId = extras.getString(BundleKeys.KEY_ROOM_ID, "");
- this.currentConversation = Parcels.unwrap(extras.getParcelable(BundleKeys.KEY_ROOM));
- this.userBeingCalled = extras.getParcelable(BundleKeys.KEY_USER_ENTITY);
-
- this.originalBundle = extras;
- credentials = ApiUtils.getCredentials(userBeingCalled.getUsername(), userBeingCalled.getToken());
-
- setCallDescriptionText();
-
- if (currentConversation == null) {
- handleFromNotification();
- } else {
- setUpAfterConversationIsKnown();
- }
-
- if (DoNotDisturbUtils.INSTANCE.shouldPlaySound()) {
- playRingtoneSound();
- }
-
- initClickListeners();
- }
-
- @Override
- public void onStart() {
- super.onStart();
-
- if (handler == null) {
- handler = new Handler();
-
- try {
- cache.evictAll();
- } catch (IOException e) {
- Log.e(TAG, "Failed to evict cache");
- }
- }
- }
-
- private void initClickListeners() {
- binding.callAnswerVoiceOnlyView.setOnClickListener(l -> {
- Log.d(TAG, "accept call (voice only)");
- originalBundle.putBoolean(BundleKeys.KEY_CALL_VOICE_ONLY, true);
- proceedToCall();
- });
-
- binding.callAnswerCameraView.setOnClickListener(l -> {
- Log.d(TAG, "accept call (with video)");
- originalBundle.putBoolean(BundleKeys.KEY_CALL_VOICE_ONLY, false);
- proceedToCall();
- });
-
- binding.hangupButton.setOnClickListener(l -> hangup());
- }
-
- private void setCallDescriptionText() {
- String callDescriptionWithoutTypeInfo =
- String.format(
- getResources().getString(R.string.nc_call_unknown),
- getResources().getString(R.string.nc_app_product_name));
-
- binding.incomingCallVoiceOrVideoTextView.setText(callDescriptionWithoutTypeInfo);
- }
-
- private void showAnswerControls() {
- binding.callAnswerCameraView.setVisibility(View.VISIBLE);
- binding.callAnswerVoiceOnlyView.setVisibility(View.VISIBLE);
- }
-
- private void hangup() {
- leavingScreen = true;
- finish();
- }
-
- private void proceedToCall() {
- originalBundle.putString(BundleKeys.KEY_ROOM_TOKEN, currentConversation.getToken());
- originalBundle.putString(BundleKeys.KEY_CONVERSATION_NAME, currentConversation.getDisplayName());
-
- ParticipantPermissions participantPermission = new ParticipantPermissions(userBeingCalled, currentConversation);
- originalBundle.putBoolean(
- BundleKeys.KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_AUDIO,
- participantPermission.canPublishAudio());
- originalBundle.putBoolean(
- BundleKeys.KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_VIDEO,
- participantPermission.canPublishVideo());
-
- Intent intent = new Intent(this, CallActivity.class);
- intent.putExtras(originalBundle);
- startActivity(intent);
- }
-
- private void checkIfAnyParticipantsRemainInRoom() {
- int apiVersion = ApiUtils.getCallApiVersion(userBeingCalled, new int[]{ApiUtils.APIv4, 1});
-
- ncApi.getPeersForCall(
- credentials,
- ApiUtils.getUrlForCall(
- apiVersion,
- userBeingCalled.getBaseUrl(),
- currentConversation.getToken()))
- .subscribeOn(Schedulers.io())
- .repeatWhen(completed -> completed.zipWith(Observable.range(1, 12), (n, i) -> i)
- .flatMap(retryCount -> Observable.timer(5, TimeUnit.SECONDS))
- .takeWhile(observable -> !leavingScreen))
- .subscribe(new Observer<ParticipantsOverall>() {
- @Override
- public void onSubscribe(@NonNull Disposable d) {
- disposablesList.add(d);
- }
-
- @Override
- public void onNext(@NonNull ParticipantsOverall participantsOverall) {
- boolean hasParticipantsInCall = false;
- boolean inCallOnDifferentDevice = false;
- List<Participant> participantList = participantsOverall.getOcs().getData();
- hasParticipantsInCall = participantList.size() > 0;
-
- if (hasParticipantsInCall) {
- for (Participant participant : participantList) {
- if (participant.getCalculatedActorType() == Participant.ActorType.USERS &&
- participant.getCalculatedActorId().equals(userBeingCalled.getUserId())) {
- inCallOnDifferentDevice = true;
- break;
- }
- }
- }
-
- if (!hasParticipantsInCall || inCallOnDifferentDevice) {
- runOnUiThread(() -> hangup());
- }
- }
-
- @Override
- public void onError(@NonNull Throwable e) {
- Log.e(TAG, "error while getPeersForCall", e);
- }
-
- @Override
- public void onComplete() {
- runOnUiThread(() -> hangup());
- }
- });
-
- }
-
- private void handleFromNotification() {
- int apiVersion = ApiUtils.getConversationApiVersion(userBeingCalled, new int[]{ApiUtils.APIv4,
- ApiUtils.APIv3, 1});
-
- ncApi.getRoom(credentials, ApiUtils.getUrlForRoom(apiVersion, userBeingCalled.getBaseUrl(), roomId))
- .subscribeOn(Schedulers.io())
- .retry(3)
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(new Observer<RoomOverall>() {
- @Override
- public void onSubscribe(@NonNull Disposable d) {
- disposablesList.add(d);
- }
-
- @Override
- public void onNext(@NonNull RoomOverall roomOverall) {
- currentConversation = roomOverall.getOcs().getData();
- setUpAfterConversationIsKnown();
-
- if (apiVersion >= 3) {
- boolean hasCallFlags =
- CapabilitiesUtilNew.hasSpreedFeatureCapability(userBeingCalled,
- "conversation-call-flags");
- if (hasCallFlags) {
- if (isInCallWithVideo(currentConversation.getCallFlag())) {
- binding.incomingCallVoiceOrVideoTextView.setText(
- String.format(getResources().getString(R.string.nc_call_video),
- getResources().getString(R.string.nc_app_product_name)));
- } else {
- binding.incomingCallVoiceOrVideoTextView.setText(
- String.format(getResources().getString(R.string.nc_call_voice),
- getResources().getString(R.string.nc_app_product_name)));
- }
- }
- }
- }
-
- @Override
- public void onError(@NonNull Throwable e) {
- Log.e(TAG, e.getMessage(), e);
- }
-
- @Override
- public void onComplete() {
- // unused atm
- }
- });
- }
-
- private boolean isInCallWithVideo(int callFlag) {
- return (callFlag >= Participant.InCallFlags.IN_CALL + Participant.InCallFlags.WITH_VIDEO);
- }
-
- private void setUpAfterConversationIsKnown() {
- binding.conversationNameTextView.setText(currentConversation.getDisplayName());
-
- if(currentConversation.getType() == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL){
- setAvatarForOneToOneCall();
- } else {
- binding.avatarImageView.setImageResource(R.drawable.ic_circular_group);
- }
-
- checkIfAnyParticipantsRemainInRoom();
- showAnswerControls();
- }
-
- private void setAvatarForOneToOneCall() {
- ImageRequest imageRequest =
- DisplayUtils.getImageRequestForUrl(
- ApiUtils.getUrlForAvatar(userBeingCalled.getBaseUrl(),
- currentConversation.getName(),
- true));
-
- ImagePipeline imagePipeline = Fresco.getImagePipeline();
- DataSource<CloseableReference<CloseableImage>> dataSource = imagePipeline.fetchDecodedImage(imageRequest, null);
-
- dataSource.subscribe(new BaseBitmapDataSubscriber() {
- @Override
- protected void onNewResultImpl(@Nullable Bitmap bitmap) {
- binding.avatarImageView.getHierarchy().setImage(
- new BitmapDrawable(getResources(), bitmap),
- 100,
- true);
- }
-
- @Override
- protected void onFailureImpl(DataSource<CloseableReference<CloseableImage>> dataSource) {
- Log.e(TAG, "failed to load avatar");
- }
- }, UiThreadImmediateExecutorService.getInstance());
- }
-
- private void endMediaNotifications() {
- if (mediaPlayer != null) {
- if (mediaPlayer.isPlaying()) {
- mediaPlayer.stop();
- }
-
- mediaPlayer.release();
- mediaPlayer = null;
- }
- }
-
- @Override
- public void onDestroy() {
- leavingScreen = true;
- if (handler != null) {
- handler.removeCallbacksAndMessages(null);
- handler = null;
- }
- dispose();
- endMediaNotifications();
- super.onDestroy();
- }
-
- private void dispose() {
- if (disposablesList != null) {
- for (Disposable disposable : disposablesList) {
- if (!disposable.isDisposed()) {
- disposable.dispose();
- }
- }
- }
- }
-
- private void playRingtoneSound() {
- Uri ringtoneUri = NotificationUtils.INSTANCE.getCallRingtoneUri(getApplicationContext(), appPreferences);
- if (ringtoneUri != null) {
- mediaPlayer = new MediaPlayer();
- try {
- mediaPlayer.setDataSource(this, ringtoneUri);
-
- mediaPlayer.setLooping(true);
- AudioAttributes audioAttributes = new AudioAttributes
- .Builder()
- .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
- .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
- .build();
- mediaPlayer.setAudioAttributes(audioAttributes);
-
- mediaPlayer.setOnPreparedListener(mp -> mediaPlayer.start());
-
- mediaPlayer.prepareAsync();
- } catch (IOException e) {
- Log.e(TAG, "Failed to set data source");
- }
- }
- }
-
- @RequiresApi(api = Build.VERSION_CODES.O)
- @Override
- public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
- super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
- isInPipMode = isInPictureInPictureMode;
- if (isInPictureInPictureMode) {
- updateUiForPipMode();
- } else {
- updateUiForNormalMode();
- }
- }
-
- public void updateUiForPipMode() {
- binding.callAnswerButtons.setVisibility(View.INVISIBLE);
- binding.incomingCallRelativeLayout.setVisibility(View.INVISIBLE);
- }
-
- public void updateUiForNormalMode() {
- binding.callAnswerButtons.setVisibility(View.VISIBLE);
- binding.incomingCallRelativeLayout.setVisibility(View.VISIBLE);
- }
-
- @Override
- void suppressFitsSystemWindows() {
- binding.controllerCallNotificationLayout.setFitsSystemWindows(false);
- }
-} \ No newline at end of file
diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.kt
new file mode 100644
index 000000000..3d30e3a0c
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.kt
@@ -0,0 +1,476 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.activities
+
+import android.annotation.SuppressLint
+import android.app.Notification
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.content.res.Configuration
+import android.graphics.Bitmap
+import android.graphics.drawable.BitmapDrawable
+import android.media.AudioAttributes
+import android.media.MediaPlayer
+import android.os.Build
+import android.os.Bundle
+import android.os.Handler
+import android.os.SystemClock
+import android.util.Log
+import android.view.View
+import androidx.annotation.RequiresApi
+import androidx.core.app.NotificationCompat
+import autodagger.AutoInjector
+import com.facebook.common.executors.UiThreadImmediateExecutorService
+import com.facebook.common.references.CloseableReference
+import com.facebook.datasource.DataSource
+import com.facebook.drawee.backends.pipeline.Fresco
+import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber
+import com.facebook.imagepipeline.image.CloseableImage
+import com.nextcloud.talk.R
+import com.nextcloud.talk.api.NcApi
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
+import com.nextcloud.talk.data.user.model.User
+import com.nextcloud.talk.databinding.CallNotificationActivityBinding
+import com.nextcloud.talk.models.json.conversations.Conversation
+import com.nextcloud.talk.models.json.conversations.RoomOverall
+import com.nextcloud.talk.models.json.participants.Participant
+import com.nextcloud.talk.models.json.participants.ParticipantsOverall
+import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.DisplayUtils
+import com.nextcloud.talk.utils.DoNotDisturbUtils.shouldPlaySound
+import com.nextcloud.talk.utils.NotificationUtils
+import com.nextcloud.talk.utils.NotificationUtils.getCallRingtoneUri
+import com.nextcloud.talk.utils.ParticipantPermissions
+import com.nextcloud.talk.utils.bundle.BundleKeys
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CALL_VOICE_ONLY
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_NAME
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY
+import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.hasSpreedFeatureCapability
+import io.reactivex.Observable
+import io.reactivex.Observer
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.Disposable
+import io.reactivex.schedulers.Schedulers
+import okhttp3.Cache
+import org.parceler.Parcels
+import java.io.IOException
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+
+@SuppressLint("LongLogTag")
+@AutoInjector(NextcloudTalkApplication::class)
+class CallNotificationActivity : CallBaseActivity() {
+ @JvmField
+ @Inject
+ var ncApi: NcApi? = null
+
+ @JvmField
+ @Inject
+ var cache: Cache? = null
+
+ private val disposablesList: MutableList<Disposable> = ArrayList()
+ private var originalBundle: Bundle? = null
+ private var roomToken: String? = null
+ private var userBeingCalled: User? = null
+ private var credentials: String? = null
+ private var currentConversation: Conversation? = null
+ private var mediaPlayer: MediaPlayer? = null
+ private var leavingScreen = false
+ private var handler: Handler? = null
+ private var binding: CallNotificationActivityBinding? = null
+ override fun onCreate(savedInstanceState: Bundle?) {
+ Log.d(TAG, "onCreate")
+ super.onCreate(savedInstanceState)
+ sharedApplication!!.componentApplication.inject(this)
+ binding = CallNotificationActivityBinding.inflate(layoutInflater)
+ setContentView(binding!!.root)
+ hideNavigationIfNoPipAvailable()
+ val extras = intent.extras
+ roomToken = extras!!.getString(KEY_ROOM_TOKEN, "")
+ currentConversation = Parcels.unwrap(extras.getParcelable(KEY_ROOM))
+ userBeingCalled = extras.getParcelable(KEY_USER_ENTITY)
+ originalBundle = extras
+ credentials = ApiUtils.getCredentials(userBeingCalled!!.username, userBeingCalled!!.token)
+ setCallDescriptionText()
+ if (currentConversation == null) {
+ handleFromNotification()
+ } else {
+ setUpAfterConversationIsKnown()
+ }
+ if (shouldPlaySound()) {
+ playRingtoneSound()
+ }
+ initClickListeners()
+ }
+
+ override fun onStart() {
+ super.onStart()
+ if (handler == null) {
+ handler = Handler()
+ try {
+ cache!!.evictAll()
+ } catch (e: IOException) {
+ Log.e(TAG, "Failed to evict cache")
+ }
+ }
+ }
+
+ private fun initClickListeners() {
+ binding!!.callAnswerVoiceOnlyView.setOnClickListener {
+ Log.d(TAG, "accept call (voice only)")
+ originalBundle!!.putBoolean(KEY_CALL_VOICE_ONLY, true)
+ proceedToCall()
+ }
+ binding!!.callAnswerCameraView.setOnClickListener {
+ Log.d(TAG, "accept call (with video)")
+ originalBundle!!.putBoolean(KEY_CALL_VOICE_ONLY, false)
+ proceedToCall()
+ }
+ binding!!.hangupButton.setOnClickListener { hangup() }
+ }
+
+ private fun setCallDescriptionText() {
+ val callDescriptionWithoutTypeInfo = String.format(
+ resources.getString(R.string.nc_call_unknown),
+ resources.getString(R.string.nc_app_product_name)
+ )
+ binding!!.incomingCallVoiceOrVideoTextView.text = callDescriptionWithoutTypeInfo
+ }
+
+ private fun showAnswerControls() {
+ binding!!.callAnswerCameraView.visibility = View.VISIBLE
+ binding!!.callAnswerVoiceOnlyView.visibility = View.VISIBLE
+ }
+
+ private fun hangup() {
+ leavingScreen = true
+ dispose()
+ endMediaNotifications()
+ finish()
+ }
+
+ private fun proceedToCall() {
+ originalBundle!!.putString(KEY_ROOM_TOKEN, currentConversation!!.token)
+ originalBundle!!.putString(KEY_CONVERSATION_NAME, currentConversation!!.displayName)
+
+ val participantPermission = ParticipantPermissions(
+ userBeingCalled!!,
+ currentConversation!!
+ )
+ originalBundle!!.putBoolean(
+ BundleKeys.KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_AUDIO,
+ participantPermission.canPublishAudio()
+ )
+ originalBundle!!.putBoolean(
+ BundleKeys.KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_VIDEO,
+ participantPermission.canPublishVideo()
+ )
+
+ val intent = Intent(this, CallActivity::class.java)
+ intent.putExtras(originalBundle!!)
+ startActivity(intent)
+ }
+
+ private fun checkIfAnyParticipantsRemainInRoom() {
+ val apiVersion = ApiUtils.getCallApiVersion(userBeingCalled, intArrayOf(ApiUtils.APIv4, 1))
+ ncApi!!.getPeersForCall(
+ credentials,
+ ApiUtils.getUrlForCall(
+ apiVersion,
+ userBeingCalled!!.baseUrl,
+ currentConversation!!.token
+ )
+ )
+ .subscribeOn(Schedulers.io())
+ .repeatWhen { completed: Observable<Any?> ->
+ completed.zipWith(Observable.range(TIMER_START, TIMER_COUNT)) { _: Any?, i: Int? -> i!! }
+ .flatMap { Observable.timer(TIMER_DELAY, TimeUnit.SECONDS) }
+ .takeWhile { !leavingScreen }
+ }
+ .subscribe(object : Observer<ParticipantsOverall> {
+ override fun onSubscribe(d: Disposable) {
+ disposablesList.add(d)
+ }
+
+ override fun onNext(participantsOverall: ParticipantsOverall) {
+ val hasParticipantsInCall: Boolean
+ var inCallOnDifferentDevice = false
+ val participantList = participantsOverall.ocs!!.data
+ hasParticipantsInCall = participantList!!.isNotEmpty()
+ if (hasParticipantsInCall) {
+ for (participant in participantList) {
+ if (participant.calculatedActorType === Participant.ActorType.USERS &&
+ participant.calculatedActorId == userBeingCalled!!.userId
+ ) {
+ inCallOnDifferentDevice = true
+ break
+ }
+ }
+ }
+ if (inCallOnDifferentDevice) {
+ runOnUiThread { hangup() }
+ }
+ if (!hasParticipantsInCall) {
+ showMissedCallNotification()
+ runOnUiThread { hangup() }
+ }
+ }
+
+ override fun onError(e: Throwable) {
+ Log.e(TAG, "error while getPeersForCall", e)
+ }
+
+ override fun onComplete() {
+ showMissedCallNotification()
+ runOnUiThread { hangup() }
+ }
+ })
+ }
+
+ private fun showMissedCallNotification() {
+ val mNotifyManager: NotificationManager?
+ val mBuilder: NotificationCompat.Builder?
+
+ mNotifyManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ mBuilder = NotificationCompat.Builder(
+ context,
+ NotificationUtils.NotificationChannels
+ .NOTIFICATION_CHANNEL_MESSAGES_V4.name
+ )
+
+ val notification: Notification = mBuilder
+ .setContentTitle(
+ String.format(resources.getString(R.string.nc_missed_call), currentConversation!!.displayName)
+ )
+ .setSmallIcon(R.drawable.ic_baseline_phone_missed_24)
+ .setOngoing(false)
+ .setAutoCancel(true)
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .setContentIntent(getIntentToOpenConversation())
+ .build()
+
+ val notificationId: Int = SystemClock.uptimeMillis().toInt()
+ mNotifyManager.notify(notificationId, notification)
+ }
+
+ private fun getIntentToOpenConversation(): PendingIntent? {
+ val bundle = Bundle()
+ val intent = Intent(context, MainActivity::class.java)
+ intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
+
+ bundle.putString(KEY_ROOM_TOKEN, currentConversation?.token)
+ bundle.putParcelable(KEY_USER_ENTITY, userBeingCalled)
+ bundle.putBoolean(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL, false)
+
+ intent.putExtras(bundle)
+
+ val requestCode = System.currentTimeMillis().toInt()
+ val intentFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ PendingIntent.FLAG_MUTABLE
+ } else {
+ 0
+ }
+ return PendingIntent.getActivity(context, requestCode, intent, intentFlag)
+ }
+
+ @Suppress("MagicNumber")
+ private fun handleFromNotification() {
+ val apiVersion = ApiUtils.getConversationApiVersion(
+ userBeingCalled,
+ intArrayOf(
+ ApiUtils.APIv4,
+ ApiUtils.APIv3, 1
+ )
+ )
+ ncApi!!.getRoom(credentials, ApiUtils.getUrlForRoom(apiVersion, userBeingCalled!!.baseUrl, roomToken))
+ .subscribeOn(Schedulers.io())
+ .retry(GET_ROOM_RETRY_COUNT)
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(object : Observer<RoomOverall> {
+ override fun onSubscribe(d: Disposable) {
+ disposablesList.add(d)
+ }
+
+ override fun onNext(roomOverall: RoomOverall) {
+ currentConversation = roomOverall.ocs!!.data
+ setUpAfterConversationIsKnown()
+ if (apiVersion >= 3) {
+ val hasCallFlags = hasSpreedFeatureCapability(
+ userBeingCalled,
+ "conversation-call-flags"
+ )
+ if (hasCallFlags) {
+ if (isInCallWithVideo(currentConversation!!.callFlag)) {
+ binding!!.incomingCallVoiceOrVideoTextView.text = String.format(
+ resources.getString(R.string.nc_call_video),
+ resources.getString(R.string.nc_app_product_name)
+ )
+ } else {
+ binding!!.incomingCallVoiceOrVideoTextView.text = String.format(
+ resources.getString(R.string.nc_call_voice),
+ resources.getString(R.string.nc_app_product_name)
+ )
+ }
+ }
+ }
+ }
+
+ override fun onError(e: Throwable) {
+ Log.e(TAG, e.message, e)
+ }
+
+ override fun onComplete() {
+ // unused atm
+ }
+ })
+ }
+
+ private fun isInCallWithVideo(callFlag: Int): Boolean {
+ return callFlag >= Participant.InCallFlags.IN_CALL + Participant.InCallFlags.WITH_VIDEO
+ }
+
+ private fun setUpAfterConversationIsKnown() {
+ binding!!.conversationNameTextView.text = currentConversation!!.displayName
+ if (currentConversation!!.type === Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) {
+ setAvatarForOneToOneCall()
+ } else {
+ binding!!.avatarImageView.setImageResource(R.drawable.ic_circular_group)
+ }
+ checkIfAnyParticipantsRemainInRoom()
+ showAnswerControls()
+ }
+
+ @Suppress("MagicNumber")
+ private fun setAvatarForOneToOneCall() {
+ val imageRequest = DisplayUtils.getImageRequestForUrl(
+ ApiUtils.getUrlForAvatar(
+ userBeingCalled!!.baseUrl,
+ currentConversation!!.name,
+ true
+ )
+ )
+ val imagePipeline = Fresco.getImagePipeline()
+ val dataSource = imagePipeline.fetchDecodedImage(imageRequest, null)
+ dataSource.subscribe(
+ object : BaseBitmapDataSubscriber() {
+ override fun onNewResultImpl(bitmap: Bitmap?) {
+ binding!!.avatarImageView.hierarchy.setImage(
+ BitmapDrawable(resources, bitmap), 100f,
+ true
+ )
+ }
+
+ override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage?>>) {
+ Log.e(TAG, "failed to load avatar")
+ }
+ },
+ UiThreadImmediateExecutorService.getInstance()
+ )
+ }
+
+ private fun endMediaNotifications() {
+ if (mediaPlayer != null) {
+ if (mediaPlayer!!.isPlaying) {
+ mediaPlayer!!.stop()
+ }
+ mediaPlayer!!.release()
+ mediaPlayer = null
+ }
+ }
+
+ public override fun onDestroy() {
+ leavingScreen = true
+ if (handler != null) {
+ handler!!.removeCallbacksAndMessages(null)
+ handler = null
+ }
+ dispose()
+ endMediaNotifications()
+ super.onDestroy()
+ }
+
+ private fun dispose() {
+ for (disposable in disposablesList) {
+ if (!disposable.isDisposed) {
+ disposable.dispose()
+ }
+ }
+ }
+
+ private fun playRingtoneSound() {
+ val ringtoneUri = getCallRingtoneUri(applicationContext, appPreferences)
+ if (ringtoneUri != null) {
+ mediaPlayer = MediaPlayer()
+ try {
+ mediaPlayer!!.setDataSource(this, ringtoneUri)
+ mediaPlayer!!.isLooping = true
+ val audioAttributes = AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+ .build()
+ mediaPlayer!!.setAudioAttributes(audioAttributes)
+ mediaPlayer!!.setOnPreparedListener { mediaPlayer!!.start() }
+ mediaPlayer!!.prepareAsync()
+ } catch (e: IOException) {
+ Log.e(TAG, "Failed to set data source")
+ }
+ }
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
+ super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
+ isInPipMode = isInPictureInPictureMode
+ if (isInPictureInPictureMode) {
+ updateUiForPipMode()
+ } else {
+ updateUiForNormalMode()
+ }
+ }
+
+ public override fun updateUiForPipMode() {
+ binding!!.callAnswerButtons.visibility = View.INVISIBLE
+ binding!!.incomingCallRelativeLayout.visibility = View.INVISIBLE
+ }
+
+ public override fun updateUiForNormalMode() {
+ binding!!.callAnswerButtons.visibility = View.VISIBLE
+ binding!!.incomingCallRelativeLayout.visibility = View.VISIBLE
+ }
+
+ public override fun suppressFitsSystemWindows() {
+ binding!!.controllerCallNotificationLayout.fitsSystemWindows = false
+ }
+
+ companion object {
+ const val TAG = "CallNotificationActivity"
+ const val TIMER_START = 1
+ const val TIMER_COUNT = 12
+ const val TIMER_DELAY: Long = 5
+ const val GET_ROOM_RETRY_COUNT: Long = 3
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/events/CallNotificationClick.kt b/app/src/main/java/com/nextcloud/talk/events/CallNotificationClick.kt
deleted file mode 100644
index ad8c52fde..000000000
--- a/app/src/main/java/com/nextcloud/talk/events/CallNotificationClick.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Nextcloud Talk application
- *
- * @author Mario Danic
- * Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-package com.nextcloud.talk.events
-
-class CallNotificationClick
diff --git a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.java b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.java
deleted file mode 100644
index e946511de..000000000
--- a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.java
+++ /dev/null
@@ -1,695 +0,0 @@
-/*
- * Nextcloud Talk application
- *
- * @author Andy Scherzinger
- * @author Mario Danic
- * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
- * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-package com.nextcloud.talk.jobs;
-
-import android.app.Notification;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.media.AudioAttributes;
-import android.media.MediaPlayer;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.service.notification.StatusBarNotification;
-import android.text.TextUtils;
-import android.util.Base64;
-import android.util.Log;
-
-import com.bluelinelabs.logansquare.LoganSquare;
-import com.nextcloud.talk.R;
-import com.nextcloud.talk.activities.CallActivity;
-import com.nextcloud.talk.activities.MainActivity;
-import com.nextcloud.talk.api.NcApi;
-import com.nextcloud.talk.application.NextcloudTalkApplication;
-import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager;
-import com.nextcloud.talk.data.user.model.User;
-import com.nextcloud.talk.models.SignatureVerification;
-import com.nextcloud.talk.models.json.chat.ChatUtils;
-import com.nextcloud.talk.models.json.conversations.Conversation;
-import com.nextcloud.talk.models.json.conversations.RoomOverall;
-import com.nextcloud.talk.models.json.notifications.NotificationOverall;
-import com.nextcloud.talk.models.json.push.DecryptedPushMessage;
-import com.nextcloud.talk.models.json.push.NotificationUser;
-import com.nextcloud.talk.receivers.DirectReplyReceiver;
-import com.nextcloud.talk.receivers.MarkAsReadReceiver;
-import com.nextcloud.talk.utils.ApiUtils;
-import com.nextcloud.talk.utils.DoNotDisturbUtils;
-import com.nextcloud.talk.utils.NotificationUtils;
-import com.nextcloud.talk.utils.PushUtils;
-import com.nextcloud.talk.utils.UserIdUtils;
-import com.nextcloud.talk.utils.bundle.BundleKeys;
-import com.nextcloud.talk.utils.preferences.AppPreferences;
-import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder;
-
-import org.parceler.Parcels;
-
-import java.io.IOException;
-import java.net.CookieManager;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.security.PrivateKey;
-import java.util.HashMap;
-import java.util.Objects;
-import java.util.zip.CRC32;
-
-import javax.crypto.Cipher;
-import javax.crypto.NoSuchPaddingException;
-import javax.inject.Inject;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationCompat.MessagingStyle;
-import androidx.core.app.NotificationManagerCompat;
-import androidx.core.app.Person;
-import androidx.core.app.RemoteInput;
-import androidx.emoji.text.EmojiCompat;
-import androidx.work.Data;
-import androidx.work.Worker;
-import androidx.work.WorkerParameters;
-import autodagger.AutoInjector;
-import io.reactivex.Maybe;
-import io.reactivex.Observer;
-import io.reactivex.disposables.Disposable;
-import okhttp3.JavaNetCookieJar;
-import okhttp3.OkHttpClient;
-import retrofit2.Retrofit;
-
-@AutoInjector(NextcloudTalkApplication.class)
-public class NotificationWorker extends Worker {
- public static final String TAG = "NotificationWorker";
- private static final String CHAT = "chat";
- private static final String ROOM = "room";
-
- @Inject
- AppPreferences appPreferences;
-
- @Inject
- ArbitraryStorageManager arbitraryStorageManager;
-
- @Inject
- Retrofit retrofit;
-
- @Inject
- OkHttpClient okHttpClient;
-
- private NcApi ncApi;
-
- private DecryptedPushMessage decryptedPushMessage;
- private Context context;
- private SignatureVerification signatureVerification;
- private String conversationType = "one2one";
-
- private String credentials;
- private boolean muteCall = false;
- private boolean importantConversation = false;
-
- public NotificationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
- super(context, workerParams);
- }
-
- private void showNotificationForCallWithNoPing(Intent intent) {
- User user = signatureVerification.getUser();
-
- importantConversation = arbitraryStorageManager.getStorageSetting(
- UserIdUtils.INSTANCE.getIdForUser(user),
- "important_conversation",
- intent.getExtras().getString(BundleKeys.KEY_ROOM_TOKEN))
- .map(arbitraryStorage -> {
- if (arbitraryStorage != null && arbitraryStorage.getValue() != null) {
- return Boolean.parseBoolean(arbitraryStorage.getValue());
- } else {
- return importantConversation;
- }
- })
- .switchIfEmpty(Maybe.just(importantConversation))
- .blockingGet();
-
- Log.e(TAG, "showNotificationForCallWithNoPing: importantConversation: " + importantConversation);
-
- int apiVersion = ApiUtils.getConversationApiVersion(user, new int[] {ApiUtils.APIv4, 1});
-
- ncApi.getRoom(credentials, ApiUtils.getUrlForRoom(apiVersion, user.getBaseUrl(),
- intent.getExtras().getString(BundleKeys.KEY_ROOM_TOKEN)))
- .blockingSubscribe(new Observer<RoomOverall>() {
- @Override
- public void onSubscribe(Disposable d) {
- // unused atm
- }
-
- @Override
- public void onNext(RoomOverall roomOverall) {
- Conversation conversation = roomOverall.getOcs().getData();
-
- intent.putExtra(BundleKeys.KEY_ROOM, Parcels.wrap(conversation));
- if (conversation.getType().equals(Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) ||
- (!TextUtils.isEmpty(conversation.getObjectType()) && "share:password".equals
- (conversation.getObjectType()))) {
- context.startActivity(intent);
- } else {
- if (conversation.getType().equals(Conversation.ConversationType.ROOM_GROUP_CALL)) {
- conversationType = "group";
- } else {
- conversationType = "public";
- }
- if (decryptedPushMessage.getNotificationId() != Long.MIN_VALUE) {
- showNotificationWithObjectData(intent);
- } else {
- showNotification(intent);
- }
- }
-
- muteCall = conversation.getNotificationCalls() != 1;
- }
-
- @Override
- public void onError(Throwable e) {
- // unused atm
- }
-
- @Override
- public void onComplete() {
- // unused atm
- }
- });
- }
-
- private void showNotificationWithObjectData(Intent intent) {
- User user = signatureVerification.getUser();
- ncApi.getNotification(credentials, ApiUtils.getUrlForNotificationWithId(user.getBaseUrl(),
- Long.toString(decryptedPushMessage.getNotificationId())))
- .blockingSubscribe(new Observer<NotificationOverall>() {
- @Override
- public void onSubscribe(Disposable d) {
- // unused atm
- }
-
- @Override
- public void onNext(NotificationOverall notificationOverall) {
- com.nextcloud.talk.models.json.notifications.Notification notification =
- notificationOverall.getOcs().getNotification();
-
- if (notification.getMessageRichParameters() != null &&
- notification.getMessageRichParameters().size() > 0) {
- decryptedPushMessage.setText(ChatUtils.Companion.getParsedMessage(
- notification.getMessageRich(),
- notification.getMessageRichParameters()));
- } else {
- decryptedPushMessage.setText(notification.getMessage());
- }
-
- HashMap<String, HashMap<String, String>> subjectRichParameters = notification
- .getSubjectRichParameters();
-
- decryptedPushMessage.setTimestamp(notification.getDatetime().getMillis());
-
- if (subjectRichParameters != null && subjectRichParameters.size() > 0) {
- HashMap<String, String> callHashMap = subjectRichParameters.get("call");
- HashMap<String, String> userHashMap = subjectRichParameters.get("user");
- HashMap<String, String> guestHashMap = subjectRichParameters.get("guest");
-
- if (callHashMap != null && callHashMap.size() > 0 && callHashMap.containsKey("name")) {
- if (subjectRichParameters.containsKey("reaction")) {
- decryptedPushMessage.setSubject("");
- decryptedPushMessage.setText(notification.getSubject());
- } else if (Objects.equals(notification.getObjectType(), "chat")) {
- decryptedPushMessage.setSubject(Objects.requireNonNull(callHashMap.get("name")));
- } else {
- decryptedPushMessage.setSubject(Objects.requireNonNull(notification.getSubject()));
- }
-
- if (callHashMap.containsKey("call-type")) {
- conversationType = callHashMap.get("call-type");
- }
- }
-
- NotificationUser notificationUser = new NotificationUser();
- if (userHashMap != null && !userHashMap.isEmpty()) {
- notificationUser.setId(userHashMap.get("id"));
- notificationUser.setType(userHashMap.get("type"));
- notificationUser.setName(userHashMap.get("name"));
- decryptedPushMessage.setNotificationUser(notificationUser);
- } else if (guestHashMap != null && !guestHashMap.isEmpty()) {
- notificationUser.setId(guestHashMap.get("id"));
- notificationUser.setType(guestHashMap.get("type"));
- notificationUser.setName(guestHashMap.get("name"));
- decryptedPushMessage.setNotificationUser(notificationUser);
- }
- }
-
- decryptedPushMessage.setObjectId(notification.getObjectId());
-
- showNotification(intent);
- }
-
- @Override
- public void onError(Throwable e) {
- // unused atm
- }
-
- @Override
- public void onComplete() {
- // unused atm
- }
- });
- }
-
- private void showNotification(Intent intent) {
- int smallIcon;
- Bitmap largeIcon;
- String category;
- int priority = Notification.PRIORITY_HIGH;
-
- smallIcon = R.drawable.ic_logo;
-
- if (CHAT.equals(decryptedPushMessage.getType()) || ROOM.equals(decryptedPushMessage.getType())) {
- category = Notification.CATEGORY_MESSAGE;
- } else {
- category = Notification.CATEGORY_CALL;
- }
-
- switch (conversationType) {
- case "one2one":
- decryptedPushMessage.setSubject("");
- case "group":
- largeIcon = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_people_group_black_24px);
- break;
- case "public":
- largeIcon = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_link_black_24px);
- break;
- default:
- // assuming one2one
- if (CHAT.equals(decryptedPushMessage.getType()) || ROOM.equals(decryptedPushMessage.getType())) {
- largeIcon = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_comment);
- } else {
- largeIcon = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_call_black_24dp);
- }
- }
-
- // Use unique request code to make sure that a new PendingIntent gets created for each notification
- // See https://github.com/nextcloud/talk-android/issues/2111
- int requestCode = (int) System.currentTimeMillis();
- int intentFlag;
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- intentFlag = PendingIntent.FLAG_MUTABLE;
- } else {
- intentFlag = 0;
- }
- PendingIntent pendingIntent = PendingIntent.getActivity(context, requestCode, intent, intentFlag);
-
- Uri uri = Uri.parse(signatureVerification.getUser().getBaseUrl());
- String baseUrl = uri.getHost();
-
- NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context, "1")
- .setLargeIcon(largeIcon)
- .setSmallIcon(smallIcon)
- .setCategory(category)
- .setPriority(priority)
- .setSubText(baseUrl)
- .setWhen(decryptedPushMessage.getTimestamp())
- .setShowWhen(true)
- .setContentIntent(pendingIntent)
- .setAutoCancel(true);
-
- if (!TextUtils.isEmpty(decryptedPushMessage.getSubject())) {
- notificationBuilder.setContentTitle(EmojiCompat.get().process(decryptedPushMessage.getSubject()));
- }
-
- if (!TextUtils.isEmpty(decryptedPushMessage.getText())) {
- notificationBuilder.setContentText(EmojiCompat.get().process(decryptedPushMessage.getText()));
- }
-
- if (Build.VERSION.SDK_INT >= 23) {
- // This method should exist since API 21, but some phones don't have it
- // So as a safeguard, we don't use it until 23
-
- notificationBuilder.setColor(context.getResources().getColor(R.color.colorPrimary));
- }
-
- Bundle notificationInfo = new Bundle();
- notificationInfo.putLong(BundleKeys.KEY_INTERNAL_USER_ID,
- signatureVerification.getUser().getId());
- // could be an ID or a TOKEN
- notificationInfo.putString(BundleKeys.KEY_ROOM_TOKEN,
- decryptedPushMessage.getId());
- notificationInfo.putLong(BundleKeys.KEY_NOTIFICATION_ID,
- decryptedPushMessage.getNotificationId());
- notificationBuilder.setExtras(notificationInfo);
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- if (CHAT.equals(decryptedPushMessage.getType()) || ROOM.equals(decryptedPushMessage.getType())) {
- notificationBuilder.setChannelId(NotificationUtils.NotificationChannels.NOTIFICATION_CHANNEL_MESSAGES_V4.name());
- }
- } else {
- // red color for the lights
- notificationBuilder.setLights(0xFFFF0000, 200, 200);
- }
-
- notificationBuilder.setContentIntent(pendingIntent);
-
- String groupName = signatureVerification.getUser().getId() + "@" + decryptedPushMessage.getId();
- notificationBuilder.setGroup(Long.toString(calculateCRC32(groupName)));
-
- StatusBarNotification activeStatusBarNotification =
- NotificationUtils.INSTANCE.findNotificationForRoom(context,
- signatureVerification.getUser(),
- decryptedPushMessage.getId());
-
- // NOTE - systemNotificationId is an internal ID used on the device only.
- // It is NOT the same as the notification ID used in communication with the server.
- int systemNotificationId;
- if (activeStatusBarNotification != null) {
- systemNotificationId = activeStatusBarNotification.getId();
- } else {
- systemNotificationId = (int) calculateCRC32(String.valueOf(System.currentTimeMillis()));
- }
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
- CHAT.equals(decryptedPushMessage.getType()) &&
- decryptedPushMessage.getNotificationUser() != null) {
- prepareChatNotification(notificationBuilder, activeStatusBarNotification, systemNotificationId);
- }
-
- sendNotification(systemNotificationId, notificationBuilder.build());
- }
-
- private long calculateCRC32(String s) {
- CRC32 crc32 = new CRC32();
- crc32.update(s.getBytes());
- return crc32.getValue();
- }
-
- @RequiresApi(api = Build.VERSION_CODES.N)
- private void prepareChatNotification(NotificationCompat.Builder notificationBuilder,
- StatusBarNotification activeStatusBarNotification,
- int systemNotificationId) {
-
- final NotificationUser notificationUser = decryptedPushMessage.getNotificationUser();
- final String userType = notificationUser.getType();
-
- MessagingStyle style = null;
- if (activeStatusBarNotification != null) {
- style = MessagingStyle.extractMessagingStyleFromNotification(activeStatusBarNotification.getNotification());
- }
-
- Person.Builder person =
- new Person.Builder()
- .setKey(signatureVerification.getUser().getId() + "@" + notificationUser.getId())
- .setName(EmojiCompat.get().process(notificationUser.getName()))
- .setBot("bot".equals(userType));
-
- notificationBuilder.setOnlyAlertOnce(true);
- addReplyAction(notificationBuilder, systemNotificationId);
- addMarkAsReadAction(notificationBuilder, systemNotificationId);
-
- if ("user".equals(userType) || "guest".equals(userType)) {
- String baseUrl = signatureVerification.getUser().getBaseUrl();
- String avatarUrl = "user".equals(userType) ?
- ApiUtils.getUrlForAvatar(baseUrl, notificationUser.getId(), false) :
- ApiUtils.getUrlForGuestAvatar(baseUrl, notificationUser.getName(), false);
- person.setIcon(NotificationUtils.INSTANCE.loadAvatarSync(avatarUrl));
- }
-
- notificationBuilder.setStyle(getStyle(person.build(), style));
- }
-
- private PendingIntent buildIntentForAction(Class<?> cls, int systemNotificationId, int messageId) {
- Intent actualIntent = new Intent(context, cls);
-
- // NOTE - systemNotificationId is an internal ID used on the device only.
- // It is NOT the same as the notification ID used in communication with the server.
- actualIntent.putExtra(BundleKeys.KEY_SYSTEM_NOTIFICATION_ID, systemNotificationId);
- actualIntent.putExtra(BundleKeys.KEY_INTERNAL_USER_ID,
- Objects.requireNonNull(signatureVerification.getUser()).getId());
- actualIntent.putExtra(BundleKeys.KEY_ROOM_TOKEN, decryptedPushMessage.getId());
- actualIntent.putExtra(BundleKeys.KEY_MESSAGE_ID, messageId);
-
- int intentFlag;
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- intentFlag = PendingIntent.FLAG_MUTABLE|PendingIntent.FLAG_UPDATE_CURRENT;
- } else {
- intentFlag = PendingIntent.FLAG_UPDATE_CURRENT;
- }
-
- return PendingIntent.getBroadcast(context, systemNotificationId, actualIntent, intentFlag);
- }
-
- private void addMarkAsReadAction(NotificationCompat.Builder notificationBuilder, int systemNotificationId) {
- if (decryptedPushMessage.getObjectId() != null) {
- int messageId = 0;
- try {
- messageId = parseMessageId(decryptedPushMessage.getObjectId());
- } catch (NumberFormatException nfe) {
- Log.e(TAG, "Failed to parse messageId from objectId, skip adding mark-as-read action.", nfe);
- return;
- }
-
- // Build a PendingIntent for the mark as read action
- PendingIntent pendingIntent = buildIntentForAction(MarkAsReadReceiver.class,
- systemNotificationId,
- messageId);
-
- NotificationCompat.Action action =
- new NotificationCompat.Action.Builder(R.drawable.ic_eye,
- context.getResources().getString(R.string.nc_mark_as_read),
- pendingIntent)
- .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
- .setShowsUserInterface(false)
- .build();
-
- notificationBuilder.addAction(action);
- }
- }
-
- @RequiresApi(api = Build.VERSION_CODES.N)
- private void addReplyAction(NotificationCompat.Builder notificationBuilder, int systemNotificationId) {
- String replyLabel = context.getResources().getString(R.string.nc_reply);
-
- RemoteInput remoteInput = new RemoteInput.Builder(NotificationUtils.KEY_DIRECT_REPLY)
- .setLabel(replyLabel)
- .build();
-
- // Build a PendingIntent for the reply action
- PendingIntent replyPendingIntent = buildIntentForAction(DirectReplyReceiver.class, systemNotificationId, 0);
-
- NotificationCompat.Action replyAction =
- new NotificationCompat.Action.Builder(R.drawable.ic_reply, replyLabel, replyPendingIntent)
- .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
- .setShowsUserInterface(false)
- // Allows system to generate replies by context of conversation.
- // https://developer.android.com/reference/androidx/core/app/NotificationCompat.Action.Builder#setAllowGeneratedReplies(boolean)
- // Good question is - do we really want it?
- .setAllowGeneratedReplies(true)
- .addRemoteInput(remoteInput)
- .build();
-
- notificationBuilder.addAction(replyAction);
- }
-
- @RequiresApi(api = Build.VERSION_CODES.N)
- private MessagingStyle getStyle(Person person, @Nullable MessagingStyle style) {
- MessagingStyle newStyle = new MessagingStyle(person);
-
- newStyle.setConversationTitle(decryptedPushMessage.getSubject());
- newStyle.setGroupConversation(!"one2one".equals(conversationType));
-
- if (style != null) {
- style.getMessages().forEach(message -> newStyle.addMessage(
- new MessagingStyle.Message(message.getText(),
- message.getTimestamp(),
- message.getPerson())));
- }
-
- newStyle.addMessage(decryptedPushMessage.getText(), decryptedPushMessage.getTimestamp(), person);
- return newStyle;
- }
-
- private int parseMessageId(@NonNull String objectId) {
- String[] objectIdParts = objectId.split("/");
- if (objectIdParts.length < 2) {
- throw new NumberFormatException("Invalid objectId, doesn't contain at least one '/'");
- } else {
- return Integer.parseInt(objectIdParts[1]);
- }
- }
-
- private void sendNotification(int notificationId, Notification notification) {
- NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
- notificationManager.notify(notificationId, notification);
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- // On devices with Android 8.0 (Oreo) or later, notification sound will be handled by the system
- // if notifications have not been disabled by the user.
- return;
- }
-
- if (!Notification.CATEGORY_CALL.equals(notification.category) || !muteCall) {
- Uri soundUri = NotificationUtils.INSTANCE.getMessageRingtoneUri(context, appPreferences);
- if (soundUri != null && !ApplicationWideCurrentRoomHolder.getInstance().isInCall() &&
- (DoNotDisturbUtils.INSTANCE.shouldPlaySound() || importantConversation)) {
- AudioAttributes.Builder audioAttributesBuilder = new AudioAttributes.Builder().setContentType
- (AudioAttributes.CONTENT_TYPE_SONIFICATION);
-
- if (CHAT.equals(decryptedPushMessage.getType()) || ROOM.equals(decryptedPushMessage.getType())) {
- audioAttributesBuilder.setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT);
- } else {
- audioAttributesBuilder.setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST);
- }
-
- MediaPlayer mediaPlayer = new MediaPlayer();
- try {
- mediaPlayer.setDataSource(context, soundUri);
- mediaPlayer.setAudioAttributes(audioAttributesBuilder.build());
-
- mediaPlayer.setOnPreparedListener(mp -> mediaPlayer.start());
- mediaPlayer.setOnCompletionListener(MediaPlayer::release);
-
- mediaPlayer.prepareAsync();
- } catch (IOException e) {
- Log.e(TAG, "Failed to set data source");
- }
- }
- }
- }
-
- @NonNull
- @Override
- public Result doWork() {
- NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
-
- context = getApplicationContext();
- Data data = getInputData();
- String subject = data.getString(BundleKeys.KEY_NOTIFICATION_SUBJECT);
- String signature = data.getString(BundleKeys.KEY_NOTIFICATION_SIGNATURE);
-
- try {
- byte[] base64DecodedSubject = Base64.decode(subject, Base64.DEFAULT);
- byte[] base64DecodedSignature = Base64.decode(signature, Base64.DEFAULT);
- PushUtils pushUtils = new PushUtils();
- PrivateKey privateKey = (PrivateKey) pushUtils.readKeyFromFile(false);
-
- try {
- signatureVerification = pushUtils.verifySignature(base64DecodedSignature,
- base64DecodedSubject);
-
- if (signatureVerification.getSignatureValid()) {
- Cipher cipher = Cipher.getInstance("RSA/None/PKCS1Padding");
- cipher.init(Cipher.DECRYPT_MODE, privateKey);
- byte[] decryptedSubject = cipher.doFinal(base64DecodedSubject);
- decryptedPushMessage = LoganSquare.parse(new String(decryptedSubject),
- DecryptedPushMessage.class);
-
- decryptedPushMessage.setTimestamp(System.currentTimeMillis());
- if (decryptedPushMessage.getDelete()) {
- NotificationUtils.INSTANCE.cancelExistingNotificationWithId(
- context,
- signatureVerification.getUser(),
- decryptedPushMessage.getNotificationId());
- } else if (decryptedPushMessage.getDeleteAll()) {
- NotificationUtils.INSTANCE.cancelAllNotificationsForAccount(
- context,
- signatureVerification.getUser());
- } else if (decryptedPushMessage.getDeleteMultiple()) {
- for (long notificationId : decryptedPushMessage.getNotificationIds()) {
- NotificationUtils.INSTANCE.cancelExistingNotificationWithId(
- context,
- signatureVerification.getUser(),
- notificationId);
- }
- } else {
- credentials = ApiUtils.getCredentials(signatureVerification.getUser().getUsername(),
- signatureVerification.getUser().getToken());
-
- ncApi = retrofit.newBuilder().client(okHttpClient.newBuilder().cookieJar(new
- JavaNetCookieJar(new CookieManager())).build()).build().create(NcApi.class);
-
- boolean shouldShowNotification = "spreed".equals(decryptedPushMessage.getApp());
-
- if (shouldShowNotification) {
- Intent intent;
- Bundle bundle = new Bundle();
-
-
- boolean startACall = "call".equals(decryptedPushMessage.getType());
- if (startACall) {
- intent = new Intent(context, CallActivity.class);
- } else {
- intent = new Intent(context, MainActivity.class);
- }
-
- intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
-
- bundle.putString(BundleKeys.KEY_ROOM_TOKEN, decryptedPushMessage.getId());
-
- bundle.putParcelable(BundleKeys.KEY_USER_ENTITY,
- signatureVerification.getUser());
-
- bundle.putBoolean(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL,
- startACall);
-
- intent.putExtras(bundle);
-
- Log.e(TAG, "Notification: " + decryptedPushMessage.getType());
-
- switch (decryptedPushMessage.getType()) {
- case "call":
- if (bundle.containsKey(BundleKeys.KEY_ROOM_TOKEN)) {
- showNotificationForCallWithNoPing(intent);
- }
- break;
- case "room":
- if (bundle.containsKey(BundleKeys.KEY_ROOM_TOKEN)) {
- showNotificationWithObjectData(intent);
- }
- break;
- case "chat":
- if (decryptedPushMessage.getNotificationId() != Long.MIN_VALUE) {
- showNotificationWithObjectData(intent);
- } else {
- showNotification(intent);
- }
- break;
- default:
- break;
- }
-
- }
- }
- }
- } catch (NoSuchAlgorithmException e1) {
- Log.d(TAG, "No proper algorithm to decrypt the message " + e1.getLocalizedMessage());
- } catch (NoSuchPaddingException e1) {
- Log.d(TAG, "No proper padding to decrypt the message " + e1.getLocalizedMessage());
- } catch (InvalidKeyException e1) {
- Log.d(TAG, "Invalid private key " + e1.getLocalizedMessage());
- }
- } catch (Exception exception) {
- Log.d(TAG, "Something went very wrong " + exception.getLocalizedMessage());
- }
- return Result.success();
- }
-}
diff --git a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt
new file mode 100644
index 000000000..2d7dc11b7
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt
@@ -0,0 +1,849 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Andy Scherzinger
+ * @author Mario Danic
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
+ * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
+ * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.jobs
+
+import android.app.Notification
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Context.NOTIFICATION_SERVICE
+import android.content.Intent
+import android.graphics.Bitmap
+import android.media.AudioAttributes
+import android.media.MediaPlayer
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.os.SystemClock
+import android.service.notification.StatusBarNotification
+import android.text.TextUtils
+import android.util.Base64
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.app.Person
+import androidx.core.app.RemoteInput
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.drawable.toBitmap
+import androidx.emoji.text.EmojiCompat
+import androidx.work.Data
+import androidx.work.Worker
+import androidx.work.WorkerParameters
+import autodagger.AutoInjector
+import com.bluelinelabs.logansquare.LoganSquare
+import com.nextcloud.talk.R
+import com.nextcloud.talk.activities.CallNotificationActivity
+import com.nextcloud.talk.activities.MainActivity
+import com.nextcloud.talk.api.NcApi
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
+import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager
+import com.nextcloud.talk.models.SignatureVerification
+import com.nextcloud.talk.models.json.chat.ChatUtils.Companion.getParsedMessage
+import com.nextcloud.talk.models.json.conversations.RoomOverall
+import com.nextcloud.talk.models.json.notifications.NotificationOverall
+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.models.json.push.NotificationUser
+import com.nextcloud.talk.receivers.DirectReplyReceiver
+import com.nextcloud.talk.receivers.MarkAsReadReceiver
+import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.DoNotDisturbUtils.shouldPlaySound
+import com.nextcloud.talk.utils.NotificationUtils
+import com.nextcloud.talk.utils.NotificationUtils.cancelAllNotificationsForAccount
+import com.nextcloud.talk.utils.NotificationUtils.cancelNotification
+import com.nextcloud.talk.utils.NotificationUtils.findNotificationForRoom
+import com.nextcloud.talk.utils.NotificationUtils.getCallRingtoneUri
+import com.nextcloud.talk.utils.NotificationUtils.getMessageRingtoneUri
+import com.nextcloud.talk.utils.NotificationUtils.loadAvatarSync
+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_INTERNAL_USER_ID
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_MESSAGE_ID
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_NOTIFICATION_ID
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SYSTEM_NOTIFICATION_ID
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY
+import com.nextcloud.talk.utils.preferences.AppPreferences
+import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder
+import io.reactivex.Observable
+import io.reactivex.Observer
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.Disposable
+import io.reactivex.schedulers.Schedulers
+import okhttp3.JavaNetCookieJar
+import okhttp3.OkHttpClient
+import retrofit2.Retrofit
+import java.io.IOException
+import java.net.CookieManager
+import java.security.InvalidKeyException
+import java.security.NoSuchAlgorithmException
+import java.security.PrivateKey
+import java.util.concurrent.TimeUnit
+import java.util.function.Consumer
+import java.util.zip.CRC32
+import javax.crypto.Cipher
+import javax.crypto.NoSuchPaddingException
+import javax.inject.Inject
+
+@AutoInjector(NextcloudTalkApplication::class)
+class NotificationWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
+
+ @Inject
+ lateinit var appPreferences: AppPreferences
+
+ @JvmField
+ @Inject
+ var arbitraryStorageManager: ArbitraryStorageManager? = null
+
+ @JvmField
+ @Inject
+ var retrofit: Retrofit? = null
+
+ @JvmField
+ @Inject
+ var okHttpClient: OkHttpClient? = null
+ private lateinit var credentials: String
+ private lateinit var ncApi: NcApi
+ private lateinit var pushMessage: DecryptedPushMessage
+ private lateinit var signatureVerification: SignatureVerification
+ private var context: Context? = null
+ private var conversationType: String? = "one2one"
+ private var muteCall = false
+ private var importantConversation = false
+ private lateinit var notificationManager: NotificationManagerCompat
+
+ override fun doWork(): Result {
+ sharedApplication!!.componentApplication.inject(this)
+ context = applicationContext
+
+ initDecryptedData(inputData)
+ initNcApiAndCredentials()
+
+ notificationManager = NotificationManagerCompat.from(context!!)
+
+ pushMessage.timestamp = System.currentTimeMillis()
+
+ Log.d(TAG, pushMessage.toString())
+ Log.d(TAG, "pushMessage.id (=KEY_ROOM_TOKEN): " + pushMessage.id)
+ Log.d(TAG, "pushMessage.notificationId: " + pushMessage.notificationId)
+ Log.d(TAG, "pushMessage.notificationIds: " + pushMessage.notificationIds)
+ Log.d(TAG, "pushMessage.timestamp: " + pushMessage.timestamp)
+
+ if (pushMessage.delete) {
+ cancelNotification(context, signatureVerification.user!!, pushMessage.notificationId)
+ } else if (pushMessage.deleteAll) {
+ cancelAllNotificationsForAccount(context, signatureVerification.user!!)
+ } else if (pushMessage.deleteMultiple) {
+ for (notificationId in pushMessage.notificationIds!!) {
+ cancelNotification(context, signatureVerification.user!!, notificationId)
+ }
+ } else if (isSpreedNotification()) {
+ Log.d(TAG, "pushMessage.type: " + pushMessage.type)
+ when (pushMessage.type) {
+ "chat" -> handleChatNotification()
+ "room" -> handleRoomNotification()
+ "call" -> handleCallNotification()
+ else -> Log.e(TAG, "unknown pushMessage.type")
+ }
+ } else {
+ Log.d(TAG, "a pushMessage that is not for spreed was received.")
+ }
+
+ return Result.success()
+ }
+
+ private fun handleChatNotification() {
+ val chatIntent = Intent(context, MainActivity::class.java)
+ chatIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
+ val chatBundle = Bundle()
+ chatBundle.putString(KEY_ROOM_TOKEN, pushMessage.id)
+ chatBundle.putParcelable(KEY_USER_ENTITY, signatureVerification.user)
+ chatBundle.putBoolean(KEY_FROM_NOTIFICATION_START_CALL, false)
+ chatIntent.putExtras(chatBundle)
+ if (pushMessage.notificationId != Long.MIN_VALUE) {
+ showNotificationWithObjectData(chatIntent)
+ } else {
+ showNotification(chatIntent)
+ }
+ }
+
+ /**
+ * handle messages with type 'room', e.g. "xxx invited you to a group conversation"
+ */
+ private fun handleRoomNotification() {
+ val intent = Intent(context, MainActivity::class.java)
+ intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
+ val bundle = Bundle()
+ bundle.putString(KEY_ROOM_TOKEN, pushMessage.id)
+ bundle.putParcelable(KEY_USER_ENTITY, signatureVerification.user)
+ bundle.putBoolean(KEY_FROM_NOTIFICATION_START_CALL, false)
+ intent.putExtras(bundle)
+ if (bundle.containsKey(KEY_ROOM_TOKEN)) {
+ showNotificationWithObjectData(intent)
+ }
+ }
+
+ private fun handleCallNotification() {
+ val fullScreenIntent = Intent(context, CallNotificationActivity::class.java)
+ val bundle = Bundle()
+ bundle.putString(KEY_ROOM_TOKEN, pushMessage.id)
+ bundle.putParcelable(KEY_USER_ENTITY, signatureVerification.user)
+ 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 requestCode = System.currentTimeMillis().toInt()
+
+ val fullScreenPendingIntent = PendingIntent.getActivity(
+ context,
+ requestCode,
+ 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
+ .NotificationChannels.NOTIFICATION_CHANNEL_CALLS_V4.name
+ val uri = Uri.parse(signatureVerification.user!!.baseUrl)
+ val baseUrl = uri.host
+
+ val notification =
+ NotificationCompat.Builder(applicationContext, notificationChannelId)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setCategory(NotificationCompat.CATEGORY_CALL)
+ .setSmallIcon(R.drawable.ic_call_black_24dp)
+ .setSubText(baseUrl)
+ .setShowWhen(true)
+ .setWhen(pushMessage.timestamp)
+ .setContentTitle(EmojiCompat.get().process(pushMessage.subject))
+ .setAutoCancel(true)
+ .setOngoing(true)
+ .setContentIntent(fullScreenPendingIntent)
+ .setFullScreenIntent(fullScreenPendingIntent, true)
+ .setSound(soundUri)
+ .build()
+ notification.flags = notification.flags or Notification.FLAG_INSISTENT
+
+ sendNotification(pushMessage.timestamp.toInt(), notification)
+
+ checkIfCallIsActive(signatureVerification, pushMessage)
+ }
+
+ private fun initNcApiAndCredentials() {
+ credentials = ApiUtils.getCredentials(
+ signatureVerification.user!!.username,
+ signatureVerification.user!!.token
+ )
+ ncApi = retrofit!!.newBuilder().client(
+ okHttpClient!!.newBuilder().cookieJar(
+ JavaNetCookieJar(
+ CookieManager()
+ )
+ ).build()
+ ).build().create(
+ NcApi::class.java
+ )
+ }
+
+ @Suppress("TooGenericExceptionCaught", "NestedBlockDepth", "ComplexMethod", "LongMethod")
+ private fun initDecryptedData(inputData: Data) {
+ val subject = inputData.getString(BundleKeys.KEY_NOTIFICATION_SUBJECT)
+ val signature = inputData.getString(BundleKeys.KEY_NOTIFICATION_SIGNATURE)
+ 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) {
+ val cipher = Cipher.getInstance("RSA/None/PKCS1Padding")
+ cipher.init(Cipher.DECRYPT_MODE, privateKey)
+ val decryptedSubject = cipher.doFinal(base64DecodedSubject)
+
+ pushMessage = LoganSquare.parse(
+ String(decryptedSubject),
+ DecryptedPushMessage::class.java
+ )
+ }
+ } catch (e: NoSuchAlgorithmException) {
+ Log.e(TAG, "No proper algorithm to decrypt the message ", e)
+ } catch (e: NoSuchPaddingException) {
+ Log.e(TAG, "No proper padding to decrypt the message ", e)
+ } catch (e: InvalidKeyException) {
+ Log.e(TAG, "Invalid private key ", e)
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Error occurred while initializing decoded data ", e)
+ }
+ }
+
+ private fun isSpreedNotification() = SPREED_APP == pushMessage.app
+
+ private fun showNotificationWithObjectData(intent: Intent) {
+ val user = signatureVerification.user
+
+ // see https://github.com/nextcloud/notifications/blob/master/docs/ocs-endpoint-v2.md
+ ncApi.getNotification(
+ credentials,
+ ApiUtils.getUrlForNotificationWithId(
+ user!!.baseUrl,
+ (pushMessage.notificationId!!).toString()
+ )
+ )
+ .blockingSubscribe(object : Observer<NotificationOverall> {
+ override fun onSubscribe(d: Disposable) {
+ // unused atm
+ }
+
+ override fun onNext(notificationOverall: NotificationOverall) {
+ val ncNotification = notificationOverall.ocs!!.notification
+
+ if (ncNotification!!.messageRichParameters != null &&
+ ncNotification.messageRichParameters!!.size > 0
+ ) {
+ pushMessage.text = getParsedMessage(
+ ncNotification.messageRich,
+ ncNotification.messageRichParameters
+ )
+ } else {
+ pushMessage.text = ncNotification.message
+ }
+
+ val subjectRichParameters = ncNotification.subjectRichParameters
+
+ pushMessage.timestamp = ncNotification.datetime!!.millis
+
+ if (subjectRichParameters != null && subjectRichParameters.size > 0) {
+ val callHashMap = subjectRichParameters["call"]
+ val userHashMap = subjectRichParameters["user"]
+ val guestHashMap = subjectRichParameters["guest"]
+ if (callHashMap != null && callHashMap.size > 0 && callHashMap.containsKey("name")) {
+ if (subjectRichParameters.containsKey("reaction")) {
+ pushMessage.subject = ""
+ pushMessage.text = ncNotification.subject
+ } else if (ncNotification.objectType == "chat") {
+ pushMessage.subject = callHashMap["name"]!!
+ } else {
+ pushMessage.subject = ncNotification.subject!!
+ }
+ if (callHashMap.containsKey("call-type")) {
+ conversationType = callHashMap["call-type"]
+ }
+ }
+ val notificationUser = NotificationUser()
+ if (userHashMap != null && userHashMap.isNotEmpty()) {
+ notificationUser.id = userHashMap["id"]
+ notificationUser.type = userHashMap["type"]
+ notificationUser.name = userHashMap["name"]
+ pushMessage.notificationUser = notificationUser
+ } else if (guestHashMap != null && guestHashMap.isNotEmpty()) {
+ notificationUser.id = guestHashMap["id"]
+ notificationUser.type = guestHashMap["type"]
+ notificationUser.name = guestHashMap["name"]
+ pushMessage.notificationUser = notificationUser
+ }
+ }
+ pushMessage.objectId = ncNotification.objectId
+ showNotification(intent)
+ }
+
+ override fun onError(e: Throwable) {
+ // unused atm
+ }
+
+ override fun onComplete() {
+ // unused atm
+ }
+ })
+ }
+
+ @Suppress("MagicNumber")
+ private fun showNotification(intent: Intent) {
+ val largeIcon: Bitmap
+ val priority = NotificationCompat.PRIORITY_HIGH
+ val smallIcon: Int = R.drawable.ic_logo
+ val category: String = if (CHAT == pushMessage.type || ROOM == pushMessage.type) {
+ Notification.CATEGORY_MESSAGE
+ } else {
+ Notification.CATEGORY_CALL
+ }
+ when (conversationType) {
+ "one2one" -> {
+ pushMessage.subject = ""
+ largeIcon = ContextCompat.getDrawable(context!!, R.drawable.ic_people_group_black_24px)?.toBitmap()!!
+ }
+ "group" ->
+ largeIcon = ContextCompat.getDrawable(context!!, R.drawable.ic_people_group_black_24px)?.toBitmap()!!
+ "public" -> largeIcon = ContextCompat.getDrawable(context!!, R.drawable.ic_link_black_24px)?.toBitmap()!!
+ else -> // assuming one2one
+ largeIcon = if (CHAT == pushMessage.type || ROOM == pushMessage.type) {
+ ContextCompat.getDrawable(context!!, R.drawable.ic_comment)?.toBitmap()!!
+ } else {
+ ContextCompat.getDrawable(context!!, R.drawable.ic_call_black_24dp)?.toBitmap()!!
+ }
+ }
+
+ // Use unique request code to make sure that a new PendingIntent gets created for each notification
+ // See https://github.com/nextcloud/talk-android/issues/2111
+ val requestCode = System.currentTimeMillis().toInt()
+ val intentFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ PendingIntent.FLAG_MUTABLE
+ } else {
+ 0
+ }
+ val pendingIntent = PendingIntent.getActivity(context, requestCode, intent, intentFlag)
+ val uri = Uri.parse(signatureVerification.user!!.baseUrl)
+ val baseUrl = uri.host
+ val notificationBuilder = NotificationCompat.Builder(context!!, "1")
+ .setLargeIcon(largeIcon)
+ .setSmallIcon(smallIcon)
+ .setCategory(category)
+ .setPriority(priority)
+ .setSubText(baseUrl)
+ .setWhen(pushMessage.timestamp)
+ .setShowWhen(true)
+ .setContentIntent(pendingIntent)
+ .setAutoCancel(true)
+ if (!TextUtils.isEmpty(pushMessage.subject)) {
+ notificationBuilder.setContentTitle(
+ EmojiCompat.get().process(pushMessage.subject)
+ )
+ }
+ if (!TextUtils.isEmpty(pushMessage.text)) {
+ notificationBuilder.setContentText(
+ EmojiCompat.get().process(pushMessage.text!!)
+ )
+ }
+ if (Build.VERSION.SDK_INT >= 23) {
+ // This method should exist since API 21, but some phones don't have it
+ // So as a safeguard, we don't use it until 23
+ notificationBuilder.color = context!!.resources.getColor(R.color.colorPrimary)
+ }
+ val notificationInfoBundle = Bundle()
+ notificationInfoBundle.putLong(KEY_INTERNAL_USER_ID, signatureVerification.user!!.id!!)
+ // could be an ID or a TOKEN
+ notificationInfoBundle.putString(KEY_ROOM_TOKEN, pushMessage.id)
+ notificationInfoBundle.putLong(KEY_NOTIFICATION_ID, pushMessage.notificationId!!)
+ notificationBuilder.setExtras(notificationInfoBundle)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ if (CHAT == pushMessage.type || ROOM == pushMessage.type) {
+ notificationBuilder.setChannelId(
+ NotificationUtils.NotificationChannels.NOTIFICATION_CHANNEL_MESSAGES_V4.name
+ )
+ }
+ } else {
+ // red color for the lights
+ notificationBuilder.setLights(-0x10000, 200, 200)
+ }
+
+ notificationBuilder.setContentIntent(pendingIntent)
+ val groupName = signatureVerification.user!!.id.toString() + "@" + pushMessage.id
+ notificationBuilder.setGroup(calculateCRC32(groupName).toString())
+ val activeStatusBarNotification = findNotificationForRoom(
+ context,
+ signatureVerification.user!!,
+ pushMessage.id!!
+ )
+
+ // NOTE - systemNotificationId is an internal ID used on the device only.
+ // It is NOT the same as the notification ID used in communication with the server.
+ val systemNotificationId: Int =
+ activeStatusBarNotification?.id ?: calculateCRC32(System.currentTimeMillis().toString()).toInt()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && CHAT == pushMessage.type &&
+ pushMessage.notificationUser != null
+ ) {
+ prepareChatNotification(notificationBuilder, activeStatusBarNotification, systemNotificationId)
+ }
+ sendNotification(systemNotificationId, notificationBuilder.build())
+ }
+
+ private fun calculateCRC32(s: String): Long {
+ val crc32 = CRC32()
+ crc32.update(s.toByteArray())
+ return crc32.value
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ private fun prepareChatNotification(
+ notificationBuilder: NotificationCompat.Builder,
+ activeStatusBarNotification: StatusBarNotification?,
+ systemNotificationId: Int
+ ) {
+ val notificationUser = pushMessage.notificationUser
+ val userType = notificationUser!!.type
+ var style: NotificationCompat.MessagingStyle? = null
+ if (activeStatusBarNotification != null) {
+ style = NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(
+ activeStatusBarNotification.notification
+ )
+ }
+ val person = Person.Builder()
+ .setKey(signatureVerification.user!!.id.toString() + "@" + notificationUser.id)
+ .setName(EmojiCompat.get().process(notificationUser.name!!))
+ .setBot("bot" == userType)
+ notificationBuilder.setOnlyAlertOnce(true)
+ addReplyAction(notificationBuilder, systemNotificationId)
+ addMarkAsReadAction(notificationBuilder, systemNotificationId)
+
+ if ("user" == userType || "guest" == userType) {
+ val baseUrl = signatureVerification.user!!.baseUrl
+ val avatarUrl = if ("user" == userType) ApiUtils.getUrlForAvatar(
+ baseUrl,
+ notificationUser.id,
+ false
+ ) else ApiUtils.getUrlForGuestAvatar(baseUrl, notificationUser.name, false)
+ person.setIcon(loadAvatarSync(avatarUrl))
+ }
+ notificationBuilder.setStyle(getStyle(person.build(), style))
+ }
+
+ private fun buildIntentForAction(cls: Class<*>, systemNotificationId: Int, messageId: Int): PendingIntent {
+ val actualIntent = Intent(context, cls)
+
+ // NOTE - systemNotificationId is an internal ID used on the device only.
+ // It is NOT the same as the notification ID used in communication with the server.
+ actualIntent.putExtra(KEY_SYSTEM_NOTIFICATION_ID, systemNotificationId)
+ actualIntent.putExtra(KEY_INTERNAL_USER_ID, signatureVerification.user?.id)
+ actualIntent.putExtra(KEY_ROOM_TOKEN, pushMessage.id)
+ actualIntent.putExtra(KEY_MESSAGE_ID, messageId)
+
+ val intentFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ } else {
+ PendingIntent.FLAG_UPDATE_CURRENT
+ }
+ return PendingIntent.getBroadcast(context, systemNotificationId, actualIntent, intentFlag)
+ }
+
+ private fun addMarkAsReadAction(notificationBuilder: NotificationCompat.Builder, systemNotificationId: Int) {
+ if (pushMessage.objectId != null) {
+ val messageId: Int = try {
+ parseMessageId(pushMessage.objectId!!)
+ } catch (nfe: NumberFormatException) {
+ Log.e(TAG, "Failed to parse messageId from objectId, skip adding mark-as-read action.", nfe)
+ return
+ }
+
+ val pendingIntent = buildIntentForAction(
+ MarkAsReadReceiver::class.java,
+ systemNotificationId,
+ messageId
+ )
+ val action = NotificationCompat.Action.Builder(
+ R.drawable.ic_eye,
+ context!!.resources.getString(R.string.nc_mark_as_read),
+ pendingIntent
+ )
+ .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
+ .setShowsUserInterface(false)
+ .build()
+ notificationBuilder.addAction(action)
+ }
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ private fun addReplyAction(notificationBuilder: NotificationCompat.Builder, systemNotificationId: Int) {
+ val replyLabel = context!!.resources.getString(R.string.nc_reply)
+ val remoteInput = RemoteInput.Builder(NotificationUtils.KEY_DIRECT_REPLY)
+ .setLabel(replyLabel)
+ .build()
+
+ val replyPendingIntent = buildIntentForAction(DirectReplyReceiver::class.java, systemNotificationId, 0)
+ val replyAction = NotificationCompat.Action.Builder(R.drawable.ic_reply, replyLabel, replyPendingIntent)
+ .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
+ .setShowsUserInterface(false)
+ .setAllowGeneratedReplies(true)
+ .addRemoteInput(remoteInput)
+ .build()
+ notificationBuilder.addAction(replyAction)
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ private fun getStyle(person: Person, style: NotificationCompat.MessagingStyle?): NotificationCompat.MessagingStyle {
+ val newStyle = NotificationCompat.MessagingStyle(person)
+ newStyle.conversationTitle = pushMessage.subject
+ newStyle.isGroupConversation = "one2one" != conversationType
+ style?.messages?.forEach(
+ Consumer { message: NotificationCompat.MessagingStyle.Message ->
+ newStyle.addMessage(
+ NotificationCompat.MessagingStyle.Message(
+ message.text,
+ message.timestamp,
+ message.person
+ )
+ )
+ }
+ )
+ newStyle.addMessage(pushMessage.text, pushMessage.timestamp, person)
+ return newStyle
+ }
+
+ @Throws(NumberFormatException::class)
+ private fun parseMessageId(objectId: String): Int {
+ val objectIdParts = objectId.split("/".toRegex()).toTypedArray()
+ return if (objectIdParts.size < 2) {
+ throw NumberFormatException("Invalid objectId, doesn't contain at least one '/'")
+ } else {
+ objectIdParts[1].toInt()
+ }
+ }
+
+ private fun sendNotification(notificationId: Int, notification: Notification) {
+ Log.d(TAG, "show notification with id $notificationId")
+ notificationManager.notify(notificationId, notification)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ // On devices with Android 8.0 (Oreo) or later, notification sound will be handled by the system
+ // if notifications have not been disabled by the user.
+ return
+ }
+ if (Notification.CATEGORY_CALL != notification.category || !muteCall) {
+ val soundUri = getMessageRingtoneUri(context!!, appPreferences)
+ if (soundUri != null && !ApplicationWideCurrentRoomHolder.getInstance().isInCall &&
+ (shouldPlaySound() || importantConversation)
+ ) {
+ val audioAttributesBuilder =
+ AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ if (CHAT == pushMessage.type || ROOM == pushMessage.type) {
+ audioAttributesBuilder.setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT)
+ } else {
+ audioAttributesBuilder.setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST)
+ }
+ val mediaPlayer = MediaPlayer()
+ try {
+ mediaPlayer.setDataSource(context!!, soundUri)
+ mediaPlayer.setAudioAttributes(audioAttributesBuilder.build())
+ mediaPlayer.setOnPreparedListener { mediaPlayer.start() }
+ mediaPlayer.setOnCompletionListener { obj: MediaPlayer -> obj.release() }
+ mediaPlayer.prepareAsync()
+ } catch (e: IOException) {
+ Log.e(TAG, "Failed to set data source")
+ }
+ }
+ }
+ }
+
+ private fun removeNotification(notificationId: Int) {
+ Log.d(TAG, "removed notification with id $notificationId")
+ notificationManager.cancel(notificationId)
+ }
+
+ private fun checkIfCallIsActive(
+ signatureVerification: SignatureVerification,
+ decryptedPushMessage: DecryptedPushMessage
+ ) {
+ Log.d(TAG, "checkIfCallIsActive")
+ var hasParticipantsInCall = true
+ var inCallOnDifferentDevice = false
+
+ val apiVersion = ApiUtils.getConversationApiVersion(
+ signatureVerification.user,
+ intArrayOf(ApiUtils.APIv4, 1)
+ )
+
+ var isCallNotificationVisible = true
+
+ ncApi.getPeersForCall(
+ credentials,
+ ApiUtils.getUrlForCall(
+ apiVersion,
+ signatureVerification.user!!.baseUrl,
+ decryptedPushMessage.id
+ )
+ )
+ .repeatWhen { completed ->
+ completed.zipWith(Observable.range(TIMER_START, TIMER_COUNT)) { _, i -> i }
+ .flatMap { Observable.timer(TIMER_DELAY, TimeUnit.SECONDS) }
+ .takeWhile { isCallNotificationVisible && hasParticipantsInCall && !inCallOnDifferentDevice }
+ }
+ .subscribeOn(Schedulers.io())
+ .subscribe(object : Observer<ParticipantsOverall> {
+ override fun onSubscribe(d: Disposable) = Unit
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun onNext(participantsOverall: ParticipantsOverall) {
+ val participantList: List<Participant> = participantsOverall.ocs!!.data!!
+ hasParticipantsInCall = participantList.isNotEmpty()
+ if (hasParticipantsInCall) {
+ for (participant in participantList) {
+ if (participant.actorId == signatureVerification.user!!.userId &&
+ participant.actorType == Participant.ActorType.USERS
+ ) {
+ inCallOnDifferentDevice = true
+ break
+ }
+ }
+ }
+ if (inCallOnDifferentDevice) {
+ Log.d(TAG, "inCallOnDifferentDevice is true")
+ removeNotification(decryptedPushMessage.timestamp.toInt())
+ }
+
+ if (!hasParticipantsInCall) {
+ showMissedCallNotification()
+ Log.d(TAG, "no participants in call")
+ removeNotification(decryptedPushMessage.timestamp.toInt())
+ }
+
+ isCallNotificationVisible = isCallNotificationVisible(decryptedPushMessage)
+ }
+
+ override fun onError(e: Throwable) {
+ Log.e(TAG, "Error in getPeersForCall", e)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun onComplete() {
+
+ if (isCallNotificationVisible) {
+ // this state can be reached when call timeout is reached.
+ showMissedCallNotification()
+ }
+
+ removeNotification(decryptedPushMessage.timestamp.toInt())
+ }
+ })
+ }
+
+ fun showMissedCallNotification() {
+ val apiVersion = ApiUtils.getConversationApiVersion(
+ signatureVerification.user,
+ intArrayOf(
+ ApiUtils.APIv4,
+ ApiUtils.APIv3, 1
+ )
+ )
+ ncApi.getRoom(
+ credentials,
+ ApiUtils.getUrlForRoom(
+ apiVersion, signatureVerification.user?.baseUrl,
+ pushMessage.id
+ )
+ )
+ .subscribeOn(Schedulers.io())
+ .retry(GET_ROOM_RETRY_COUNT)
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(object : Observer<RoomOverall> {
+ override fun onSubscribe(d: Disposable) {
+ // unused atm
+ }
+
+ override fun onNext(roomOverall: RoomOverall) {
+ val currentConversation = roomOverall.ocs!!.data
+ val notificationBuilder: NotificationCompat.Builder?
+
+ notificationBuilder = NotificationCompat.Builder(
+ context!!,
+ NotificationUtils.NotificationChannels
+ .NOTIFICATION_CHANNEL_MESSAGES_V4.name
+ )
+
+ val notification: Notification = notificationBuilder
+ .setContentTitle(
+ String.format(
+ context!!.resources.getString(R.string.nc_missed_call),
+ currentConversation!!.displayName
+ )
+ )
+ .setSmallIcon(R.drawable.ic_baseline_phone_missed_24)
+ .setOngoing(false)
+ .setAutoCancel(true)
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .setContentIntent(getIntentToOpenConversation())
+ .build()
+
+ val notificationId: Int = SystemClock.uptimeMillis().toInt()
+ notificationManager.notify(notificationId, notification)
+ Log.d(TAG, "'you missed a call' notification was created")
+ }
+
+ override fun onError(e: Throwable) {
+ Log.e(TAG, "An error occurred while fetching room for the 'missed call' notification", e)
+ }
+
+ override fun onComplete() {
+ // unused atm
+ }
+ })
+ }
+
+ private fun getIntentToOpenConversation(): PendingIntent? {
+ val bundle = Bundle()
+ val intent = Intent(context, MainActivity::class.java)
+ intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
+
+ bundle.putString(KEY_ROOM_TOKEN, pushMessage.id)
+ bundle.putParcelable(KEY_USER_ENTITY, signatureVerification.user)
+ bundle.putBoolean(KEY_FROM_NOTIFICATION_START_CALL, false)
+
+ intent.putExtras(bundle)
+
+ val requestCode = System.currentTimeMillis().toInt()
+ val intentFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ PendingIntent.FLAG_MUTABLE
+ } else {
+ 0
+ }
+ return PendingIntent.getActivity(context, requestCode, intent, intentFlag)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun isCallNotificationVisible(decryptedPushMessage: DecryptedPushMessage): Boolean {
+ var isVisible = false
+
+ val notificationManager = context!!.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+ val notifications = notificationManager.activeNotifications
+ for (notification in notifications) {
+ if (notification.id == decryptedPushMessage.timestamp.toInt()) {
+ isVisible = true
+ break
+ }
+ }
+ return isVisible
+ }
+
+ companion object {
+ val TAG = NotificationWorker::class.simpleName
+ private const val CHAT = "chat"
+ private const val ROOM = "room"
+ private const val SPREED_APP = "spreed"
+ private const val TIMER_START = 1
+ private const val TIMER_COUNT = 12
+ private const val TIMER_DELAY: Long = 5
+ private const val GET_ROOM_RETRY_COUNT: Long = 3
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt
index a58126b22..5be620dd3 100644
--- a/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt
+++ b/app/src/main/java/com/nextcloud/talk/jobs/UploadAndShareFilesWorker.kt
@@ -128,7 +128,7 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
val mimeType = context.contentResolver.getType(sourceFileUri)?.toMediaTypeOrNull()
uploadSuccess = ChunkedFileUploader(
- okHttpClient!!,
+ okHttpClient,
currentUser,
roomToken,
metaData,
diff --git a/app/src/main/java/com/nextcloud/talk/models/json/notifications/Notification.kt b/app/src/main/java/com/nextcloud/talk/models/json/notifications/Notification.kt
index 0d9f1e8f8..6df6f3af1 100644
--- a/app/src/main/java/com/nextcloud/talk/models/json/notifications/Notification.kt
+++ b/app/src/main/java/com/nextcloud/talk/models/json/notifications/Notification.kt
@@ -56,7 +56,7 @@ data class Notification(
@JsonField(name = ["messageRich"])
var messageRich: String?,
@JsonField(name = ["messageRichParameters"])
- var messageRichParameters: HashMap<String, HashMap<String, String>>?,
+ var messageRichParameters: HashMap<String?, HashMap<String?, String?>>?,
@JsonField(name = ["link"])
var link: String?,
@JsonField(name = ["actions"])
diff --git a/app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationOverall.kt
index 2e20a6349..5c5fb2287 100644
--- a/app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationOverall.kt
+++ b/app/src/main/java/com/nextcloud/talk/models/json/notifications/NotificationOverall.kt
@@ -24,6 +24,8 @@ package com.nextcloud.talk.models.json.notifications
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject
+// see https://github.com/nextcloud/notifications/blob/master/docs/ocs-endpoint-v2.md
+
@JsonObject
data class NotificationOverall(
@JsonField(name = ["ocs"])
diff --git a/app/src/main/java/com/nextcloud/talk/models/json/push/DecryptedPushMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/push/DecryptedPushMessage.kt
index 001305b51..264184a75 100644
--- a/app/src/main/java/com/nextcloud/talk/models/json/push/DecryptedPushMessage.kt
+++ b/app/src/main/java/com/nextcloud/talk/models/json/push/DecryptedPushMessage.kt
@@ -70,6 +70,7 @@ data class DecryptedPushMessage(
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() : this(null, null, "", null, 0, null, false, false, false, null, null, 0, null)
+ @Suppress("Detekt.ComplexMethod")
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
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 c12c04572..9e3841544 100644
--- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java
+++ b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java
@@ -391,6 +391,7 @@ public class ApiUtils {
getApplicationContext().getResources().getString(R.string.nc_push_server_url) + "/devices";
}
+ // see https://github.com/nextcloud/notifications/blob/master/docs/ocs-endpoint-v2.md
public static String getUrlForNotificationWithId(String baseUrl, String notificationId) {
return baseUrl + ocsApiVersion + "/apps/notifications/api/v2/notifications/" + notificationId;
}
diff --git a/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt
index 4341f18c1..62bbc837c 100644
--- a/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt
+++ b/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt
@@ -47,6 +47,7 @@ import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.preferences.AppPreferences
import java.io.IOException
+@Suppress("TooManyFunctions")
object NotificationUtils {
enum class NotificationChannels {
@@ -241,7 +242,7 @@ object NotificationUtils {
}
}
- fun cancelExistingNotificationWithId(context: Context?, conversationUser: User, notificationId: Long?) {
+ fun cancelNotification(context: Context?, conversationUser: User, notificationId: Long?) {
scanNotifications(context, conversationUser) { notificationManager, statusBarNotification, notification ->
if (notificationId == notification.extras.getLong(BundleKeys.KEY_NOTIFICATION_ID)) {
notificationManager.cancel(statusBarNotification.id)
diff --git a/app/src/main/res/drawable/ic_baseline_phone_missed_24.xml b/app/src/main/res/drawable/ic_baseline_phone_missed_24.xml
new file mode 100644
index 000000000..7928dce1a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_phone_missed_24.xml
@@ -0,0 +1,5 @@
+<vector android:autoMirrored="true" android:height="24dp"
+ android:tint="#000000" android:viewportHeight="24"
+ android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M6.5,5.5L12,11l7,-7 -1,-1 -6,6 -4.5,-4.5L11,4.5L11,3L5,3v6h1.5L6.5,5.5zM23.71,16.67C20.66,13.78 16.54,12 12,12 7.46,12 3.34,13.78 0.29,16.67c-0.18,0.18 -0.29,0.43 -0.29,0.71s0.11,0.53 0.29,0.71l2.48,2.48c0.18,0.18 0.43,0.29 0.71,0.29 0.27,0 0.52,-0.11 0.7,-0.28 0.79,-0.74 1.69,-1.36 2.66,-1.85 0.33,-0.16 0.56,-0.5 0.56,-0.9v-3.1c1.45,-0.48 3,-0.73 4.6,-0.73 1.6,0 3.15,0.25 4.6,0.72v3.1c0,0.39 0.23,0.74 0.56,0.9 0.98,0.49 1.87,1.12 2.67,1.85 0.18,0.18 0.43,0.28 0.7,0.28 0.28,0 0.53,-0.11 0.71,-0.29l2.48,-2.48c0.18,-0.18 0.29,-0.43 0.29,-0.71s-0.12,-0.52 -0.3,-0.7z"/>
+</vector>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ef4df1cff..8f674a172 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -210,6 +210,7 @@
<string name="nc_call_state_in_call">%1$s in call</string>
<string name="nc_call_state_with_phone">%1$s with phone</string>
<string name="nc_call_state_with_video">%1$s with video</string>
+ <string name="nc_missed_call">You missed a call from %s</string>
<!-- Picture in Picture -->
<string name="nc_pip_microphone_mute">Mute microphone</string>
@@ -246,7 +247,6 @@
<string name="nc_share_subject">%1$s invitation</string>
<string name="nc_share_text_pass">\nPassword: %1$s</string>
- <!-- Magical stuff -->
<string name="nc_push_to_talk">Push-to-talk</string>
<string name="nc_push_to_talk_desc">With microphone disabled, click&amp;hold to use Push-to-talk</string>
<string name="nc_configure_cert_auth">Select authentication certificate</string>