diff --git a/app/build.gradle b/app/build.gradle
index 5d6587019..4f750b15f 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -36,7 +36,7 @@ apply plugin: "org.jlleitschuh.gradle.ktlint"
apply plugin: 'kotlinx-serialization'
android {
- compileSdkVersion 31
+ compileSdkVersion 32
buildToolsVersion '33.0.0'
namespace 'com.nextcloud.talk'
@@ -48,8 +48,8 @@ android {
// mayor.minor.hotfix.increment (for increment: 01-50=Alpha / 51-89=RC / 90-99=stable)
// xx .xxx .xx .xx
- versionCode 150010007
- versionName "15.1.0 Alpha 07"
+ versionCode 150010008
+ versionName "15.1.0 Alpha 08"
flavorDimensions "default"
renderscriptTargetApi 19
@@ -142,7 +142,7 @@ android {
ext {
androidxCameraVersion = "1.1.0"
coilKtVersion = "2.2.2"
- daggerVersion = "2.44"
+ daggerVersion = "2.44.2"
lifecycleVersion = '2.5.1'
okhttpVersion = "4.10.0"
materialDialogsVersion = "3.3.0"
@@ -151,10 +151,10 @@ ext {
roomVersion = "2.4.3"
workVersion = "2.7.1"
markwonVersion = "4.6.2"
- espressoVersion = "3.4.0"
+ espressoVersion = "3.5.0"
}
-def webRtcVersion = "96.4664.0"
+def webRtcVersion = "106.5249.0"
tasks.register('downloadWebRtc', DownloadWebRtcTask){
version = webRtcVersion
}
@@ -175,7 +175,7 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1"
implementation 'androidx.appcompat:appcompat:1.5.1'
- implementation 'com.google.android.material:material:1.6.1'
+ implementation 'com.google.android.material:material:1.7.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation "com.vanniktech:emoji-google:0.15.0"
implementation group: 'androidx.emoji', name: 'emoji-bundled', version: '1.1.0'
@@ -207,14 +207,14 @@ dependencies {
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation "io.reactivex.rxjava2:rxjava:2.2.21"
- implementation 'com.bluelinelabs:conductor:3.1.7'
+ implementation 'com.bluelinelabs:conductor:3.1.8'
implementation "com.squareup.okhttp3:okhttp:${okhttpVersion}"
implementation "com.squareup.okhttp3:okhttp-urlconnection:${okhttpVersion}"
implementation "com.squareup.okhttp3:logging-interceptor:${okhttpVersion}"
implementation 'com.bluelinelabs:logansquare:1.3.7'
- implementation 'com.fasterxml.jackson.core:jackson-core:2.13.4'
+ implementation 'com.fasterxml.jackson.core:jackson-core:2.14.0'
kapt 'com.bluelinelabs:logansquare-compiler:1.3.7'
implementation "com.squareup.retrofit2:retrofit:${retrofit2Version}"
@@ -254,7 +254,7 @@ dependencies {
implementation 'com.github.nextcloud-deps.fresco:webpsupport:v111'
implementation 'com.github.nextcloud-deps.fresco:animated-gif:v111'
implementation 'com.github.nextcloud-deps.fresco:imagepipeline-okhttp3:v111'
- implementation 'joda-time:joda-time:2.12.0'
+ implementation 'joda-time:joda-time:2.12.1'
implementation "io.coil-kt:coil:${coilKtVersion}"
implementation "io.coil-kt:coil-gif:${coilKtVersion}"
implementation "io.coil-kt:coil-svg:${coilKtVersion}"
@@ -280,8 +280,7 @@ dependencies {
implementation "io.noties.markwon:core:$markwonVersion"
- //implementation 'com.github.dhaval2404:imagepicker:1.8'
- implementation 'com.github.nextcloud-deps:ImagePicker:1.8.0.2'
+ implementation 'com.github.nextcloud-deps:ImagePicker:2.1.0.2'
implementation 'com.elyeproj.libraries:loaderviewlibrary:2.0.0'
implementation 'org.osmdroid:osmdroid-android:6.1.14'
@@ -293,9 +292,9 @@ dependencies {
implementation 'androidx.core:core-ktx:1.8.0'
testImplementation 'junit:junit:4.13.2'
- testImplementation 'org.mockito:mockito-core:4.8.1'
+ testImplementation 'org.mockito:mockito-core:4.9.0'
- androidTestImplementation "androidx.test:core:1.4.0"
+ androidTestImplementation "androidx.test:core:1.5.0"
// Espresso core
androidTestImplementation ("androidx.test.espresso:espresso-core:$espressoVersion", {
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..04f61b052
--- /dev/null
+++ b/app/src/gplay/java/com/nextcloud/talk/services/firebase/NCFirebaseMessagingService.kt
@@ -0,0 +1,97 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * @author Tim Krüger
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe
+ * 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)
+ .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/CallActivity.java b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java
index 0a771ea02..7fc54561f 100644
--- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java
+++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java
@@ -138,6 +138,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
import javax.inject.Inject;
@@ -439,7 +440,7 @@ public class CallActivity extends CallBaseActivity {
binding.gridview.setOnItemClickListener((parent, view, position, id) -> animateCallControls(true, 0));
binding.callStates.callStateRelativeLayout.setOnClickListener(l -> {
- if (currentCallStatus.equals(CallStatus.CALLING_TIMEOUT)) {
+ if (currentCallStatus == CallStatus.CALLING_TIMEOUT) {
setCallState(CallStatus.RECONNECTING);
hangupNetworkCalls(false);
}
@@ -746,7 +747,7 @@ public class CallActivity extends CallBaseActivity {
}
private boolean isConnectionEstablished() {
- return (currentCallStatus.equals(CallStatus.JOINED) || currentCallStatus.equals(CallStatus.IN_CONVERSATION));
+ return (currentCallStatus == CallStatus.JOINED || currentCallStatus == CallStatus.IN_CONVERSATION);
}
@AfterPermissionGranted(100)
@@ -837,9 +838,9 @@ public class CallActivity extends CallBaseActivity {
Log.d(TAG, "onAudioManagerDevicesChanged: " + availableDevices + ", "
+ "currentDevice: " + currentDevice);
- final boolean shouldDisableProximityLock = (currentDevice.equals(WebRtcAudioManager.AudioDevice.WIRED_HEADSET)
- || currentDevice.equals(WebRtcAudioManager.AudioDevice.SPEAKER_PHONE)
- || currentDevice.equals(WebRtcAudioManager.AudioDevice.BLUETOOTH));
+ final boolean shouldDisableProximityLock = (currentDevice == WebRtcAudioManager.AudioDevice.WIRED_HEADSET
+ || currentDevice == WebRtcAudioManager.AudioDevice.SPEAKER_PHONE
+ || currentDevice == WebRtcAudioManager.AudioDevice.BLUETOOTH);
if (shouldDisableProximityLock) {
powerManagerUtils.updatePhoneState(PowerManagerUtils.PhoneState.WITHOUT_PROXIMITY_SENSOR_LOCK);
@@ -1229,7 +1230,7 @@ public class CallActivity extends CallBaseActivity {
Log.d(TAG, "localStream is null");
}
- if (!currentCallStatus.equals(CallStatus.LEAVING)) {
+ if (currentCallStatus != CallStatus.LEAVING) {
hangup(true);
}
powerManagerUtils.updatePhoneState(PowerManagerUtils.PhoneState.IDLE);
@@ -1456,7 +1457,7 @@ public class CallActivity extends CallBaseActivity {
@Override
public void onNext(@io.reactivex.annotations.NonNull GenericOverall genericOverall) {
- if (!currentCallStatus.equals(CallStatus.LEAVING)) {
+ if (currentCallStatus != CallStatus.LEAVING) {
setCallState(CallStatus.JOINED);
ApplicationWideCurrentRoomHolder.getInstance().setInCall(true);
@@ -1472,6 +1473,8 @@ public class CallActivity extends CallBaseActivity {
int apiVersion = ApiUtils.getSignalingApiVersion(conversationUser,
new int[]{ApiUtils.APIv3, 2, 1});
+ AtomicInteger delayOnError = new AtomicInteger(0);
+
ncApi.pullSignalingMessages(credentials,
ApiUtils.getUrlForSignaling(apiVersion,
baseUrl,
@@ -1480,7 +1483,22 @@ public class CallActivity extends CallBaseActivity {
.observeOn(AndroidSchedulers.mainThread())
.repeatWhen(observable -> observable)
.takeWhile(observable -> isConnectionEstablished())
- .retry(3, observable -> isConnectionEstablished())
+ .doOnNext(value -> delayOnError.set(0))
+ .retryWhen(errors -> errors
+ .flatMap(error -> {
+ if (!isConnectionEstablished()) {
+ return Observable.error(error);
+ }
+
+ if (delayOnError.get() == 0) {
+ delayOnError.set(1);
+ } else if (delayOnError.get() < 16) {
+ delayOnError.set(delayOnError.get() * 2);
+ }
+
+ return Observable.timer(delayOnError.get(), TimeUnit.SECONDS);
+ })
+ )
.subscribe(new Observer() {
@Override
public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
@@ -1531,7 +1549,7 @@ public class CallActivity extends CallBaseActivity {
conversationUser, externalSignalingServer.getExternalSignalingTicket(),
TextUtils.isEmpty(credentials));
} else {
- if (webSocketClient.isConnected() && currentCallStatus.equals(CallStatus.PUBLISHER_FAILED)) {
+ if (webSocketClient.isConnected() && currentCallStatus == CallStatus.PUBLISHER_FAILED) {
webSocketClient.restartWebSocket();
}
}
@@ -1549,7 +1567,7 @@ public class CallActivity extends CallBaseActivity {
@Subscribe(threadMode = ThreadMode.BACKGROUND)
public void onMessageEvent(WebSocketCommunicationEvent webSocketCommunicationEvent) {
- if (CallStatus.LEAVING.equals(currentCallStatus)) {
+ if (currentCallStatus == CallStatus.LEAVING) {
return;
}
@@ -1557,7 +1575,7 @@ public class CallActivity extends CallBaseActivity {
case "hello":
Log.d(TAG, "onMessageEvent 'hello'");
if (!webSocketCommunicationEvent.getHashMap().containsKey("oldResumeId")) {
- if (currentCallStatus.equals(CallStatus.RECONNECTING)) {
+ if (currentCallStatus == CallStatus.RECONNECTING) {
hangup(false);
} else {
initiateCall();
@@ -1642,7 +1660,7 @@ public class CallActivity extends CallBaseActivity {
private void receivedSignalingMessage(Signaling signaling) throws IOException {
String messageType = signaling.getType();
- if (!isConnectionEstablished() && !currentCallStatus.equals(CallStatus.CONNECTING)) {
+ if (!isConnectionEstablished() && currentCallStatus != CallStatus.CONNECTING) {
return;
}
@@ -1871,7 +1889,7 @@ public class CallActivity extends CallBaseActivity {
userIdsBySessionId.put(participant.get("sessionId").toString(), userId);
} else {
Log.d(TAG, " inCallFlag of currentSessionId: " + inCallFlag);
- if (inCallFlag == 0 && !CallStatus.LEAVING.equals(currentCallStatus) && ApplicationWideCurrentRoomHolder.getInstance().isInCall()) {
+ if (inCallFlag == 0 && currentCallStatus != CallStatus.LEAVING && ApplicationWideCurrentRoomHolder.getInstance().isInCall()) {
Log.d(TAG, "Most probably a moderator ended the call for all.");
hangup(true);
}
@@ -1891,7 +1909,7 @@ public class CallActivity extends CallBaseActivity {
// Calculate sessions that join the call
newSessions.removeAll(oldSessions);
- if (!isConnectionEstablished() && !currentCallStatus.equals(CallStatus.CONNECTING)) {
+ if (!isConnectionEstablished() && currentCallStatus != CallStatus.CONNECTING) {
return;
}
@@ -1920,7 +1938,7 @@ public class CallActivity extends CallBaseActivity {
});
}
- if (newSessions.size() > 0 && !currentCallStatus.equals(CallStatus.IN_CONVERSATION)) {
+ if (newSessions.size() > 0 && currentCallStatus != CallStatus.IN_CONVERSATION) {
setCallState(CallStatus.IN_CONVERSATION);
}
@@ -2069,8 +2087,9 @@ public class CallActivity extends CallBaseActivity {
if (!(peerConnectionWrappers = getPeerConnectionWrapperListForSessionId(sessionId)).isEmpty()) {
for (PeerConnectionWrapper peerConnectionWrapper : peerConnectionWrappers) {
if (peerConnectionWrapper.getSessionId().equals(sessionId)) {
- if (VIDEO_STREAM_TYPE_SCREEN.equals(peerConnectionWrapper.getVideoStreamType()) || !justScreen) {
- runOnUiThread(() -> removeMediaStream(sessionId));
+ String videoStreamType = peerConnectionWrapper.getVideoStreamType();
+ if (VIDEO_STREAM_TYPE_SCREEN.equals(videoStreamType) || !justScreen) {
+ runOnUiThread(() -> removeMediaStream(sessionId, videoStreamType));
deletePeerConnection(peerConnectionWrapper);
}
}
@@ -2078,9 +2097,9 @@ public class CallActivity extends CallBaseActivity {
}
}
- private void removeMediaStream(String sessionId) {
+ private void removeMediaStream(String sessionId, String videoStreamType) {
Log.d(TAG, "removeMediaStream");
- participantDisplayItems.remove(sessionId);
+ participantDisplayItems.remove(sessionId + "-" + videoStreamType);
if (!isDestroyed()) {
initGridAdapter();
@@ -2145,21 +2164,22 @@ public class CallActivity extends CallBaseActivity {
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMessageEvent(PeerConnectionEvent peerConnectionEvent) {
String sessionId = peerConnectionEvent.getSessionId();
+ String participantDisplayItemId = sessionId + "-" + peerConnectionEvent.getVideoStreamType();
if (peerConnectionEvent.getPeerConnectionEventType() ==
PeerConnectionEvent.PeerConnectionEventType.PEER_CONNECTED) {
- if (webSocketClient != null && webSocketClient.getSessionId() == sessionId) {
+ if (webSocketClient != null && webSocketClient.getSessionId() != null && webSocketClient.getSessionId().equals(sessionId)) {
updateSelfVideoViewConnected(true);
- } else if (participantDisplayItems.get(sessionId) != null) {
- participantDisplayItems.get(sessionId).setConnected(true);
+ } else if (participantDisplayItems.get(participantDisplayItemId) != null) {
+ participantDisplayItems.get(participantDisplayItemId).setConnected(true);
participantsAdapter.notifyDataSetChanged();
}
} else if (peerConnectionEvent.getPeerConnectionEventType() ==
PeerConnectionEvent.PeerConnectionEventType.PEER_DISCONNECTED) {
- if (webSocketClient != null && webSocketClient.getSessionId() == sessionId) {
+ if (webSocketClient != null && webSocketClient.getSessionId() != null && webSocketClient.getSessionId().equals(sessionId)) {
updateSelfVideoViewConnected(false);
- } else if (participantDisplayItems.get(sessionId) != null) {
- participantDisplayItems.get(sessionId).setConnected(false);
+ } else if (participantDisplayItems.get(participantDisplayItemId) != null) {
+ participantDisplayItems.get(participantDisplayItemId).setConnected(false);
participantsAdapter.notifyDataSetChanged();
}
} else if (peerConnectionEvent.getPeerConnectionEventType() ==
@@ -2174,27 +2194,27 @@ public class CallActivity extends CallBaseActivity {
boolean enableVideo = peerConnectionEvent.getPeerConnectionEventType() ==
PeerConnectionEvent.PeerConnectionEventType.SENSOR_FAR && videoOn;
if (EffortlessPermissions.hasPermissions(this, PERMISSIONS_CAMERA) &&
- (currentCallStatus.equals(CallStatus.CONNECTING) || isConnectionEstablished()) && videoOn
+ (currentCallStatus == CallStatus.CONNECTING || isConnectionEstablished()) && videoOn
&& enableVideo != localVideoTrack.enabled()) {
toggleMedia(enableVideo, true);
}
}
} else if (peerConnectionEvent.getPeerConnectionEventType() ==
PeerConnectionEvent.PeerConnectionEventType.NICK_CHANGE) {
- if (participantDisplayItems.get(sessionId) != null) {
- participantDisplayItems.get(sessionId).setNick(peerConnectionEvent.getNick());
+ if (participantDisplayItems.get(participantDisplayItemId) != null) {
+ participantDisplayItems.get(participantDisplayItemId).setNick(peerConnectionEvent.getNick());
participantsAdapter.notifyDataSetChanged();
}
} else if (peerConnectionEvent.getPeerConnectionEventType() ==
PeerConnectionEvent.PeerConnectionEventType.VIDEO_CHANGE && !isVoiceOnlyCall) {
- if (participantDisplayItems.get(sessionId) != null) {
- participantDisplayItems.get(sessionId).setStreamEnabled(peerConnectionEvent.getChangeValue());
+ if (participantDisplayItems.get(participantDisplayItemId) != null) {
+ participantDisplayItems.get(participantDisplayItemId).setStreamEnabled(peerConnectionEvent.getChangeValue());
participantsAdapter.notifyDataSetChanged();
}
} else if (peerConnectionEvent.getPeerConnectionEventType() ==
PeerConnectionEvent.PeerConnectionEventType.AUDIO_CHANGE) {
- if (participantDisplayItems.get(sessionId) != null) {
- participantDisplayItems.get(sessionId).setAudioEnabled(peerConnectionEvent.getChangeValue());
+ if (participantDisplayItems.get(participantDisplayItemId) != null) {
+ participantDisplayItems.get(participantDisplayItemId).setAudioEnabled(peerConnectionEvent.getChangeValue());
participantsAdapter.notifyDataSetChanged();
}
} else if (peerConnectionEvent.getPeerConnectionEventType() ==
@@ -2382,33 +2402,22 @@ public class CallActivity extends CallBaseActivity {
}
}
- String urlForAvatar;
- if (!TextUtils.isEmpty(userId4Usage)) {
- urlForAvatar = ApiUtils.getUrlForAvatar(baseUrl,
- userId4Usage,
- true);
- } else {
- urlForAvatar = ApiUtils.getUrlForGuestAvatar(baseUrl,
- nick,
- true);
- }
-
- ParticipantDisplayItem participantDisplayItem = new ParticipantDisplayItem(userId4Usage,
+ ParticipantDisplayItem participantDisplayItem = new ParticipantDisplayItem(baseUrl,
+ userId4Usage,
session,
connected,
nick,
- urlForAvatar,
mediaStream,
videoStreamType,
videoStreamEnabled,
rootEglBase);
- participantDisplayItems.put(session, participantDisplayItem);
+ participantDisplayItems.put(session + "-" + videoStreamType, participantDisplayItem);
initGridAdapter();
}
private void setCallState(CallStatus callState) {
- if (currentCallStatus == null || !currentCallStatus.equals(callState)) {
+ if (currentCallStatus == null || currentCallStatus != callState) {
currentCallStatus = callState;
if (handler == null) {
handler = new Handler(Looper.getMainLooper());
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..3d30e3a0c
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/activities/CallNotificationActivity.kt
@@ -0,0 +1,476 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe
+ * 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)
+ }
+
+ @Suppress("MagicNumber")
+ private fun handleFromNotification() {
+ val apiVersion = ApiUtils.getConversationApiVersion(
+ userBeingCalled,
+ intArrayOf(
+ ApiUtils.APIv4,
+ ApiUtils.APIv3, 1
+ )
+ )
+ ncApi!!.getRoom(credentials, ApiUtils.getUrlForRoom(apiVersion, userBeingCalled!!.baseUrl, roomToken))
+ .subscribeOn(Schedulers.io())
+ .retry(GET_ROOM_RETRY_COUNT)
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(object : Observer {
+ override fun onSubscribe(d: Disposable) {
+ disposablesList.add(d)
+ }
+
+ override fun onNext(roomOverall: RoomOverall) {
+ currentConversation = roomOverall.ocs!!.data
+ setUpAfterConversationIsKnown()
+ if (apiVersion >= 3) {
+ val hasCallFlags = hasSpreedFeatureCapability(
+ userBeingCalled,
+ "conversation-call-flags"
+ )
+ if (hasCallFlags) {
+ if (isInCallWithVideo(currentConversation!!.callFlag)) {
+ binding!!.incomingCallVoiceOrVideoTextView.text = String.format(
+ resources.getString(R.string.nc_call_video),
+ resources.getString(R.string.nc_app_product_name)
+ )
+ } else {
+ binding!!.incomingCallVoiceOrVideoTextView.text = String.format(
+ resources.getString(R.string.nc_call_voice),
+ resources.getString(R.string.nc_app_product_name)
+ )
+ }
+ }
+ }
+ }
+
+ override fun onError(e: Throwable) {
+ Log.e(TAG, e.message, e)
+ }
+
+ override fun onComplete() {
+ // unused atm
+ }
+ })
+ }
+
+ private fun isInCallWithVideo(callFlag: Int): Boolean {
+ return callFlag >= Participant.InCallFlags.IN_CALL + Participant.InCallFlags.WITH_VIDEO
+ }
+
+ private fun setUpAfterConversationIsKnown() {
+ binding!!.conversationNameTextView.text = currentConversation!!.displayName
+ if (currentConversation!!.type === Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) {
+ setAvatarForOneToOneCall()
+ } else {
+ binding!!.avatarImageView.setImageResource(R.drawable.ic_circular_group)
+ }
+ checkIfAnyParticipantsRemainInRoom()
+ showAnswerControls()
+ }
+
+ @Suppress("MagicNumber")
+ private fun setAvatarForOneToOneCall() {
+ val imageRequest = DisplayUtils.getImageRequestForUrl(
+ ApiUtils.getUrlForAvatar(
+ userBeingCalled!!.baseUrl,
+ currentConversation!!.name,
+ true
+ )
+ )
+ val imagePipeline = Fresco.getImagePipeline()
+ val dataSource = imagePipeline.fetchDecodedImage(imageRequest, null)
+ dataSource.subscribe(
+ object : BaseBitmapDataSubscriber() {
+ override fun onNewResultImpl(bitmap: Bitmap?) {
+ binding!!.avatarImageView.hierarchy.setImage(
+ BitmapDrawable(resources, bitmap), 100f,
+ true
+ )
+ }
+
+ override fun onFailureImpl(dataSource: DataSource>) {
+ 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/activities/FullScreenImageActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/FullScreenImageActivity.kt
index b993e5ee6..5bdd38f25 100644
--- a/app/src/main/java/com/nextcloud/talk/activities/FullScreenImageActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/activities/FullScreenImageActivity.kt
@@ -46,7 +46,7 @@ class FullScreenImageActivity : AppCompatActivity() {
private lateinit var path: String
private var showFullscreen = false
- override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_preview, menu)
return true
}
diff --git a/app/src/main/java/com/nextcloud/talk/activities/FullScreenMediaActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/FullScreenMediaActivity.kt
index cfadded62..2a076065d 100644
--- a/app/src/main/java/com/nextcloud/talk/activities/FullScreenMediaActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/activities/FullScreenMediaActivity.kt
@@ -49,7 +49,7 @@ class FullScreenMediaActivity : AppCompatActivity(), Player.Listener {
private lateinit var path: String
private lateinit var player: SimpleExoPlayer
- override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_preview, menu)
return true
}
diff --git a/app/src/main/java/com/nextcloud/talk/activities/FullScreenTextViewerActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/FullScreenTextViewerActivity.kt
index 5bf306c1c..e0dd0d35d 100644
--- a/app/src/main/java/com/nextcloud/talk/activities/FullScreenTextViewerActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/activities/FullScreenTextViewerActivity.kt
@@ -50,7 +50,7 @@ class FullScreenTextViewerActivity : AppCompatActivity() {
private lateinit var path: String
- override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_preview, menu)
return true
}
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.java b/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.java
index adc75a338..3ed18bf76 100644
--- a/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.java
+++ b/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.java
@@ -1,9 +1,14 @@
package com.nextcloud.talk.adapters;
+import android.text.TextUtils;
+
+import com.nextcloud.talk.utils.ApiUtils;
+
import org.webrtc.EglBase;
import org.webrtc.MediaStream;
public class ParticipantDisplayItem {
+ private String baseUrl;
private String userId;
private String session;
private boolean connected;
@@ -15,16 +20,18 @@ public class ParticipantDisplayItem {
private EglBase rootEglBase;
private boolean isAudioEnabled;
- public ParticipantDisplayItem(String userId, String session, boolean connected, String nick, String urlForAvatar, MediaStream mediaStream, String streamType, boolean streamEnabled, EglBase rootEglBase) {
+ public ParticipantDisplayItem(String baseUrl, String userId, String session, boolean connected, String nick, MediaStream mediaStream, String streamType, boolean streamEnabled, EglBase rootEglBase) {
+ this.baseUrl = baseUrl;
this.userId = userId;
this.session = session;
this.connected = connected;
this.nick = nick;
- this.urlForAvatar = urlForAvatar;
this.mediaStream = mediaStream;
this.streamType = streamType;
this.streamEnabled = streamEnabled;
this.rootEglBase = rootEglBase;
+
+ this.updateUrlForAvatar();
}
public String getUserId() {
@@ -33,6 +40,8 @@ public class ParticipantDisplayItem {
public void setUserId(String userId) {
this.userId = userId;
+
+ this.updateUrlForAvatar();
}
public String getSession() {
@@ -57,14 +66,20 @@ public class ParticipantDisplayItem {
public void setNick(String nick) {
this.nick = nick;
+
+ this.updateUrlForAvatar();
}
public String getUrlForAvatar() {
return urlForAvatar;
}
- public void setUrlForAvatar(String urlForAvatar) {
- this.urlForAvatar = urlForAvatar;
+ private void updateUrlForAvatar() {
+ if (!TextUtils.isEmpty(userId)) {
+ urlForAvatar = ApiUtils.getUrlForAvatar(baseUrl, userId, true);
+ } else {
+ urlForAvatar = ApiUtils.getUrlForGuestAvatar(baseUrl, nick, true);
+ }
}
public MediaStream getMediaStream() {
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/ContactItem.java b/app/src/main/java/com/nextcloud/talk/adapters/items/ContactItem.java
index a785def5e..6ebb30e2e 100644
--- a/app/src/main/java/com/nextcloud/talk/adapters/items/ContactItem.java
+++ b/app/src/main/java/com/nextcloud/talk/adapters/items/ContactItem.java
@@ -145,8 +145,8 @@ public class ContactItem extends AbstractFlexibleItem= Build.VERSION_CODES.O) {
Drawable[] layers = new Drawable[2];
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/ParticipantItem.java b/app/src/main/java/com/nextcloud/talk/adapters/items/ParticipantItem.java
index eb440001f..ea676457b 100644
--- a/app/src/main/java/com/nextcloud/talk/adapters/items/ParticipantItem.java
+++ b/app/src/main/java/com/nextcloud/talk/adapters/items/ParticipantItem.java
@@ -140,8 +140,8 @@ public class ParticipantItem extends AbstractFlexibleItem startActivityForResult(intent, REQUEST_CODE_IMAGE_PICKER) }
}
private fun showBrowserScreen() {
@@ -584,21 +584,21 @@ class ProfileController : BaseController(R.layout.controller_profile) {
}
private fun openImageWithPicker(file: File) {
- val intent = with(activity!!)
- .fileOnly()
+ with(activity!!)
+ .provider(ImageProvider.URI)
.crop()
.cropSquare()
.compress(MAX_SIZE)
.maxResultSize(MAX_SIZE, MAX_SIZE)
- .prepareIntent()
- intent.putExtra("extra.file", file)
- startActivityForResult(intent, REQUEST_CODE_IMAGE_PICKER)
+ .setUri(Uri.fromFile(file))
+ .createIntent { intent -> startActivityForResult(intent, REQUEST_CODE_IMAGE_PICKER) }
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK) {
if (requestCode == REQUEST_CODE_IMAGE_PICKER) {
- uploadAvatar(getFile(data))
+ val uri: Uri = data?.data!!
+ uploadAvatar(uri.toFile())
} else if (requestCode == REQUEST_CODE_SELECT_REMOTE_FILES) {
val pathList = data?.getStringArrayListExtra(RemoteFileBrowserActivity.EXTRA_SELECTED_PATHS)
if (pathList?.size!! >= 1) {
diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java b/app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java
index 71904e43a..06b91d670 100644
--- a/app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java
+++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java
@@ -299,7 +299,7 @@ public class RestModule {
@Override
public void run() {
- if (Proxy.Type.SOCKS.equals(Proxy.Type.valueOf(appPreferences.getProxyType()))) {
+ if (Proxy.Type.valueOf(appPreferences.getProxyType()) == Proxy.Type.SOCKS) {
proxy = new Proxy(Proxy.Type.valueOf(appPreferences.getProxyType()),
InetSocketAddress.createUnresolved(appPreferences.getProxyHost(), Integer.parseInt(
appPreferences.getProxyPort())));
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..2d7dc11b7
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt
@@ -0,0 +1,849 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Andy Scherzinger
+ * @author Mario Danic
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe
+ * 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.R
+import com.nextcloud.talk.activities.CallNotificationActivity
+import com.nextcloud.talk.activities.MainActivity
+import com.nextcloud.talk.api.NcApi
+import com.nextcloud.talk.application.NextcloudTalkApplication
+import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
+import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager
+import com.nextcloud.talk.models.SignatureVerification
+import com.nextcloud.talk.models.json.chat.ChatUtils.Companion.getParsedMessage
+import com.nextcloud.talk.models.json.conversations.RoomOverall
+import com.nextcloud.talk.models.json.notifications.NotificationOverall
+import com.nextcloud.talk.models.json.participants.Participant
+import com.nextcloud.talk.models.json.participants.ParticipantsOverall
+import com.nextcloud.talk.models.json.push.DecryptedPushMessage
+import com.nextcloud.talk.models.json.push.NotificationUser
+import com.nextcloud.talk.receivers.DirectReplyReceiver
+import com.nextcloud.talk.receivers.MarkAsReadReceiver
+import com.nextcloud.talk.utils.ApiUtils
+import com.nextcloud.talk.utils.DoNotDisturbUtils.shouldPlaySound
+import com.nextcloud.talk.utils.NotificationUtils
+import com.nextcloud.talk.utils.NotificationUtils.cancelAllNotificationsForAccount
+import com.nextcloud.talk.utils.NotificationUtils.cancelNotification
+import com.nextcloud.talk.utils.NotificationUtils.findNotificationForRoom
+import com.nextcloud.talk.utils.NotificationUtils.getCallRingtoneUri
+import com.nextcloud.talk.utils.NotificationUtils.getMessageRingtoneUri
+import com.nextcloud.talk.utils.NotificationUtils.loadAvatarSync
+import com.nextcloud.talk.utils.PushUtils
+import com.nextcloud.talk.utils.bundle.BundleKeys
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FROM_NOTIFICATION_START_CALL
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_MESSAGE_ID
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_NOTIFICATION_ID
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SYSTEM_NOTIFICATION_ID
+import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY
+import com.nextcloud.talk.utils.preferences.AppPreferences
+import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder
+import io.reactivex.Observable
+import io.reactivex.Observer
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.Disposable
+import io.reactivex.schedulers.Schedulers
+import okhttp3.JavaNetCookieJar
+import okhttp3.OkHttpClient
+import retrofit2.Retrofit
+import java.io.IOException
+import java.net.CookieManager
+import java.security.InvalidKeyException
+import java.security.NoSuchAlgorithmException
+import java.security.PrivateKey
+import java.util.concurrent.TimeUnit
+import java.util.function.Consumer
+import java.util.zip.CRC32
+import javax.crypto.Cipher
+import javax.crypto.NoSuchPaddingException
+import javax.inject.Inject
+
+@AutoInjector(NextcloudTalkApplication::class)
+class NotificationWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
+
+ @Inject
+ lateinit var appPreferences: AppPreferences
+
+ @JvmField
+ @Inject
+ var arbitraryStorageManager: ArbitraryStorageManager? = null
+
+ @JvmField
+ @Inject
+ var retrofit: Retrofit? = null
+
+ @JvmField
+ @Inject
+ var okHttpClient: OkHttpClient? = null
+ private lateinit var credentials: String
+ private lateinit var ncApi: NcApi
+ private lateinit var pushMessage: DecryptedPushMessage
+ private lateinit var signatureVerification: SignatureVerification
+ private var context: Context? = null
+ private var conversationType: String? = "one2one"
+ private var muteCall = false
+ private var importantConversation = false
+ private lateinit var notificationManager: NotificationManagerCompat
+
+ override fun doWork(): Result {
+ sharedApplication!!.componentApplication.inject(this)
+ context = applicationContext
+
+ initDecryptedData(inputData)
+ initNcApiAndCredentials()
+
+ notificationManager = NotificationManagerCompat.from(context!!)
+
+ pushMessage.timestamp = System.currentTimeMillis()
+
+ Log.d(TAG, pushMessage.toString())
+ Log.d(TAG, "pushMessage.id (=KEY_ROOM_TOKEN): " + pushMessage.id)
+ Log.d(TAG, "pushMessage.notificationId: " + pushMessage.notificationId)
+ Log.d(TAG, "pushMessage.notificationIds: " + pushMessage.notificationIds)
+ Log.d(TAG, "pushMessage.timestamp: " + pushMessage.timestamp)
+
+ if (pushMessage.delete) {
+ cancelNotification(context, signatureVerification.user!!, pushMessage.notificationId)
+ } else if (pushMessage.deleteAll) {
+ cancelAllNotificationsForAccount(context, signatureVerification.user!!)
+ } else if (pushMessage.deleteMultiple) {
+ for (notificationId in pushMessage.notificationIds!!) {
+ cancelNotification(context, signatureVerification.user!!, notificationId)
+ }
+ } else if (isSpreedNotification()) {
+ Log.d(TAG, "pushMessage.type: " + pushMessage.type)
+ when (pushMessage.type) {
+ "chat" -> handleChatNotification()
+ "room" -> handleRoomNotification()
+ "call" -> handleCallNotification()
+ else -> Log.e(TAG, "unknown pushMessage.type")
+ }
+ } else {
+ Log.d(TAG, "a pushMessage that is not for spreed was received.")
+ }
+
+ return Result.success()
+ }
+
+ private fun handleChatNotification() {
+ val chatIntent = Intent(context, MainActivity::class.java)
+ chatIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
+ val chatBundle = Bundle()
+ chatBundle.putString(KEY_ROOM_TOKEN, pushMessage.id)
+ chatBundle.putParcelable(KEY_USER_ENTITY, signatureVerification.user)
+ chatBundle.putBoolean(KEY_FROM_NOTIFICATION_START_CALL, false)
+ chatIntent.putExtras(chatBundle)
+ if (pushMessage.notificationId != Long.MIN_VALUE) {
+ showNotificationWithObjectData(chatIntent)
+ } else {
+ showNotification(chatIntent)
+ }
+ }
+
+ /**
+ * handle messages with type 'room', e.g. "xxx invited you to a group conversation"
+ */
+ private fun handleRoomNotification() {
+ val intent = Intent(context, MainActivity::class.java)
+ intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
+ val bundle = Bundle()
+ bundle.putString(KEY_ROOM_TOKEN, pushMessage.id)
+ bundle.putParcelable(KEY_USER_ENTITY, signatureVerification.user)
+ bundle.putBoolean(KEY_FROM_NOTIFICATION_START_CALL, false)
+ intent.putExtras(bundle)
+ if (bundle.containsKey(KEY_ROOM_TOKEN)) {
+ showNotificationWithObjectData(intent)
+ }
+ }
+
+ private fun handleCallNotification() {
+ val fullScreenIntent = Intent(context, CallNotificationActivity::class.java)
+ val bundle = Bundle()
+ bundle.putString(KEY_ROOM_TOKEN, pushMessage.id)
+ bundle.putParcelable(KEY_USER_ENTITY, signatureVerification.user)
+ bundle.putBoolean(KEY_FROM_NOTIFICATION_START_CALL, true)
+ fullScreenIntent.putExtras(bundle)
+ fullScreenIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
+
+ val requestCode = System.currentTimeMillis().toInt()
+
+ val fullScreenPendingIntent = PendingIntent.getActivity(
+ context,
+ requestCode,
+ fullScreenIntent,
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ } else {
+ PendingIntent.FLAG_UPDATE_CURRENT
+ }
+ )
+
+ val soundUri = getCallRingtoneUri(applicationContext, appPreferences)
+ val notificationChannelId = NotificationUtils
+ .NotificationChannels.NOTIFICATION_CHANNEL_CALLS_V4.name
+ val uri = Uri.parse(signatureVerification.user!!.baseUrl)
+ val baseUrl = uri.host
+
+ val notification =
+ NotificationCompat.Builder(applicationContext, notificationChannelId)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setCategory(NotificationCompat.CATEGORY_CALL)
+ .setSmallIcon(R.drawable.ic_call_black_24dp)
+ .setSubText(baseUrl)
+ .setShowWhen(true)
+ .setWhen(pushMessage.timestamp)
+ .setContentTitle(EmojiCompat.get().process(pushMessage.subject))
+ .setAutoCancel(true)
+ .setOngoing(true)
+ .setContentIntent(fullScreenPendingIntent)
+ .setFullScreenIntent(fullScreenPendingIntent, true)
+ .setSound(soundUri)
+ .build()
+ notification.flags = notification.flags or Notification.FLAG_INSISTENT
+
+ sendNotification(pushMessage.timestamp.toInt(), notification)
+
+ checkIfCallIsActive(signatureVerification, pushMessage)
+ }
+
+ private fun initNcApiAndCredentials() {
+ credentials = ApiUtils.getCredentials(
+ signatureVerification.user!!.username,
+ signatureVerification.user!!.token
+ )
+ ncApi = retrofit!!.newBuilder().client(
+ okHttpClient!!.newBuilder().cookieJar(
+ JavaNetCookieJar(
+ CookieManager()
+ )
+ ).build()
+ ).build().create(
+ NcApi::class.java
+ )
+ }
+
+ @Suppress("TooGenericExceptionCaught", "NestedBlockDepth", "ComplexMethod", "LongMethod")
+ private fun initDecryptedData(inputData: Data) {
+ val subject = inputData.getString(BundleKeys.KEY_NOTIFICATION_SUBJECT)
+ val signature = inputData.getString(BundleKeys.KEY_NOTIFICATION_SIGNATURE)
+ try {
+ val base64DecodedSubject = Base64.decode(subject, Base64.DEFAULT)
+ val base64DecodedSignature = Base64.decode(signature, Base64.DEFAULT)
+ val pushUtils = PushUtils()
+ val privateKey = pushUtils.readKeyFromFile(false) as PrivateKey
+ try {
+ signatureVerification = pushUtils.verifySignature(
+ base64DecodedSignature,
+ base64DecodedSubject
+ )
+ if (signatureVerification.signatureValid) {
+ val cipher = Cipher.getInstance("RSA/None/PKCS1Padding")
+ cipher.init(Cipher.DECRYPT_MODE, privateKey)
+ val decryptedSubject = cipher.doFinal(base64DecodedSubject)
+
+ pushMessage = LoganSquare.parse(
+ String(decryptedSubject),
+ DecryptedPushMessage::class.java
+ )
+ }
+ } catch (e: NoSuchAlgorithmException) {
+ Log.e(TAG, "No proper algorithm to decrypt the message ", e)
+ } catch (e: NoSuchPaddingException) {
+ Log.e(TAG, "No proper padding to decrypt the message ", e)
+ } catch (e: InvalidKeyException) {
+ Log.e(TAG, "Invalid private key ", e)
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Error occurred while initializing decoded data ", e)
+ }
+ }
+
+ private fun isSpreedNotification() = SPREED_APP == pushMessage.app
+
+ private fun showNotificationWithObjectData(intent: Intent) {
+ val user = signatureVerification.user
+
+ // see https://github.com/nextcloud/notifications/blob/master/docs/ocs-endpoint-v2.md
+ ncApi.getNotification(
+ credentials,
+ ApiUtils.getUrlForNotificationWithId(
+ user!!.baseUrl,
+ (pushMessage.notificationId!!).toString()
+ )
+ )
+ .blockingSubscribe(object : Observer {
+ override fun onSubscribe(d: Disposable) {
+ // unused atm
+ }
+
+ override fun onNext(notificationOverall: NotificationOverall) {
+ val ncNotification = notificationOverall.ocs!!.notification
+
+ if (ncNotification!!.messageRichParameters != null &&
+ ncNotification.messageRichParameters!!.size > 0
+ ) {
+ pushMessage.text = getParsedMessage(
+ ncNotification.messageRich,
+ ncNotification.messageRichParameters
+ )
+ } else {
+ pushMessage.text = ncNotification.message
+ }
+
+ val subjectRichParameters = ncNotification.subjectRichParameters
+
+ pushMessage.timestamp = ncNotification.datetime!!.millis
+
+ if (subjectRichParameters != null && subjectRichParameters.size > 0) {
+ val callHashMap = subjectRichParameters["call"]
+ val userHashMap = subjectRichParameters["user"]
+ val guestHashMap = subjectRichParameters["guest"]
+ if (callHashMap != null && callHashMap.size > 0 && callHashMap.containsKey("name")) {
+ if (subjectRichParameters.containsKey("reaction")) {
+ pushMessage.subject = ""
+ pushMessage.text = ncNotification.subject
+ } else if (ncNotification.objectType == "chat") {
+ pushMessage.subject = callHashMap["name"]!!
+ } else {
+ pushMessage.subject = ncNotification.subject!!
+ }
+ if (callHashMap.containsKey("call-type")) {
+ conversationType = callHashMap["call-type"]
+ }
+ }
+ val notificationUser = NotificationUser()
+ if (userHashMap != null && userHashMap.isNotEmpty()) {
+ notificationUser.id = userHashMap["id"]
+ notificationUser.type = userHashMap["type"]
+ notificationUser.name = userHashMap["name"]
+ pushMessage.notificationUser = notificationUser
+ } else if (guestHashMap != null && guestHashMap.isNotEmpty()) {
+ notificationUser.id = guestHashMap["id"]
+ notificationUser.type = guestHashMap["type"]
+ notificationUser.name = guestHashMap["name"]
+ pushMessage.notificationUser = notificationUser
+ }
+ }
+ pushMessage.objectId = ncNotification.objectId
+ showNotification(intent)
+ }
+
+ override fun onError(e: Throwable) {
+ // unused atm
+ }
+
+ override fun onComplete() {
+ // unused atm
+ }
+ })
+ }
+
+ @Suppress("MagicNumber")
+ private fun showNotification(intent: Intent) {
+ val largeIcon: Bitmap
+ val priority = NotificationCompat.PRIORITY_HIGH
+ val smallIcon: Int = R.drawable.ic_logo
+ val category: String = if (CHAT == pushMessage.type || ROOM == pushMessage.type) {
+ Notification.CATEGORY_MESSAGE
+ } else {
+ Notification.CATEGORY_CALL
+ }
+ when (conversationType) {
+ "one2one" -> {
+ pushMessage.subject = ""
+ largeIcon = ContextCompat.getDrawable(context!!, R.drawable.ic_people_group_black_24px)?.toBitmap()!!
+ }
+ "group" ->
+ largeIcon = ContextCompat.getDrawable(context!!, R.drawable.ic_people_group_black_24px)?.toBitmap()!!
+ "public" -> largeIcon = ContextCompat.getDrawable(context!!, R.drawable.ic_link_black_24px)?.toBitmap()!!
+ else -> // assuming one2one
+ largeIcon = if (CHAT == pushMessage.type || ROOM == pushMessage.type) {
+ ContextCompat.getDrawable(context!!, R.drawable.ic_comment)?.toBitmap()!!
+ } else {
+ ContextCompat.getDrawable(context!!, R.drawable.ic_call_black_24dp)?.toBitmap()!!
+ }
+ }
+
+ // Use unique request code to make sure that a new PendingIntent gets created for each notification
+ // See https://github.com/nextcloud/talk-android/issues/2111
+ val requestCode = System.currentTimeMillis().toInt()
+ val intentFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ PendingIntent.FLAG_MUTABLE
+ } else {
+ 0
+ }
+ val pendingIntent = PendingIntent.getActivity(context, requestCode, intent, intentFlag)
+ val uri = Uri.parse(signatureVerification.user!!.baseUrl)
+ val baseUrl = uri.host
+ val notificationBuilder = NotificationCompat.Builder(context!!, "1")
+ .setLargeIcon(largeIcon)
+ .setSmallIcon(smallIcon)
+ .setCategory(category)
+ .setPriority(priority)
+ .setSubText(baseUrl)
+ .setWhen(pushMessage.timestamp)
+ .setShowWhen(true)
+ .setContentIntent(pendingIntent)
+ .setAutoCancel(true)
+ if (!TextUtils.isEmpty(pushMessage.subject)) {
+ notificationBuilder.setContentTitle(
+ EmojiCompat.get().process(pushMessage.subject)
+ )
+ }
+ if (!TextUtils.isEmpty(pushMessage.text)) {
+ notificationBuilder.setContentText(
+ EmojiCompat.get().process(pushMessage.text!!)
+ )
+ }
+ if (Build.VERSION.SDK_INT >= 23) {
+ // This method should exist since API 21, but some phones don't have it
+ // So as a safeguard, we don't use it until 23
+ notificationBuilder.color = context!!.resources.getColor(R.color.colorPrimary)
+ }
+ val notificationInfoBundle = Bundle()
+ notificationInfoBundle.putLong(KEY_INTERNAL_USER_ID, signatureVerification.user!!.id!!)
+ // could be an ID or a TOKEN
+ notificationInfoBundle.putString(KEY_ROOM_TOKEN, pushMessage.id)
+ notificationInfoBundle.putLong(KEY_NOTIFICATION_ID, pushMessage.notificationId!!)
+ notificationBuilder.setExtras(notificationInfoBundle)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ if (CHAT == pushMessage.type || ROOM == pushMessage.type) {
+ notificationBuilder.setChannelId(
+ NotificationUtils.NotificationChannels.NOTIFICATION_CHANNEL_MESSAGES_V4.name
+ )
+ }
+ } else {
+ // red color for the lights
+ notificationBuilder.setLights(-0x10000, 200, 200)
+ }
+
+ notificationBuilder.setContentIntent(pendingIntent)
+ val groupName = signatureVerification.user!!.id.toString() + "@" + pushMessage.id
+ notificationBuilder.setGroup(calculateCRC32(groupName).toString())
+ val activeStatusBarNotification = findNotificationForRoom(
+ context,
+ signatureVerification.user!!,
+ pushMessage.id!!
+ )
+
+ // NOTE - systemNotificationId is an internal ID used on the device only.
+ // It is NOT the same as the notification ID used in communication with the server.
+ val systemNotificationId: Int =
+ activeStatusBarNotification?.id ?: calculateCRC32(System.currentTimeMillis().toString()).toInt()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && CHAT == pushMessage.type &&
+ pushMessage.notificationUser != null
+ ) {
+ prepareChatNotification(notificationBuilder, activeStatusBarNotification, systemNotificationId)
+ }
+ sendNotification(systemNotificationId, notificationBuilder.build())
+ }
+
+ private fun calculateCRC32(s: String): Long {
+ val crc32 = CRC32()
+ crc32.update(s.toByteArray())
+ return crc32.value
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ private fun prepareChatNotification(
+ notificationBuilder: NotificationCompat.Builder,
+ activeStatusBarNotification: StatusBarNotification?,
+ systemNotificationId: Int
+ ) {
+ val notificationUser = pushMessage.notificationUser
+ val userType = notificationUser!!.type
+ var style: NotificationCompat.MessagingStyle? = null
+ if (activeStatusBarNotification != null) {
+ style = NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(
+ activeStatusBarNotification.notification
+ )
+ }
+ val person = Person.Builder()
+ .setKey(signatureVerification.user!!.id.toString() + "@" + notificationUser.id)
+ .setName(EmojiCompat.get().process(notificationUser.name!!))
+ .setBot("bot" == userType)
+ notificationBuilder.setOnlyAlertOnce(true)
+ addReplyAction(notificationBuilder, systemNotificationId)
+ addMarkAsReadAction(notificationBuilder, systemNotificationId)
+
+ if ("user" == userType || "guest" == userType) {
+ val baseUrl = signatureVerification.user!!.baseUrl
+ val avatarUrl = if ("user" == userType) ApiUtils.getUrlForAvatar(
+ baseUrl,
+ notificationUser.id,
+ false
+ ) else ApiUtils.getUrlForGuestAvatar(baseUrl, notificationUser.name, false)
+ person.setIcon(loadAvatarSync(avatarUrl))
+ }
+ notificationBuilder.setStyle(getStyle(person.build(), style))
+ }
+
+ private fun buildIntentForAction(cls: Class<*>, systemNotificationId: Int, messageId: Int): PendingIntent {
+ val actualIntent = Intent(context, cls)
+
+ // NOTE - systemNotificationId is an internal ID used on the device only.
+ // It is NOT the same as the notification ID used in communication with the server.
+ actualIntent.putExtra(KEY_SYSTEM_NOTIFICATION_ID, systemNotificationId)
+ actualIntent.putExtra(KEY_INTERNAL_USER_ID, signatureVerification.user?.id)
+ actualIntent.putExtra(KEY_ROOM_TOKEN, pushMessage.id)
+ actualIntent.putExtra(KEY_MESSAGE_ID, messageId)
+
+ val intentFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ } else {
+ PendingIntent.FLAG_UPDATE_CURRENT
+ }
+ return PendingIntent.getBroadcast(context, systemNotificationId, actualIntent, intentFlag)
+ }
+
+ private fun addMarkAsReadAction(notificationBuilder: NotificationCompat.Builder, systemNotificationId: Int) {
+ if (pushMessage.objectId != null) {
+ val messageId: Int = try {
+ parseMessageId(pushMessage.objectId!!)
+ } catch (nfe: NumberFormatException) {
+ Log.e(TAG, "Failed to parse messageId from objectId, skip adding mark-as-read action.", nfe)
+ return
+ }
+
+ val pendingIntent = buildIntentForAction(
+ MarkAsReadReceiver::class.java,
+ systemNotificationId,
+ messageId
+ )
+ val action = NotificationCompat.Action.Builder(
+ R.drawable.ic_eye,
+ context!!.resources.getString(R.string.nc_mark_as_read),
+ pendingIntent
+ )
+ .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
+ .setShowsUserInterface(false)
+ .build()
+ notificationBuilder.addAction(action)
+ }
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ private fun addReplyAction(notificationBuilder: NotificationCompat.Builder, systemNotificationId: Int) {
+ val replyLabel = context!!.resources.getString(R.string.nc_reply)
+ val remoteInput = RemoteInput.Builder(NotificationUtils.KEY_DIRECT_REPLY)
+ .setLabel(replyLabel)
+ .build()
+
+ val replyPendingIntent = buildIntentForAction(DirectReplyReceiver::class.java, systemNotificationId, 0)
+ val replyAction = NotificationCompat.Action.Builder(R.drawable.ic_reply, replyLabel, replyPendingIntent)
+ .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
+ .setShowsUserInterface(false)
+ .setAllowGeneratedReplies(true)
+ .addRemoteInput(remoteInput)
+ .build()
+ notificationBuilder.addAction(replyAction)
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ private fun getStyle(person: Person, style: NotificationCompat.MessagingStyle?): NotificationCompat.MessagingStyle {
+ val newStyle = NotificationCompat.MessagingStyle(person)
+ newStyle.conversationTitle = pushMessage.subject
+ newStyle.isGroupConversation = "one2one" != conversationType
+ style?.messages?.forEach(
+ Consumer { message: NotificationCompat.MessagingStyle.Message ->
+ newStyle.addMessage(
+ NotificationCompat.MessagingStyle.Message(
+ message.text,
+ message.timestamp,
+ message.person
+ )
+ )
+ }
+ )
+ newStyle.addMessage(pushMessage.text, pushMessage.timestamp, person)
+ return newStyle
+ }
+
+ @Throws(NumberFormatException::class)
+ private fun parseMessageId(objectId: String): Int {
+ val objectIdParts = objectId.split("/".toRegex()).toTypedArray()
+ return if (objectIdParts.size < 2) {
+ throw NumberFormatException("Invalid objectId, doesn't contain at least one '/'")
+ } else {
+ objectIdParts[1].toInt()
+ }
+ }
+
+ private fun sendNotification(notificationId: Int, notification: Notification) {
+ Log.d(TAG, "show notification with id $notificationId")
+ notificationManager.notify(notificationId, notification)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ // On devices with Android 8.0 (Oreo) or later, notification sound will be handled by the system
+ // if notifications have not been disabled by the user.
+ return
+ }
+ if (Notification.CATEGORY_CALL != notification.category || !muteCall) {
+ val soundUri = getMessageRingtoneUri(context!!, appPreferences)
+ if (soundUri != null && !ApplicationWideCurrentRoomHolder.getInstance().isInCall &&
+ (shouldPlaySound() || importantConversation)
+ ) {
+ val audioAttributesBuilder =
+ AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ if (CHAT == pushMessage.type || ROOM == pushMessage.type) {
+ audioAttributesBuilder.setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT)
+ } else {
+ audioAttributesBuilder.setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST)
+ }
+ val mediaPlayer = MediaPlayer()
+ try {
+ mediaPlayer.setDataSource(context!!, soundUri)
+ mediaPlayer.setAudioAttributes(audioAttributesBuilder.build())
+ mediaPlayer.setOnPreparedListener { mediaPlayer.start() }
+ mediaPlayer.setOnCompletionListener { obj: MediaPlayer -> obj.release() }
+ mediaPlayer.prepareAsync()
+ } catch (e: IOException) {
+ Log.e(TAG, "Failed to set data source")
+ }
+ }
+ }
+ }
+
+ private fun removeNotification(notificationId: Int) {
+ Log.d(TAG, "removed notification with id $notificationId")
+ notificationManager.cancel(notificationId)
+ }
+
+ private fun checkIfCallIsActive(
+ signatureVerification: SignatureVerification,
+ decryptedPushMessage: DecryptedPushMessage
+ ) {
+ Log.d(TAG, "checkIfCallIsActive")
+ var hasParticipantsInCall = true
+ var inCallOnDifferentDevice = false
+
+ val apiVersion = ApiUtils.getConversationApiVersion(
+ signatureVerification.user,
+ intArrayOf(ApiUtils.APIv4, 1)
+ )
+
+ var isCallNotificationVisible = true
+
+ ncApi.getPeersForCall(
+ credentials,
+ ApiUtils.getUrlForCall(
+ apiVersion,
+ signatureVerification.user!!.baseUrl,
+ decryptedPushMessage.id
+ )
+ )
+ .repeatWhen { completed ->
+ completed.zipWith(Observable.range(TIMER_START, TIMER_COUNT)) { _, i -> i }
+ .flatMap { Observable.timer(TIMER_DELAY, TimeUnit.SECONDS) }
+ .takeWhile { isCallNotificationVisible && hasParticipantsInCall && !inCallOnDifferentDevice }
+ }
+ .subscribeOn(Schedulers.io())
+ .subscribe(object : Observer {
+ 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
+ )
+ )
+ .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/messagesearch/MessageSearchActivity.kt b/app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchActivity.kt
index 0f2a732a0..ddf794841 100644
--- a/app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchActivity.kt
@@ -210,13 +210,13 @@ class MessageSearchActivity : BaseActivity() {
binding.emptyContainer.emptyListView.visibility = View.VISIBLE
}
- override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_search, menu)
return true
}
- override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
- val menuItem = menu!!.findItem(R.id.action_search)
+ override fun onPrepareOptionsMenu(menu: Menu): Boolean {
+ val menuItem = menu.findItem(R.id.action_search)
searchView = menuItem.actionView as SearchView
setupSearchView()
menuItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
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/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/remotefilebrowser/activities/RemoteFileBrowserActivity.kt b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/activities/RemoteFileBrowserActivity.kt
index c91ca0ebd..bd9f74b9d 100644
--- a/app/src/main/java/com/nextcloud/talk/remotefilebrowser/activities/RemoteFileBrowserActivity.kt
+++ b/app/src/main/java/com/nextcloud/talk/remotefilebrowser/activities/RemoteFileBrowserActivity.kt
@@ -180,7 +180,7 @@ class RemoteFileBrowserActivity : AppCompatActivity(), SelectionInterface, Swipe
showList()
}
- override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.menu_share_files, menu)
filesSelectionDoneMenuItem = menu?.findItem(R.id.files_selection_done)
diff --git a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java
index c12c04572..9e3841544 100644
--- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java
+++ b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java
@@ -391,6 +391,7 @@ public class ApiUtils {
getApplicationContext().getResources().getString(R.string.nc_push_server_url) + "/devices";
}
+ // see https://github.com/nextcloud/notifications/blob/master/docs/ocs-endpoint-v2.md
public static String getUrlForNotificationWithId(String baseUrl, String notificationId) {
return baseUrl + ocsApiVersion + "/apps/notifications/api/v2/notifications/" + notificationId;
}
diff --git a/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt
index 4341f18c1..62bbc837c 100644
--- a/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt
+++ b/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt
@@ -47,6 +47,7 @@ import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.preferences.AppPreferences
import java.io.IOException
+@Suppress("TooManyFunctions")
object NotificationUtils {
enum class NotificationChannels {
@@ -241,7 +242,7 @@ object NotificationUtils {
}
}
- fun cancelExistingNotificationWithId(context: Context?, conversationUser: User, notificationId: Long?) {
+ fun cancelNotification(context: Context?, conversationUser: User, notificationId: Long?) {
scanNotifications(context, conversationUser) { notificationManager, statusBarNotification, notification ->
if (notificationId == notification.extras.getLong(BundleKeys.KEY_NOTIFICATION_ID)) {
notificationManager.cancel(statusBarNotification.id)
diff --git a/app/src/main/java/com/nextcloud/talk/webrtc/MagicWebSocketInstance.java b/app/src/main/java/com/nextcloud/talk/webrtc/MagicWebSocketInstance.java
index f8e1ef422..bd1a9d429 100644
--- a/app/src/main/java/com/nextcloud/talk/webrtc/MagicWebSocketInstance.java
+++ b/app/src/main/java/com/nextcloud/talk/webrtc/MagicWebSocketInstance.java
@@ -467,7 +467,7 @@ public class MagicWebSocketInstance extends WebSocketListener {
@Subscribe(threadMode = ThreadMode.BACKGROUND)
public void onMessageEvent(NetworkEvent networkEvent) {
- if (networkEvent.getNetworkConnectionEvent().equals(NetworkEvent.NetworkConnectionEvent.NETWORK_CONNECTED) && !isConnected()) {
+ if (networkEvent.getNetworkConnectionEvent() == NetworkEvent.NetworkConnectionEvent.NETWORK_CONNECTED && !isConnected()) {
restartWebSocket();
}
}
diff --git a/app/src/main/java/com/nextcloud/talk/webrtc/PeerConnectionWrapper.java b/app/src/main/java/com/nextcloud/talk/webrtc/PeerConnectionWrapper.java
index 64e7c0ddf..f0597f5a7 100644
--- a/app/src/main/java/com/nextcloud/talk/webrtc/PeerConnectionWrapper.java
+++ b/app/src/main/java/com/nextcloud/talk/webrtc/PeerConnectionWrapper.java
@@ -276,7 +276,7 @@ public class PeerConnectionWrapper {
@Override
public void onStateChange() {
- if (dataChannel != null && dataChannel.state().equals(DataChannel.State.OPEN) &&
+ if (dataChannel != null && dataChannel.state() == DataChannel.State.OPEN &&
dataChannel.label().equals("status")) {
sendInitialMediaStatus();
}
@@ -343,9 +343,9 @@ public class PeerConnectionWrapper {
public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
Log.d("iceConnectionChangeTo: ", iceConnectionState.name() + " over " + peerConnection.hashCode() + " " + sessionId);
- if (iceConnectionState.equals(PeerConnection.IceConnectionState.CONNECTED)) {
+ if (iceConnectionState == PeerConnection.IceConnectionState.CONNECTED) {
EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType.PEER_CONNECTED,
- sessionId, null, null, null));
+ sessionId, null, null, videoStreamType));
if (!isMCUPublisher) {
EventBus.getDefault().post(new MediaStreamEvent(remoteStream, sessionId, videoStreamType));
@@ -354,22 +354,22 @@ public class PeerConnectionWrapper {
if (hasInitiated) {
sendInitialMediaStatus();
}
- } else if (iceConnectionState.equals(PeerConnection.IceConnectionState.COMPLETED)) {
+ } else if (iceConnectionState == PeerConnection.IceConnectionState.COMPLETED) {
EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType.PEER_CONNECTED,
- sessionId, null, null, null));
- } else if (iceConnectionState.equals(PeerConnection.IceConnectionState.CLOSED)) {
+ sessionId, null, null, videoStreamType));
+ } else if (iceConnectionState == PeerConnection.IceConnectionState.CLOSED) {
EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType
.PEER_CLOSED, sessionId, null, null, videoStreamType));
- } else if (iceConnectionState.equals(PeerConnection.IceConnectionState.DISCONNECTED) ||
- iceConnectionState.equals(PeerConnection.IceConnectionState.NEW) ||
- iceConnectionState.equals(PeerConnection.IceConnectionState.CHECKING)) {
+ } else if (iceConnectionState == PeerConnection.IceConnectionState.DISCONNECTED ||
+ iceConnectionState == PeerConnection.IceConnectionState.NEW ||
+ iceConnectionState == PeerConnection.IceConnectionState.CHECKING) {
EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType.PEER_DISCONNECTED,
- sessionId, null, null, null));
- } else if (iceConnectionState.equals(PeerConnection.IceConnectionState.FAILED)) {
+ sessionId, null, null, videoStreamType));
+ } else if (iceConnectionState == PeerConnection.IceConnectionState.FAILED) {
EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType.PEER_DISCONNECTED,
- sessionId, null, null, null));
+ sessionId, null, null, videoStreamType));
if (isMCUPublisher) {
- EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType.PUBLISHER_FAILED, sessionId, null, null, null));
+ EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType.PUBLISHER_FAILED, sessionId, null, null, videoStreamType));
}
}
}
@@ -469,7 +469,7 @@ public class PeerConnectionWrapper {
if (shouldNotReceiveVideo()) {
for (RtpTransceiver t : peerConnection.getTransceivers()) {
- if (t.getMediaType().equals(MediaStreamTrack.MediaType.MEDIA_TYPE_VIDEO)) {
+ if (t.getMediaType() == MediaStreamTrack.MediaType.MEDIA_TYPE_VIDEO) {
t.stop();
}
}
diff --git a/app/src/main/java/com/nextcloud/talk/webrtc/WebRtcAudioManager.java b/app/src/main/java/com/nextcloud/talk/webrtc/WebRtcAudioManager.java
index 2c2fd2780..45c97cac3 100644
--- a/app/src/main/java/com/nextcloud/talk/webrtc/WebRtcAudioManager.java
+++ b/app/src/main/java/com/nextcloud/talk/webrtc/WebRtcAudioManager.java
@@ -129,7 +129,7 @@ public class WebRtcAudioManager {
return;
}
- if (userSelectedAudioDevice.equals(AudioDevice.SPEAKER_PHONE)
+ if (userSelectedAudioDevice == AudioDevice.SPEAKER_PHONE
&& audioDevices.contains(AudioDevice.EARPIECE)
&& audioDevices.contains(AudioDevice.SPEAKER_PHONE)) {
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-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index 01b18ea2b..f48975cd4 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -173,7 +173,6 @@
إشارات غير مقروءة
رسائل غير مقروءة
كلمة سرية جديدة
- تطبيق %1$s غير مثبت على الخادم، جارِ الإلغاء
ضيف
لا
ليس لديك رسائل بعد
diff --git a/app/src/main/res/values-b+en+001/strings.xml b/app/src/main/res/values-b+en+001/strings.xml
index dcd30ad3b..f62d2d0ac 100644
--- a/app/src/main/res/values-b+en+001/strings.xml
+++ b/app/src/main/res/values-b+en+001/strings.xml
@@ -118,7 +118,6 @@
Never joined
New conversation
New password
- %1$s app not installed on the server, aborting
Guest
No
No messages yet
diff --git a/app/src/main/res/values-bg-rBG/strings.xml b/app/src/main/res/values-bg-rBG/strings.xml
index 51bb99f5e..d81c97e87 100644
--- a/app/src/main/res/values-bg-rBG/strings.xml
+++ b/app/src/main/res/values-bg-rBG/strings.xml
@@ -187,13 +187,13 @@
Съобщението е прочетено
Съобщението е изпратено
За активиране на гласова комуникация, моля, дайте право на „Микрофон“ в системните настройки.
+ Пропуснахте обаждане от %s
Модератор
Никога не е присъединяван
Нов разговор
Непрочетени споменавания
Непрочетени съобщения.
Нова парола
- Приложението %1$s не е инсталирано на сървъра, прекратяване
Гост
Не
Няма съобщения.
diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml
index 97715bf08..4f0801d4f 100644
--- a/app/src/main/res/values-ca/strings.xml
+++ b/app/src/main/res/values-ca/strings.xml
@@ -4,6 +4,8 @@
Cerca a %s
Sortida d\'àudio
Telèfon
+ Altaveu
+ Auriculars per cable
Avatar
Absent
Esborrar el missatge d\'estat
@@ -28,12 +30,15 @@
Més gran primer
Més petit primer
No s\'han trobat resultats
+ Comença a escriure per cercar ...
+ Cerca ...
Missatges
El compte que heu seleccionat s\'ha importat i ja és disponible
Quant a
Usuari actiu
Afegeix un compte
El compte està planificat per ser suprimit i no es pot canviar
+ Obre el menú principal
Afegeix un adjunt
Afegeix emoji
Afegeix participants
@@ -62,6 +67,7 @@
Estableix
Omet
Selecciona el certificat d’autenticació
+ S\'està connectant …
Fet
Enllaç de conversa
Informació de la conversa
@@ -85,6 +91,8 @@
No s’ha pogut obtenir el nom de visualització, s\'està avortant
No s\'ha pogut emmagatzemar el nom de visualització, s\'està avortant
Correu
+ 8 hores
+ 4 setmanes
Desactivada
1 dia
1 hora
@@ -105,6 +113,7 @@
Introduïu una contrasenya
Protecció amb contrasenya
Contrasenya feble
+ Introduïu un missatge …
Conversa important
Les notificacions d\'aquesta conversa anul·laran els paràmetres de no destorbar.
Uniu-vos amb un enllaç
@@ -118,6 +127,7 @@
S\'ha arribat al límit de %s caràcters
Vestíbul
Esteu esperant al vestíbul.
+ La vostra ubicació actual
Toca per desblocar
Fes que la conversa sigui privada
Fes que la conversa sigui pública
@@ -132,7 +142,6 @@
Nova conversa
Missatges sense llegir
Nova contrasenya
- L\'aplicació %1$s no està instal·lada al servidor, s\'està avortant
Convidat
No
No hi ha cap missatge encara
diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs-rCZ/strings.xml
index b4919efe9..ccbd34f4e 100644
--- a/app/src/main/res/values-cs-rCZ/strings.xml
+++ b/app/src/main/res/values-cs-rCZ/strings.xml
@@ -187,13 +187,14 @@
Zpráva přečtena
Zpráva odeslána
Pro zapnutí hlasové komunikace udělte v nastavení systému oprávnění „Mikrofon“ .
+ Zmeškali jste hovor od %s
Moderátor
Nikdy nepřipojeno
Nová konverzace
Nepřečtená zmínění
Nepřečtené zprávy
Nové heslo
- Aplikace %1$s není na serveru nainstalována, přerušuje se
+ %1$s není k dispozici (nenainstalováno nebo administrativně omezen přístup k)
Host
Ne
Zatím žádné zprávy
diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml
index 77403ff64..b2c679a0e 100644
--- a/app/src/main/res/values-da/strings.xml
+++ b/app/src/main/res/values-da/strings.xml
@@ -129,7 +129,6 @@
Ny samtale
Ulæste beskedder
Ny adgangskode
- %1$s appen er ikke installeret på serveren, afbryder
Gæst
Nej
Ingen beskeder endnu
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 986c4897e..2b8a9a4a1 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -187,13 +187,14 @@
Nachricht gelesen
Nachricht gesendet
Um Audioanrufe zu ermöglichen, gewähren Sie die Berechtigung für das \"Mikrofon\" in den Systemeinstellungen
+ Sie haben einen Anruf von %s verpasst
Moderator
Nie beigetreten
Neue Unterhaltung
Ungelesene Erwähnungen
Ungelesene Nachrichten
Neues Passwort
- %1$s App nicht auf dem Server installiert, Abbruch
+ %1$s nicht verfügbar (nicht installiert oder von der Administration eingeschränkt)
Gast
Nein
Noch keine Nachrichten
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index 796939de3..5aff3c8da 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -165,7 +165,6 @@
Νέα συνομιλία
Μη αναγνωσμένα μηνύματα
Νέο συνθηματικό
- Η εφαρμογή %1$s δεν εγκαταστάθηκε στον διακομιστή, ματαίωση
Επισκέπτης
Όχι
Κανένα μήνυμα ακόμα
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 4a1f0df3b..7ce409c95 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -187,13 +187,14 @@
Mensajes leídos
Mensaje enviado
Para permitir la comunicación de voz, concede el permiso de \"Micrófono\" en la configuración del sistema.
+ Perdiste una llamada de %s
Moderador
Nunca unido
Nueva conversación
Menciones sin leer
Mensajes no leídos
Nueva contraseña
- La app %1$s no está instalada en el servidor. Abortando
+ %1$s no está disponible (no se encuentra instalado o está restringido por el administrador)
Invitado
No
Aún no hay mensajes
@@ -354,7 +355,7 @@
¿Enviar estos archivos a %1$s?
¿Enviar este archivo a %1$s?
Lo siento, error en la subida
- Imposible subir %1$s
+ Fallo al subir %1$s
Falla
Compartido por %1$s
Subir desde dispositivo
diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml
index 841ed6425..cc4d25ece 100644
--- a/app/src/main/res/values-eu/strings.xml
+++ b/app/src/main/res/values-eu/strings.xml
@@ -193,7 +193,6 @@
Irakurri gabeko aipamenak
Irakurri gabeko mezuak
Pasahitz berria
- %1$s app-a ez dago instalatuta zerbitzarian, bertan behera uzten
Gonbidatua
Ez
Ez dago mezurik oraindik
diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml
index 1c0083c66..1abd0e18c 100644
--- a/app/src/main/res/values-fa/strings.xml
+++ b/app/src/main/res/values-fa/strings.xml
@@ -20,6 +20,7 @@
بارگذاری …
۴ ساعت
نامرئی
+ بار کردن نتیحههای بیشتر
نماد قفل
تازهترینها اول
قدیمیترینها اول
@@ -132,7 +133,6 @@
مکالمه جدید
پیامهای خوانده نشده
گذرواژه جدید
- برنامه %1$s روی سرور نصب نمیباشد. درحال لغو
مهمان
نه
هنوز پیامی ارسال نشده است
diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml
index 98121afef..71dadb35d 100644
--- a/app/src/main/res/values-fi-rFI/strings.xml
+++ b/app/src/main/res/values-fi-rFI/strings.xml
@@ -164,7 +164,6 @@
Lukemattomat maininnat
Lukemattomat viestit
Uusi salasana
- Sovellusta %1$s ei ole asennettu, keskeytetään
Vieras
Ei
Ei viestejä vielä
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 6f404294a..ba1fcb0e9 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -187,13 +187,13 @@
Message lu
Message envoyé
Pour établir une communication audio, veuillez autoriser l’utilisation du microphone dans les paramètres du système.
+ Vous avez manqué un appel de %s
Modérateur
Jamais contacté
Nouvelle conversation
Mentions non lues
Messages non lus
Nouveau mot de passe
- Application %1$s non installée sur le serveur, abandon
Invité
Non
Pas de messages
diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml
index 2f3e7315a..694ceee5f 100644
--- a/app/src/main/res/values-gl/strings.xml
+++ b/app/src/main/res/values-gl/strings.xml
@@ -140,7 +140,6 @@
Nova conversa
Mensaxes sen ler
Novo contrasinal
- A aplicación %1$s non está instalado no servidor, interrompendo
Convidado
Non
Aínda non hai mensaxes
diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml
index c53d3f58c..cde0b5e24 100644
--- a/app/src/main/res/values-hr/strings.xml
+++ b/app/src/main/res/values-hr/strings.xml
@@ -172,7 +172,6 @@
Nepročitana spominjanja
Nove poruke
Nova zaporka
- Aplikacija %1$s nije instalirana na poslužitelju, prekid
Gost
Ne
Još nema poruka
diff --git a/app/src/main/res/values-hu-rHU/strings.xml b/app/src/main/res/values-hu-rHU/strings.xml
index a3f7edcef..7f2b33c55 100644
--- a/app/src/main/res/values-hu-rHU/strings.xml
+++ b/app/src/main/res/values-hu-rHU/strings.xml
@@ -10,6 +10,7 @@
Profilkép
Távol
Hívás értesítés nélkül
+ Kamera engedély megadva. Válassza újra a kamerát.
Profilkép választása a felhőből
Állapotüzenet törlése
Állapotüzenet törlése ennyi idő után:
@@ -23,6 +24,7 @@
Legutóbbiak
Emodzsi keresése
Titkosított
+ Hiba történt a csevegések betöltése során
Sikertelen mentés: %1$s
mappa
Betöltés…
@@ -185,23 +187,26 @@
Üzenet elolvasva
Üzenet elküldve
A hanghívás engedélyezéséhez a rendszerbeállításokban meg kell adnia a „Mikrofon” engedélyt.
+ Nem fogadott hívás a következőtől: %s
Moderátor
Soha nem csatlakozott
Új beszélgetés
Olvasatlan említések
Olvasatlan üzenetek
Új jelszó
- A(z) %1$s alkalmazás nincs telepítve a kiszolgálón, megszakítás
Vendég
Nem
Nincs még üzenet
Nincs proxy
+ Nem kapcsolhatja be a hangot.
+ Nem kapcsolhatja be a videót.
%1$s a(z) %2$s értesítési csatornán
Hívások
Értesítés a bejövő hívásokról
Üzenetek
Értesítés a bejövő üzenetekről
Feltöltések
+ Értesítés a feltöltési folyamatról
Értesítési beállítások
Mindig értesítsen
Értesítsen, ha megemlítik Önt
@@ -348,11 +353,16 @@
Fájlok küldése ide: %1$s
Ezen fájl küldése ide: %1$s
A feltöltés sikertelen
+ A(z) %1$s feltöltése sikertelen
+ Sikertelen
Megosztás innen: %1$s
Feltöltés az eszközről
Feltöltés
+ %1$s → %2$s – %3$s\%%
Fénykép készítése
+ Videó készítése
Felhasználó
+ Videórögzítés innen: %1$s
Beszédrögzítés innen: %1$s (%2$s)
Tartsa a rögzítéshez, engedje el a küldéshez.
Hangrögzítési engedély szükséges
diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml
index 78546227f..19eda86fa 100644
--- a/app/src/main/res/values-is/strings.xml
+++ b/app/src/main/res/values-is/strings.xml
@@ -127,7 +127,6 @@
Nýtt samtal
Ólesin skilaboð
Nýtt lykilorð
- %1$s forritið ekki uppsett á þjóninu, hætti við
Gestur
Nei
Engin skilaboð ennþá
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index ea44b90c4..6123cedc0 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -178,7 +178,6 @@
Menzioni non lette
Messaggi non letti
Nuova password
- Applicazione %1$s non installata, interruzione in corso
Ospite
No
Ancora nessun messaggio
diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml
index 987cb033e..1622810ad 100644
--- a/app/src/main/res/values-iw/strings.xml
+++ b/app/src/main/res/values-iw/strings.xml
@@ -127,7 +127,6 @@
דיון חדש
הודעות שלא נקראו
ססמה חדשה
- היישומון %1$s אינו מותקן על השרת, הפעולה מבוטלת
אורח/ת
לא
אין הודעות עדיין
diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml
index 65bee3e3b..91efa6cb1 100644
--- a/app/src/main/res/values-ja-rJP/strings.xml
+++ b/app/src/main/res/values-ja-rJP/strings.xml
@@ -179,7 +179,6 @@
未読の返信
未読のメッセージ
新しいパスワード
- %1$s アプリがサーバーにインストールされていません。中断します。
ゲスト
いいえ
メッセージはまだありません
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index ae048a7e1..e289350e1 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -147,7 +147,7 @@
읽지 않은 언급
읽지 않은 메세지
새 암호
- 서버에 %1$s 앱이 설치되어 있지 않음, 중단함
+ %1$s을(를) 사용할 수 없음 (설치되지 않았거나 관리자에 의해 제한됨)
손님
아니요
아직 메시지 없음
diff --git a/app/src/main/res/values-lt-rLT/strings.xml b/app/src/main/res/values-lt-rLT/strings.xml
index 008e8d77a..666c1bf34 100644
--- a/app/src/main/res/values-lt-rLT/strings.xml
+++ b/app/src/main/res/values-lt-rLT/strings.xml
@@ -125,7 +125,6 @@
Naujas pokalbis
Neskaitytos žinutės
Naujas slaptažodis
- Serveryje nėra įdiegta programėlė %1$s, nutraukiama
Svečias
Ne
Kol kas žinučių nėra
diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml
index a9818925f..5f23cdd04 100644
--- a/app/src/main/res/values-nb-rNO/strings.xml
+++ b/app/src/main/res/values-nb-rNO/strings.xml
@@ -140,7 +140,6 @@
Uleste nevner
Uleste meldinger
Nytt passord
- %1$s-appen er ikke installert på serveren, avbryter
Gjest
Nei
Ingen meldiner enda
@@ -279,6 +278,7 @@
Synkroniser kun til betrodde servere
Sammenknyttet
Lokal
+ Kun synlig for personer som matches via telefonnummerintegrasjon via Talk på mobil
Privat
Synkroniser til betrodde servere og den globale og offentlige adresseboken
Publisert
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index 926f99acb..14933f67e 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -176,7 +176,6 @@ Kies er eentje van een provider.
Ongelezen vermeldingen
Ongelezen berichten
Nieuw wachtwoord
- %1$s app is niet geïnstalleerd op de server, afbreken
Gast
Nee
Nog geen berichten
diff --git a/app/src/main/res/values-nn-rNO/strings.xml b/app/src/main/res/values-nn-rNO/strings.xml
index dec7291f2..c28392c7c 100644
--- a/app/src/main/res/values-nn-rNO/strings.xml
+++ b/app/src/main/res/values-nn-rNO/strings.xml
@@ -137,7 +137,6 @@
Ny samtale
Meldingar er ikkje lest
Nytt passord
- %1$s applikasjon er ikkje installert på denne server, avbryt
Gjest
Nei
Ingen meldingar til no
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 0a1094a77..195b5d126 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -193,7 +193,6 @@
Nieprzeczytane wzmianki
Nieprzeczytane wiadomości
Nowe hasło
- Aplikacja %1$s nie została zainstalowana na serwerze, przerwano żądanie
Gość
Nie
Nie ma nowych wiadomości
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index e13afb73f..81a941381 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -193,7 +193,6 @@
Menções não lidas
Mensagens não lidas
Nova senha
- Aplicativo %1$s não instalado no servidor, cancelando
Convidado
Não
Sem mensagens ainda
@@ -248,7 +247,7 @@
60
30
300
- Persquisar
+ Pesquisar
Selecionar uma conta
Selecionar participantes
%1$s enviou um GIF.
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 9f385ed54..9e3f1ebd2 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -193,7 +193,7 @@
Непрочитанные упоминания
Непрочитанные сообщения
Новый пароль
- Приложение %1$s не установлено на сервере. Действие отменено.
+ Приложение %1$s недоступно (не установлено, либо использование приложения ограничено администратором)
Гость
Нет
Сообщений еще нет
diff --git a/app/src/main/res/values-sc/strings.xml b/app/src/main/res/values-sc/strings.xml
index f7ab7fce9..f1cdc1cfd 100644
--- a/app/src/main/res/values-sc/strings.xml
+++ b/app/src/main/res/values-sc/strings.xml
@@ -161,7 +161,6 @@
Resonada noa
Messàgios non lèghidos
Crae noa
- %1$s s\'aplicatzione no est installada in su serbidore, annullende
Persone invitada
No
Ancora perunu messàgiu
diff --git a/app/src/main/res/values-sk-rSK/strings.xml b/app/src/main/res/values-sk-rSK/strings.xml
index c48f15563..427afdff0 100644
--- a/app/src/main/res/values-sk-rSK/strings.xml
+++ b/app/src/main/res/values-sk-rSK/strings.xml
@@ -193,7 +193,6 @@
Neprečítané upozornenia
Neprečítané správy
Nové heslo
- Aplikácia %1$s nie je na serveri nainštalovaná, ruší sa
Hosť
Nie
Ešte žiadne správy
diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml
index 3ae071fa2..40c849b68 100644
--- a/app/src/main/res/values-sl/strings.xml
+++ b/app/src/main/res/values-sl/strings.xml
@@ -9,6 +9,7 @@
Ožičene slušalke
Podoba
Ne spremljam
+ Klic brez obvestila
Izbor pogodbe iz oblaka
Počisti sporočilo stanja
Počisti sporočilo stanja po
@@ -181,7 +182,6 @@
Neprebrane omembe
Neprebrana sporočila
Novo geslo
- Program %1$s na strežniku ni nameščen, zahteva bo preklicana.
Gost
Ne
Ni še sporočil
diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml
index 56e8e51b0..879edf54f 100644
--- a/app/src/main/res/values-sr/strings.xml
+++ b/app/src/main/res/values-sr/strings.xml
@@ -121,7 +121,6 @@
Нови разговор
Непрочитане поруке
Нова лозинка
- %1$s апликација није инсталирана на серверу, прекидам
Гост
Не
Још нема порука
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index e9b9fe5ed..e5d77ea75 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -141,7 +141,6 @@
Ny konversation
Olästa meddelanden
Nytt lösenord
- %1$s app inte installerad på servern, avbryter
Gäst
Nej
Inga meddelanden än
@@ -284,6 +283,7 @@
Online-status
Lägg till alternativ
Alternativ
+ Privat omröstning
Resultat
Inställningar
Rösta
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 4bafbd70c..7b7ecbf51 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -187,13 +187,14 @@
İleti okundu
İleti gönderildi
Sesli iletişim kurabilmek için sistem ayarlarından \"Mikrofon\" erişme iznini verin.
+ %s sizi aramış
Sorumlu
Hiç katılmadı
Yeni görüşme
Okunmamış anmalar
Okunmamış iletiler
Yeni parola
- %1$s uygulaması sunucu üzerinde kurulu değil, vazgeçiliyor
+ %1$s kullanılamıyor (kurulmamış ya da yönetici tarafından engellenmiş)
Konuk
Hayır
Henüz bir ileti yok
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index e39273a62..5fd9d7677 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -166,7 +166,6 @@
Непрочитані згадки
Непрочитані повідомлення
Новий пароль
- Застосунок %1$s не встановлено на сервері, скасування
Гість
Ні
Повідомлень немає
@@ -289,7 +288,7 @@
Оберіть обліковий запис
Місце
Місце у спільному доступі
- Сортувати по
+ Впорядкувати за
Виберіть файли
Вибачте, помилка завантаження
Завантажити з пристрою
@@ -319,7 +318,7 @@
Selected
Встановити статус
Встановити повідомлення про стан
- Поділитися
+ Спільний доступ
Аудіо
Файл
Зображення та відео
diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml
index 7c8e7b9a6..aacd06147 100644
--- a/app/src/main/res/values-vi/strings.xml
+++ b/app/src/main/res/values-vi/strings.xml
@@ -126,7 +126,6 @@
Tạo đàm thoại mới
Tin nhắn chưa đọc
Mật khẩu mới
- %1$s ứng dụng chưa cài đặt trên hệ thống, hủy bỏ
Khách
Không
Chưa có thông điệp nào
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index a8626c197..4d0a6119d 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -51,6 +51,7 @@
会话名称
您输入的名称已经存在
呼叫通知
+ 正在重新连接 ...
响铃
%1$s 在通话中
%1$s 通过手机
@@ -112,6 +113,7 @@
1 天
1 小时
1 周
+ 可指定聊天消息在一定时间后过期。注意:聊天中分享的文件不会被从所有者一方删除,但是会被从会话中取消分享。
获取网络设置失败
目标服务器不支持通过移动电话加入公共对话。你可以尝试通过浏览器加入对话。
抱歉,有地方出错了!
@@ -132,6 +134,7 @@
弱密码
重发邀请
共享会话链接
+ 输入消息…
重要会话
此会话中的通知将覆盖免打扰设置
使用链接加入
@@ -140,6 +143,7 @@
无法离开会话
%1$s 我上一次更改: %2$s
离开会话
+ 正在离开通话 ...
GNU 通用公共许可证,第3版
许可证
已达到%s字符限制
@@ -167,7 +171,6 @@
未读的提及
未读消息
新密码
- 服务器未安装 %1$s 应用,中止
来宾
不
暂无消息
@@ -309,7 +312,9 @@
\n密码: %1$s
分享这个位置
选择一个账户
+ 已分享项目
Deck 卡片
+ 沒有已分享的项目
位置
共享的位置
排序依据
diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml
index c00db7cf5..0cedc10f1 100644
--- a/app/src/main/res/values-zh-rHK/strings.xml
+++ b/app/src/main/res/values-zh-rHK/strings.xml
@@ -187,13 +187,14 @@
訊息已讀
訊息已傳送
為了開啟聲音的通訊請在系統設定內同意\"麥克風\"的需求。
+ 您錯過了 %s 的來電
主持人
從未加入
新對話
未讀的提及
未讀郵件
新密碼
- 此伺服器並未安裝%1$s應用程式,操作中斷。
+ %1$s 不可用(管理員未安裝或限制)
訪客
否
目前無任何訊息
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index d8fb71cf6..e081247c5 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -175,7 +175,6 @@
未讀的提及
未讀訊息
新密碼
- 此伺服器並未安裝%1$s應用,操作中斷。
訪客
否
目前無任何訊息
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ef4df1cff..9fa3b5a2c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -58,7 +58,7 @@
Failed to fetch capabilities, aborting
Failed to fetch signaling settings
Display name couldn\'t be fetched, aborting
- %1$s app not installed on the server, aborting
+ %1$s not available (not installed or restricted by admin)
Could not store display name, aborting
Never joined
@@ -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
diff --git a/build.gradle b/build.gradle
index c414fdcaa..7691b1413 100644
--- a/build.gradle
+++ b/build.gradle
@@ -24,7 +24,7 @@
buildscript {
ext {
- kotlinVersion = '1.7.20'
+ kotlinVersion = '1.7.21'
}
repositories {
diff --git a/buildSrc/src/main/groovy/com/nextcloud/talk/gradle/DownloadWebRtcTask.groovy b/buildSrc/src/main/groovy/com/nextcloud/talk/gradle/DownloadWebRtcTask.groovy
index b581678d5..75c2f7655 100644
--- a/buildSrc/src/main/groovy/com/nextcloud/talk/gradle/DownloadWebRtcTask.groovy
+++ b/buildSrc/src/main/groovy/com/nextcloud/talk/gradle/DownloadWebRtcTask.groovy
@@ -45,7 +45,7 @@ abstract class DownloadWebRtcTask extends DefaultTask {
private String getDownloadUrl() {
def webRtcVersion = version.get()
- return "https://github.com/nextcloud-releases/talk-clients-webrtc/releases/download/${webRtcVersion}-RC1/${getFileName()}"
+ return "https://github.com/nextcloud-releases/talk-clients-webrtc/releases/download/${webRtcVersion}/${getFileName()}"
}
private String getOutputPath() {
diff --git a/settings.gradle b/settings.gradle
index bb0692b16..7cf8a9b08 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -26,3 +26,9 @@ include ':app'
// substitute module('com.github.nextcloud.android-common:ui') using project(':ui')
// }
//}
+
+//includeBuild('../../../deps/ImagePicker') {
+// dependencySubstitution {
+// substitute module('com.github.nextcloud-deps:ImagePicker') using project(':imagepicker')
+// }
+//}