From 3a517f57601d23dbbf2bc9685d06b2eb6c8eca37 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Wed, 21 Sep 2022 14:58:18 +0200 Subject: [PATCH 1/3] add missed call notification, show delivery delay in debug mode - add missed call notifications in NotificationWorker and CallNotificationActivity - introduce refactoring of Notification handling (isolate firebase stuff from other logic). All "UI-notification" logic from ChatAndCallMessagingService was moved to NotificationWorker. ChatAndCallMessagingService was renamed to NCFirebaseMessagingService because it is now only responsible for firebase stuff. This separation should make it easier for alternative push services to dock with the app (if they are incorporated in the future). - for DEBUG mode: show delivery delay time in notifications (time between sending from firebase to receive on device). Signed-off-by: Marcel Hibbe --- app/src/gplay/AndroidManifest.xml | 2 +- .../firebase/ChatAndCallMessagingService.kt | 327 ------- .../firebase/NCFirebaseMessagingService.kt | 98 ++ .../activities/CallNotificationActivity.java | 451 --------- .../activities/CallNotificationActivity.kt | 474 ++++++++++ .../talk/events/CallNotificationClick.kt | 23 - .../talk/jobs/NotificationWorker.java | 695 -------------- .../nextcloud/talk/jobs/NotificationWorker.kt | 873 ++++++++++++++++++ .../talk/jobs/UploadAndShareFilesWorker.kt | 2 +- .../models/json/notifications/Notification.kt | 2 +- .../json/notifications/NotificationOverall.kt | 2 + .../com/nextcloud/talk/utils/ApiUtils.java | 1 + .../nextcloud/talk/utils/NotificationUtils.kt | 2 +- .../nextcloud/talk/utils/bundle/BundleKeys.kt | 1 + .../drawable/ic_baseline_phone_missed_24.xml | 5 + app/src/main/res/values/strings.xml | 2 +- 16 files changed, 1459 insertions(+), 1501 deletions(-) delete mode 100644 app/src/gplay/java/com/nextcloud/talk/services/firebase/ChatAndCallMessagingService.kt create mode 100644 app/src/gplay/java/com/nextcloud/talk/services/firebase/NCFirebaseMessagingService.kt delete mode 100644 app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.java create mode 100644 app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.kt delete mode 100644 app/src/main/java/com/nextcloud/talk/events/CallNotificationClick.kt delete mode 100644 app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.java create mode 100644 app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt create mode 100644 app/src/main/res/drawable/ic_baseline_phone_missed_24.xml 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 @@ 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 - * Copyright (C) 2017-2019 Mario Danic - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package com.nextcloud.talk.services.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 { - override fun onSubscribe(d: Disposable) = Unit - - override fun onNext(participantsOverall: ParticipantsOverall) { - val participantList: List = participantsOverall.ocs!!.data!! - hasParticipantsInCall = participantList.isNotEmpty() - if (hasParticipantsInCall) { - for (participant in participantList) { - if (participant.actorId == signatureVerification.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..e36c67fd3 --- /dev/null +++ b/app/src/gplay/java/com/nextcloud/talk/services/firebase/NCFirebaseMessagingService.kt @@ -0,0 +1,98 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * @author Tim Krüger + * @author Marcel Hibbe + * Copyright (C) 2022 Marcel Hibbe + * Copyright (C) 2022 Tim Krüger + * Copyright (C) 2017-2019 Mario Danic + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.nextcloud.talk.services.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) + .putLong(BundleKeys.KEY_NOTIFICATION_SENT_TIME, remoteMessage.sentTime) + .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 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.nextcloud.talk.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 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() { - @Override - public void onSubscribe(@NonNull Disposable d) { - disposablesList.add(d); - } - - @Override - public void onNext(@NonNull ParticipantsOverall participantsOverall) { - boolean hasParticipantsInCall = false; - boolean inCallOnDifferentDevice = false; - List 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() { - @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> 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> 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..856da2405 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.kt @@ -0,0 +1,474 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * @author Marcel Hibbe + * Copyright (C) 2022 Marcel Hibbe + * Copyright (C) 2017-2018 Mario Danic + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.nextcloud.talk.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 = 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 -> + completed.zipWith(Observable.range(TIMER_START, TIMER_COUNT)) { _: Any?, i: Int? -> i!! } + .flatMap { Observable.timer(TIMER_DELAY, TimeUnit.SECONDS) } + .takeWhile { !leavingScreen } + } + .subscribe(object : Observer { + 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) + } + + 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 { + 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() + } + + 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>) { + 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 - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.nextcloud.talk.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 - * Copyright (C) 2017-2018 Mario Danic - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.nextcloud.talk.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() { - @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() { - @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> subjectRichParameters = notification - .getSubjectRichParameters(); - - decryptedPushMessage.setTimestamp(notification.getDatetime().getMillis()); - - if (subjectRichParameters != null && subjectRichParameters.size() > 0) { - HashMap callHashMap = subjectRichParameters.get("call"); - HashMap userHashMap = subjectRichParameters.get("user"); - HashMap 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..8d8e3995e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt @@ -0,0 +1,873 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * @author Mario Danic + * @author Marcel Hibbe + * Copyright (C) 2022 Marcel Hibbe + * Copyright (C) 2022 Andy Scherzinger + * Copyright (C) 2017-2018 Mario Danic + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.nextcloud.talk.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.BuildConfig +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 var deliveryDelayTime: Long = 0 + private lateinit var notificationManager: NotificationManagerCompat + + override fun doWork(): Result { + sharedApplication!!.componentApplication.inject(this) + context = applicationContext + + calculateDeliveryDelayTime(inputData) + 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 + getDeliveryDelayTimeForDebug()) + ) + .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) + } + + /** + * Calculates the time between the sent time (from firebase) and the received time on the device. + * 'deliveryDelayTime' is displayed in debug mode right after the notification message. + * A huge delay means that there might be something wrong on device side. + */ + private fun calculateDeliveryDelayTime(inputData: Data) { + val messageSentTime = inputData.getLong(BundleKeys.KEY_NOTIFICATION_SENT_TIME, 0) + deliveryDelayTime = if (messageSentTime == 0L) { + 0 + } else { + System.currentTimeMillis() - messageSentTime + } + } + + 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 + ) + } + + 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 { + 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 + } + }) + } + + 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 + getDeliveryDelayTimeForDebug()) + ) + } + if (!TextUtils.isEmpty(pushMessage.text)) { + notificationBuilder.setContentText( + EmojiCompat.get().process(pushMessage.text!! + getDeliveryDelayTimeForDebug()) + ) + } + 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 + getDeliveryDelayTimeForDebug(), pushMessage.timestamp, person) + return newStyle + } + + private fun getDeliveryDelayTimeForDebug(): String { + if (BuildConfig.DEBUG) { + return " ($deliveryDelayTime ms delay)" + } + return "" + } + + @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 { + override fun onSubscribe(d: Disposable) = Unit + + @RequiresApi(Build.VERSION_CODES.M) + override fun onNext(participantsOverall: ParticipantsOverall) { + val participantList: List = 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 { + 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 + ) + getDeliveryDelayTimeForDebug() + ) + .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>?, + var messageRichParameters: HashMap>?, @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/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..6db58d1b2 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt @@ -241,7 +241,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/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt b/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt index 82db195ed..4bf5ed90e 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt @@ -48,6 +48,7 @@ object BundleKeys { const val KEY_MODIFIED_BASE_URL = "KEY_MODIFIED_BASE_URL" const val KEY_NOTIFICATION_SUBJECT = "KEY_NOTIFICATION_SUBJECT" const val KEY_NOTIFICATION_SIGNATURE = "KEY_NOTIFICATION_SIGNATURE" + const val KEY_NOTIFICATION_SENT_TIME = "KEY_NOTIFICATION_SENT_TIME" const val KEY_INTERNAL_USER_ID = "KEY_INTERNAL_USER_ID" const val KEY_CONVERSATION_TYPE = "KEY_CONVERSATION_TYPE" const val KEY_INVITED_PARTICIPANTS = "KEY_INVITED_PARTICIPANTS" 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 @@ + + + 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 @@ %1$s in call %1$s with phone %1$s with video + You missed a call from %s Mute microphone @@ -246,7 +247,6 @@ %1$s invitation \nPassword: %1$s - Push-to-talk With microphone disabled, click&hold to use Push-to-talk Select authentication certificate From ca145d170c3629f737a966dd095e343570911f86 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Thu, 10 Nov 2022 13:38:49 +0100 Subject: [PATCH 2/3] revert calculation of delayed delivery time this didn't make sense because time between firebase and devices is not synchronized, so the results were useless. Signed-off-by: Marcel Hibbe --- .../firebase/NCFirebaseMessagingService.kt | 1 - .../nextcloud/talk/jobs/NotificationWorker.kt | 36 +++---------------- .../nextcloud/talk/utils/bundle/BundleKeys.kt | 1 - 3 files changed, 5 insertions(+), 33 deletions(-) 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 index e36c67fd3..04f61b052 100644 --- a/app/src/gplay/java/com/nextcloud/talk/services/firebase/NCFirebaseMessagingService.kt +++ b/app/src/gplay/java/com/nextcloud/talk/services/firebase/NCFirebaseMessagingService.kt @@ -65,7 +65,6 @@ class NCFirebaseMessagingService : FirebaseMessagingService() { val messageData = Data.Builder() .putString(BundleKeys.KEY_NOTIFICATION_SUBJECT, subject) .putString(BundleKeys.KEY_NOTIFICATION_SIGNATURE, signature) - .putLong(BundleKeys.KEY_NOTIFICATION_SENT_TIME, remoteMessage.sentTime) .build() val notificationWork = OneTimeWorkRequest.Builder(NotificationWorker::class.java).setInputData(messageData) diff --git a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt index 8d8e3995e..7cacd379e 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt @@ -53,7 +53,6 @@ import androidx.work.Worker import androidx.work.WorkerParameters import autodagger.AutoInjector import com.bluelinelabs.logansquare.LoganSquare -import com.nextcloud.talk.BuildConfig import com.nextcloud.talk.R import com.nextcloud.talk.activities.CallNotificationActivity import com.nextcloud.talk.activities.MainActivity @@ -136,14 +135,12 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor private var conversationType: String? = "one2one" private var muteCall = false private var importantConversation = false - private var deliveryDelayTime: Long = 0 private lateinit var notificationManager: NotificationManagerCompat override fun doWork(): Result { sharedApplication!!.componentApplication.inject(this) context = applicationContext - calculateDeliveryDelayTime(inputData) initDecryptedData(inputData) initNcApiAndCredentials() @@ -247,9 +244,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor .setSubText(baseUrl) .setShowWhen(true) .setWhen(pushMessage.timestamp) - .setContentTitle( - EmojiCompat.get().process(pushMessage.subject + getDeliveryDelayTimeForDebug()) - ) + .setContentTitle(EmojiCompat.get().process(pushMessage.subject)) .setAutoCancel(true) .setOngoing(true) .setContentIntent(fullScreenPendingIntent) @@ -263,20 +258,6 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor checkIfCallIsActive(signatureVerification, pushMessage) } - /** - * Calculates the time between the sent time (from firebase) and the received time on the device. - * 'deliveryDelayTime' is displayed in debug mode right after the notification message. - * A huge delay means that there might be something wrong on device side. - */ - private fun calculateDeliveryDelayTime(inputData: Data) { - val messageSentTime = inputData.getLong(BundleKeys.KEY_NOTIFICATION_SENT_TIME, 0) - deliveryDelayTime = if (messageSentTime == 0L) { - 0 - } else { - System.currentTimeMillis() - messageSentTime - } - } - private fun initNcApiAndCredentials() { credentials = ApiUtils.getCredentials( signatureVerification.user!!.username, @@ -456,12 +437,12 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor .setAutoCancel(true) if (!TextUtils.isEmpty(pushMessage.subject)) { notificationBuilder.setContentTitle( - EmojiCompat.get().process(pushMessage.subject + getDeliveryDelayTimeForDebug()) + EmojiCompat.get().process(pushMessage.subject) ) } if (!TextUtils.isEmpty(pushMessage.text)) { notificationBuilder.setContentText( - EmojiCompat.get().process(pushMessage.text!! + getDeliveryDelayTimeForDebug()) + EmojiCompat.get().process(pushMessage.text!!) ) } if (Build.VERSION.SDK_INT >= 23) { @@ -625,17 +606,10 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor ) } ) - newStyle.addMessage(pushMessage.text + getDeliveryDelayTimeForDebug(), pushMessage.timestamp, person) + newStyle.addMessage(pushMessage.text, pushMessage.timestamp, person) return newStyle } - private fun getDeliveryDelayTimeForDebug(): String { - if (BuildConfig.DEBUG) { - return " ($deliveryDelayTime ms delay)" - } - return "" - } - @Throws(NumberFormatException::class) private fun parseMessageId(objectId: String): Int { val objectIdParts = objectId.split("/".toRegex()).toTypedArray() @@ -801,7 +775,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor String.format( context!!.resources.getString(R.string.nc_missed_call), currentConversation!!.displayName - ) + getDeliveryDelayTimeForDebug() + ) ) .setSmallIcon(R.drawable.ic_baseline_phone_missed_24) .setOngoing(false) diff --git a/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt b/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt index 4bf5ed90e..82db195ed 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt @@ -48,7 +48,6 @@ object BundleKeys { const val KEY_MODIFIED_BASE_URL = "KEY_MODIFIED_BASE_URL" const val KEY_NOTIFICATION_SUBJECT = "KEY_NOTIFICATION_SUBJECT" const val KEY_NOTIFICATION_SIGNATURE = "KEY_NOTIFICATION_SIGNATURE" - const val KEY_NOTIFICATION_SENT_TIME = "KEY_NOTIFICATION_SENT_TIME" const val KEY_INTERNAL_USER_ID = "KEY_INTERNAL_USER_ID" const val KEY_CONVERSATION_TYPE = "KEY_CONVERSATION_TYPE" const val KEY_INVITED_PARTICIPANTS = "KEY_INVITED_PARTICIPANTS" From 3729f1130a32e83d5ab674326858beb325e57284 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Thu, 10 Nov 2022 15:40:04 +0100 Subject: [PATCH 3/3] suppress some detekt warnings Signed-off-by: Marcel Hibbe --- .../com/nextcloud/talk/activities/CallNotificationActivity.kt | 2 ++ app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt | 2 ++ .../com/nextcloud/talk/models/json/push/DecryptedPushMessage.kt | 1 + app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt | 1 + 4 files changed, 6 insertions(+) diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.kt index 856da2405..3d30e3a0c 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.kt @@ -297,6 +297,7 @@ class CallNotificationActivity : CallBaseActivity() { return PendingIntent.getActivity(context, requestCode, intent, intentFlag) } + @Suppress("MagicNumber") private fun handleFromNotification() { val apiVersion = ApiUtils.getConversationApiVersion( userBeingCalled, @@ -363,6 +364,7 @@ class CallNotificationActivity : CallBaseActivity() { showAnswerControls() } + @Suppress("MagicNumber") private fun setAvatarForOneToOneCall() { val imageRequest = DisplayUtils.getImageRequestForUrl( ApiUtils.getUrlForAvatar( diff --git a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt index 7cacd379e..2d7dc11b7 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt @@ -274,6 +274,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor ) } + @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) @@ -389,6 +390,7 @@ class NotificationWorker(context: Context, workerParams: WorkerParameters) : Wor }) } + @Suppress("MagicNumber") private fun showNotification(intent: Intent) { val largeIcon: Bitmap val priority = NotificationCompat.PRIORITY_HIGH 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/NotificationUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt index 6db58d1b2..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 {