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') +// } +//}