Merge branch 'master' into dependabot/gradle/androidx.appcompat-appcompat-1.5.1

Signed-off-by: Tim Krüger <t@timkrueger.me>
This commit is contained in:
Tim Krüger 2022-11-19 18:23:00 +01:00 committed by GitHub
commit dd46c28568
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 1627 additions and 1656 deletions

View File

@ -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", {

View File

@ -39,7 +39,7 @@
<meta-data android:name="google_analytics_adid_collection_enabled" android:value="false" />
<service
android:name=".services.firebase.ChatAndCallMessagingService"
android:name=".services.firebase.NCFirebaseMessagingService"
android:exported="false"
android:foregroundServiceType="phoneCall">
<intent-filter>

View File

@ -1,327 +0,0 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* @author Tim Krüger
* Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
* Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.services.firebase
import android.annotation.SuppressLint
import android.app.Notification
import android.app.PendingIntent
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.util.Base64
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.emoji.text.EmojiCompat
import androidx.work.Data
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import autodagger.AutoInjector
import com.bluelinelabs.logansquare.LoganSquare
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.nextcloud.talk.R
import com.nextcloud.talk.activities.CallNotificationActivity
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.events.CallNotificationClick
import com.nextcloud.talk.jobs.NotificationWorker
import com.nextcloud.talk.jobs.PushRegistrationWorker
import com.nextcloud.talk.models.SignatureVerification
import com.nextcloud.talk.models.json.participants.Participant
import com.nextcloud.talk.models.json.participants.ParticipantsOverall
import com.nextcloud.talk.models.json.push.DecryptedPushMessage
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.NotificationUtils
import com.nextcloud.talk.utils.NotificationUtils.cancelAllNotificationsForAccount
import com.nextcloud.talk.utils.NotificationUtils.cancelExistingNotificationWithId
import com.nextcloud.talk.utils.NotificationUtils.getCallRingtoneUri
import com.nextcloud.talk.utils.PushUtils
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FROM_NOTIFICATION_START_CALL
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY
import com.nextcloud.talk.utils.preferences.AppPreferences
import io.reactivex.Observable
import io.reactivex.Observer
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import okhttp3.JavaNetCookieJar
import okhttp3.OkHttpClient
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import retrofit2.Retrofit
import java.net.CookieManager
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import java.security.PrivateKey
import java.util.concurrent.TimeUnit
import javax.crypto.Cipher
import javax.crypto.NoSuchPaddingException
import javax.inject.Inject
@SuppressLint("LongLogTag")
@AutoInjector(NextcloudTalkApplication::class)
class ChatAndCallMessagingService : FirebaseMessagingService() {
@Inject
lateinit var appPreferences: AppPreferences
private var isServiceInForeground: Boolean = false
private var decryptedPushMessage: DecryptedPushMessage? = null
private var signatureVerification: SignatureVerification? = null
private var handler: Handler = Handler()
@Inject
lateinit var retrofit: Retrofit
@Inject
lateinit var okHttpClient: OkHttpClient
@Inject
lateinit var eventBus: EventBus
override fun onCreate() {
super.onCreate()
sharedApplication!!.componentApplication.inject(this)
eventBus.register(this)
}
@Subscribe(threadMode = ThreadMode.BACKGROUND)
fun onMessageEvent(event: CallNotificationClick) {
Log.d(TAG, "CallNotification was clicked")
isServiceInForeground = false
stopForeground(true)
}
override fun onDestroy() {
Log.d(TAG, "onDestroy")
isServiceInForeground = false
eventBus.unregister(this)
stopForeground(true)
handler.removeCallbacksAndMessages(null)
super.onDestroy()
}
override fun onNewToken(token: String) {
super.onNewToken(token)
sharedApplication!!.componentApplication.inject(this)
appPreferences.pushToken = token
Log.d(TAG, "onNewToken. token = $token")
val data: Data = Data.Builder().putString(PushRegistrationWorker.ORIGIN, "onNewToken").build()
val pushRegistrationWork = OneTimeWorkRequest.Builder(PushRegistrationWorker::class.java)
.setInputData(data)
.build()
WorkManager.getInstance().enqueue(pushRegistrationWork)
}
override fun onMessageReceived(remoteMessage: RemoteMessage) {
Log.d(TAG, "onMessageReceived")
sharedApplication!!.componentApplication.inject(this)
if (!remoteMessage.data["subject"].isNullOrEmpty() && !remoteMessage.data["signature"].isNullOrEmpty()) {
decryptMessage(remoteMessage.data["subject"]!!, remoteMessage.data["signature"]!!)
}
}
@Suppress("Detekt.TooGenericExceptionCaught")
private fun decryptMessage(subject: String, signature: String) {
try {
val base64DecodedSubject = Base64.decode(subject, Base64.DEFAULT)
val base64DecodedSignature = Base64.decode(signature, Base64.DEFAULT)
val pushUtils = PushUtils()
val privateKey = pushUtils.readKeyFromFile(false) as PrivateKey
try {
signatureVerification = pushUtils.verifySignature(
base64DecodedSignature,
base64DecodedSubject
)
if (signatureVerification!!.signatureValid) {
decryptMessage(privateKey, base64DecodedSubject, subject, signature)
}
} catch (e1: NoSuchAlgorithmException) {
Log.e(NotificationWorker.TAG, "No proper algorithm to decrypt the message.", e1)
} catch (e1: NoSuchPaddingException) {
Log.e(NotificationWorker.TAG, "No proper padding to decrypt the message.", e1)
} catch (e1: InvalidKeyException) {
Log.e(NotificationWorker.TAG, "Invalid private key.", e1)
}
} catch (exception: Exception) {
Log.e(NotificationWorker.TAG, "Something went very wrong!", exception)
}
}
private fun decryptMessage(
privateKey: PrivateKey,
base64DecodedSubject: ByteArray?,
subject: String,
signature: String
) {
val cipher = Cipher.getInstance("RSA/None/PKCS1Padding")
cipher.init(Cipher.DECRYPT_MODE, privateKey)
val decryptedSubject = cipher.doFinal(base64DecodedSubject)
decryptedPushMessage = LoganSquare.parse(
String(decryptedSubject),
DecryptedPushMessage::class.java
)
decryptedPushMessage?.apply {
Log.d(TAG, this.toString())
timestamp = System.currentTimeMillis()
if (delete) {
cancelExistingNotificationWithId(applicationContext, signatureVerification!!.user!!, notificationId)
} else if (deleteAll) {
cancelAllNotificationsForAccount(applicationContext, signatureVerification!!.user!!)
} else if (deleteMultiple) {
notificationIds!!.forEach {
cancelExistingNotificationWithId(applicationContext, signatureVerification!!.user!!, it)
}
} else if (type == "call") {
val fullScreenIntent = Intent(applicationContext, CallNotificationActivity::class.java)
val bundle = Bundle()
bundle.putString(BundleKeys.KEY_ROOM_ID, decryptedPushMessage!!.id)
bundle.putParcelable(KEY_USER_ENTITY, signatureVerification!!.user)
bundle.putBoolean(KEY_FROM_NOTIFICATION_START_CALL, true)
fullScreenIntent.putExtras(bundle)
fullScreenIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
val fullScreenPendingIntent = PendingIntent.getActivity(
this@ChatAndCallMessagingService,
0,
fullScreenIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
)
val soundUri = getCallRingtoneUri(applicationContext!!, appPreferences)
val notificationChannelId = NotificationUtils.NotificationChannels.NOTIFICATION_CHANNEL_CALLS_V4.name
val uri = Uri.parse(signatureVerification!!.user!!.baseUrl)
val baseUrl = uri.host
val notification =
NotificationCompat.Builder(this@ChatAndCallMessagingService, notificationChannelId)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_CALL)
.setSmallIcon(R.drawable.ic_call_black_24dp)
.setSubText(baseUrl)
.setShowWhen(true)
.setWhen(decryptedPushMessage!!.timestamp)
.setContentTitle(EmojiCompat.get().process(decryptedPushMessage!!.subject))
.setAutoCancel(true)
.setOngoing(true)
// .setTimeoutAfter(45000L)
.setContentIntent(fullScreenPendingIntent)
.setFullScreenIntent(fullScreenPendingIntent, true)
.setSound(soundUri)
.build()
notification.flags = notification.flags or Notification.FLAG_INSISTENT
isServiceInForeground = true
checkIfCallIsActive(signatureVerification!!, decryptedPushMessage!!)
startForeground(decryptedPushMessage!!.timestamp.toInt(), notification)
} else {
val messageData = Data.Builder()
.putString(BundleKeys.KEY_NOTIFICATION_SUBJECT, subject)
.putString(BundleKeys.KEY_NOTIFICATION_SIGNATURE, signature)
.build()
val pushNotificationWork =
OneTimeWorkRequest.Builder(NotificationWorker::class.java).setInputData(messageData)
.build()
WorkManager.getInstance().enqueue(pushNotificationWork)
}
}
}
private fun checkIfCallIsActive(
signatureVerification: SignatureVerification,
decryptedPushMessage: DecryptedPushMessage
) {
Log.d(TAG, "checkIfCallIsActive")
val ncApi = retrofit.newBuilder()
.client(okHttpClient.newBuilder().cookieJar(JavaNetCookieJar(CookieManager())).build()).build()
.create(NcApi::class.java)
var hasParticipantsInCall = true
var inCallOnDifferentDevice = false
val apiVersion = ApiUtils.getConversationApiVersion(
signatureVerification.user,
intArrayOf(ApiUtils.APIv4, 1)
)
ncApi.getPeersForCall(
ApiUtils.getCredentials(
signatureVerification.user!!.username,
signatureVerification.user!!.token
),
ApiUtils.getUrlForCall(
apiVersion,
signatureVerification.user!!.baseUrl,
decryptedPushMessage.id
)
)
.repeatWhen { completed ->
completed.zipWith(Observable.range(1, OBSERVABLE_COUNT), { _, i -> i })
.flatMap { Observable.timer(OBSERVABLE_DELAY, TimeUnit.SECONDS) }
.takeWhile { isServiceInForeground && hasParticipantsInCall && !inCallOnDifferentDevice }
}
.subscribeOn(Schedulers.io())
.subscribe(object : Observer<ParticipantsOverall> {
override fun onSubscribe(d: Disposable) = Unit
override fun onNext(participantsOverall: ParticipantsOverall) {
val participantList: List<Participant> = participantsOverall.ocs!!.data!!
hasParticipantsInCall = participantList.isNotEmpty()
if (hasParticipantsInCall) {
for (participant in participantList) {
if (participant.actorId == signatureVerification.user!!.userId &&
participant.actorType == Participant.ActorType.USERS
) {
inCallOnDifferentDevice = true
break
}
}
}
if (!hasParticipantsInCall || inCallOnDifferentDevice) {
Log.d(TAG, "no participants in call OR inCallOnDifferentDevice")
stopForeground(true)
handler.removeCallbacksAndMessages(null)
}
}
override fun onError(e: Throwable) = Unit
override fun onComplete() {
stopForeground(true)
handler.removeCallbacksAndMessages(null)
}
})
}
companion object {
private val TAG = ChatAndCallMessagingService::class.simpleName
private const val OBSERVABLE_COUNT = 12
private const val OBSERVABLE_DELAY: Long = 5
}
}

View File

@ -0,0 +1,97 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* @author Tim Krüger
* @author Marcel Hibbe
* Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
* Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
* Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.services.firebase
import android.util.Log
import androidx.work.Data
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import autodagger.AutoInjector
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.jobs.NotificationWorker
import com.nextcloud.talk.jobs.PushRegistrationWorker
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.preferences.AppPreferences
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class NCFirebaseMessagingService : FirebaseMessagingService() {
@Inject
lateinit var appPreferences: AppPreferences
override fun onCreate() {
Log.d(TAG, "onCreate")
super.onCreate()
sharedApplication!!.componentApplication.inject(this)
}
override fun onMessageReceived(remoteMessage: RemoteMessage) {
Log.d(TAG, "onMessageReceived")
sharedApplication!!.componentApplication.inject(this)
Log.d(TAG, "remoteMessage.priority: " + remoteMessage.priority)
Log.d(TAG, "remoteMessage.originalPriority: " + remoteMessage.originalPriority)
val data = remoteMessage.data
val subject = data[KEY_NOTIFICATION_SUBJECT]
val signature = data[KEY_NOTIFICATION_SIGNATURE]
if (!subject.isNullOrEmpty() && !signature.isNullOrEmpty()) {
val messageData = Data.Builder()
.putString(BundleKeys.KEY_NOTIFICATION_SUBJECT, subject)
.putString(BundleKeys.KEY_NOTIFICATION_SIGNATURE, signature)
.build()
val notificationWork =
OneTimeWorkRequest.Builder(NotificationWorker::class.java).setInputData(messageData)
.build()
WorkManager.getInstance().enqueue(notificationWork)
}
}
override fun onNewToken(token: String) {
super.onNewToken(token)
Log.d(TAG, "onNewToken. token = $token")
appPreferences.pushToken = token
val data: Data = Data.Builder().putString(
PushRegistrationWorker.ORIGIN,
"NCFirebaseMessagingService#onNewToken"
).build()
val pushRegistrationWork = OneTimeWorkRequest.Builder(PushRegistrationWorker::class.java)
.setInputData(data)
.build()
WorkManager.getInstance().enqueue(pushRegistrationWork)
}
companion object {
private val TAG = NCFirebaseMessagingService::class.simpleName
const val KEY_NOTIFICATION_SUBJECT = "subject"
const val KEY_NOTIFICATION_SIGNATURE = "signature"
}
}

View File

@ -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<SignalingOverall>() {
@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());

View File

@ -1,451 +0,0 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.activities;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.media.AudioAttributes;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import com.facebook.common.executors.UiThreadImmediateExecutorService;
import com.facebook.common.references.CloseableReference;
import com.facebook.datasource.DataSource;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.imagepipeline.core.ImagePipeline;
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber;
import com.facebook.imagepipeline.image.CloseableImage;
import com.facebook.imagepipeline.request.ImageRequest;
import com.nextcloud.talk.R;
import com.nextcloud.talk.api.NcApi;
import com.nextcloud.talk.application.NextcloudTalkApplication;
import com.nextcloud.talk.data.user.model.User;
import com.nextcloud.talk.databinding.CallNotificationActivityBinding;
import com.nextcloud.talk.events.CallNotificationClick;
import com.nextcloud.talk.models.json.conversations.Conversation;
import com.nextcloud.talk.models.json.conversations.RoomOverall;
import com.nextcloud.talk.models.json.participants.Participant;
import com.nextcloud.talk.models.json.participants.ParticipantsOverall;
import com.nextcloud.talk.utils.ApiUtils;
import com.nextcloud.talk.utils.DisplayUtils;
import com.nextcloud.talk.utils.DoNotDisturbUtils;
import com.nextcloud.talk.utils.NotificationUtils;
import com.nextcloud.talk.utils.ParticipantPermissions;
import com.nextcloud.talk.utils.bundle.BundleKeys;
import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew;
import com.nextcloud.talk.utils.preferences.AppPreferences;
import org.greenrobot.eventbus.EventBus;
import org.parceler.Parcels;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import autodagger.AutoInjector;
import io.reactivex.Observable;
import io.reactivex.Observer;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.annotations.NonNull;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import okhttp3.Cache;
@SuppressLint("LongLogTag")
@AutoInjector(NextcloudTalkApplication.class)
public class CallNotificationActivity extends CallBaseActivity {
public static final String TAG = "CallNotificationActivity";
@Inject
NcApi ncApi;
@Inject
AppPreferences appPreferences;
@Inject
Cache cache;
@Inject
EventBus eventBus;
@Inject
Context context;
private List<Disposable> disposablesList = new ArrayList<>();
private Bundle originalBundle;
private String roomId;
private User userBeingCalled;
private String credentials;
private Conversation currentConversation;
private MediaPlayer mediaPlayer;
private boolean leavingScreen = false;
private Handler handler;
private CallNotificationActivityBinding binding;
@Override
public void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "onCreate");
super.onCreate(savedInstanceState);
NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
binding = CallNotificationActivityBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
hideNavigationIfNoPipAvailable();
eventBus.post(new CallNotificationClick());
Bundle extras = getIntent().getExtras();
this.roomId = extras.getString(BundleKeys.KEY_ROOM_ID, "");
this.currentConversation = Parcels.unwrap(extras.getParcelable(BundleKeys.KEY_ROOM));
this.userBeingCalled = extras.getParcelable(BundleKeys.KEY_USER_ENTITY);
this.originalBundle = extras;
credentials = ApiUtils.getCredentials(userBeingCalled.getUsername(), userBeingCalled.getToken());
setCallDescriptionText();
if (currentConversation == null) {
handleFromNotification();
} else {
setUpAfterConversationIsKnown();
}
if (DoNotDisturbUtils.INSTANCE.shouldPlaySound()) {
playRingtoneSound();
}
initClickListeners();
}
@Override
public void onStart() {
super.onStart();
if (handler == null) {
handler = new Handler();
try {
cache.evictAll();
} catch (IOException e) {
Log.e(TAG, "Failed to evict cache");
}
}
}
private void initClickListeners() {
binding.callAnswerVoiceOnlyView.setOnClickListener(l -> {
Log.d(TAG, "accept call (voice only)");
originalBundle.putBoolean(BundleKeys.KEY_CALL_VOICE_ONLY, true);
proceedToCall();
});
binding.callAnswerCameraView.setOnClickListener(l -> {
Log.d(TAG, "accept call (with video)");
originalBundle.putBoolean(BundleKeys.KEY_CALL_VOICE_ONLY, false);
proceedToCall();
});
binding.hangupButton.setOnClickListener(l -> hangup());
}
private void setCallDescriptionText() {
String callDescriptionWithoutTypeInfo =
String.format(
getResources().getString(R.string.nc_call_unknown),
getResources().getString(R.string.nc_app_product_name));
binding.incomingCallVoiceOrVideoTextView.setText(callDescriptionWithoutTypeInfo);
}
private void showAnswerControls() {
binding.callAnswerCameraView.setVisibility(View.VISIBLE);
binding.callAnswerVoiceOnlyView.setVisibility(View.VISIBLE);
}
private void hangup() {
leavingScreen = true;
finish();
}
private void proceedToCall() {
originalBundle.putString(BundleKeys.KEY_ROOM_TOKEN, currentConversation.getToken());
originalBundle.putString(BundleKeys.KEY_CONVERSATION_NAME, currentConversation.getDisplayName());
ParticipantPermissions participantPermission = new ParticipantPermissions(userBeingCalled, currentConversation);
originalBundle.putBoolean(
BundleKeys.KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_AUDIO,
participantPermission.canPublishAudio());
originalBundle.putBoolean(
BundleKeys.KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_VIDEO,
participantPermission.canPublishVideo());
Intent intent = new Intent(this, CallActivity.class);
intent.putExtras(originalBundle);
startActivity(intent);
}
private void checkIfAnyParticipantsRemainInRoom() {
int apiVersion = ApiUtils.getCallApiVersion(userBeingCalled, new int[]{ApiUtils.APIv4, 1});
ncApi.getPeersForCall(
credentials,
ApiUtils.getUrlForCall(
apiVersion,
userBeingCalled.getBaseUrl(),
currentConversation.getToken()))
.subscribeOn(Schedulers.io())
.repeatWhen(completed -> completed.zipWith(Observable.range(1, 12), (n, i) -> i)
.flatMap(retryCount -> Observable.timer(5, TimeUnit.SECONDS))
.takeWhile(observable -> !leavingScreen))
.subscribe(new Observer<ParticipantsOverall>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
disposablesList.add(d);
}
@Override
public void onNext(@NonNull ParticipantsOverall participantsOverall) {
boolean hasParticipantsInCall = false;
boolean inCallOnDifferentDevice = false;
List<Participant> participantList = participantsOverall.getOcs().getData();
hasParticipantsInCall = participantList.size() > 0;
if (hasParticipantsInCall) {
for (Participant participant : participantList) {
if (participant.getCalculatedActorType() == Participant.ActorType.USERS &&
participant.getCalculatedActorId().equals(userBeingCalled.getUserId())) {
inCallOnDifferentDevice = true;
break;
}
}
}
if (!hasParticipantsInCall || inCallOnDifferentDevice) {
runOnUiThread(() -> hangup());
}
}
@Override
public void onError(@NonNull Throwable e) {
Log.e(TAG, "error while getPeersForCall", e);
}
@Override
public void onComplete() {
runOnUiThread(() -> hangup());
}
});
}
private void handleFromNotification() {
int apiVersion = ApiUtils.getConversationApiVersion(userBeingCalled, new int[]{ApiUtils.APIv4,
ApiUtils.APIv3, 1});
ncApi.getRoom(credentials, ApiUtils.getUrlForRoom(apiVersion, userBeingCalled.getBaseUrl(), roomId))
.subscribeOn(Schedulers.io())
.retry(3)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<RoomOverall>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
disposablesList.add(d);
}
@Override
public void onNext(@NonNull RoomOverall roomOverall) {
currentConversation = roomOverall.getOcs().getData();
setUpAfterConversationIsKnown();
if (apiVersion >= 3) {
boolean hasCallFlags =
CapabilitiesUtilNew.hasSpreedFeatureCapability(userBeingCalled,
"conversation-call-flags");
if (hasCallFlags) {
if (isInCallWithVideo(currentConversation.getCallFlag())) {
binding.incomingCallVoiceOrVideoTextView.setText(
String.format(getResources().getString(R.string.nc_call_video),
getResources().getString(R.string.nc_app_product_name)));
} else {
binding.incomingCallVoiceOrVideoTextView.setText(
String.format(getResources().getString(R.string.nc_call_voice),
getResources().getString(R.string.nc_app_product_name)));
}
}
}
}
@Override
public void onError(@NonNull Throwable e) {
Log.e(TAG, e.getMessage(), e);
}
@Override
public void onComplete() {
// unused atm
}
});
}
private boolean isInCallWithVideo(int callFlag) {
return (callFlag >= Participant.InCallFlags.IN_CALL + Participant.InCallFlags.WITH_VIDEO);
}
private void setUpAfterConversationIsKnown() {
binding.conversationNameTextView.setText(currentConversation.getDisplayName());
if(currentConversation.getType() == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL){
setAvatarForOneToOneCall();
} else {
binding.avatarImageView.setImageResource(R.drawable.ic_circular_group);
}
checkIfAnyParticipantsRemainInRoom();
showAnswerControls();
}
private void setAvatarForOneToOneCall() {
ImageRequest imageRequest =
DisplayUtils.getImageRequestForUrl(
ApiUtils.getUrlForAvatar(userBeingCalled.getBaseUrl(),
currentConversation.getName(),
true));
ImagePipeline imagePipeline = Fresco.getImagePipeline();
DataSource<CloseableReference<CloseableImage>> dataSource = imagePipeline.fetchDecodedImage(imageRequest, null);
dataSource.subscribe(new BaseBitmapDataSubscriber() {
@Override
protected void onNewResultImpl(@Nullable Bitmap bitmap) {
binding.avatarImageView.getHierarchy().setImage(
new BitmapDrawable(getResources(), bitmap),
100,
true);
}
@Override
protected void onFailureImpl(DataSource<CloseableReference<CloseableImage>> dataSource) {
Log.e(TAG, "failed to load avatar");
}
}, UiThreadImmediateExecutorService.getInstance());
}
private void endMediaNotifications() {
if (mediaPlayer != null) {
if (mediaPlayer.isPlaying()) {
mediaPlayer.stop();
}
mediaPlayer.release();
mediaPlayer = null;
}
}
@Override
public void onDestroy() {
leavingScreen = true;
if (handler != null) {
handler.removeCallbacksAndMessages(null);
handler = null;
}
dispose();
endMediaNotifications();
super.onDestroy();
}
private void dispose() {
if (disposablesList != null) {
for (Disposable disposable : disposablesList) {
if (!disposable.isDisposed()) {
disposable.dispose();
}
}
}
}
private void playRingtoneSound() {
Uri ringtoneUri = NotificationUtils.INSTANCE.getCallRingtoneUri(getApplicationContext(), appPreferences);
if (ringtoneUri != null) {
mediaPlayer = new MediaPlayer();
try {
mediaPlayer.setDataSource(this, ringtoneUri);
mediaPlayer.setLooping(true);
AudioAttributes audioAttributes = new AudioAttributes
.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
.build();
mediaPlayer.setAudioAttributes(audioAttributes);
mediaPlayer.setOnPreparedListener(mp -> mediaPlayer.start());
mediaPlayer.prepareAsync();
} catch (IOException e) {
Log.e(TAG, "Failed to set data source");
}
}
}
@RequiresApi(api = Build.VERSION_CODES.O)
@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
isInPipMode = isInPictureInPictureMode;
if (isInPictureInPictureMode) {
updateUiForPipMode();
} else {
updateUiForNormalMode();
}
}
public void updateUiForPipMode() {
binding.callAnswerButtons.setVisibility(View.INVISIBLE);
binding.incomingCallRelativeLayout.setVisibility(View.INVISIBLE);
}
public void updateUiForNormalMode() {
binding.callAnswerButtons.setVisibility(View.VISIBLE);
binding.incomingCallRelativeLayout.setVisibility(View.VISIBLE);
}
@Override
void suppressFitsSystemWindows() {
binding.controllerCallNotificationLayout.setFitsSystemWindows(false);
}
}

View File

@ -0,0 +1,476 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* @author Marcel Hibbe
* Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.activities
import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.media.AudioAttributes
import android.media.MediaPlayer
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.SystemClock
import android.util.Log
import android.view.View
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import autodagger.AutoInjector
import com.facebook.common.executors.UiThreadImmediateExecutorService
import com.facebook.common.references.CloseableReference
import com.facebook.datasource.DataSource
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber
import com.facebook.imagepipeline.image.CloseableImage
import com.nextcloud.talk.R
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.CallNotificationActivityBinding
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.models.json.conversations.RoomOverall
import com.nextcloud.talk.models.json.participants.Participant
import com.nextcloud.talk.models.json.participants.ParticipantsOverall
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.DoNotDisturbUtils.shouldPlaySound
import com.nextcloud.talk.utils.NotificationUtils
import com.nextcloud.talk.utils.NotificationUtils.getCallRingtoneUri
import com.nextcloud.talk.utils.ParticipantPermissions
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CALL_VOICE_ONLY
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_NAME
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY
import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.hasSpreedFeatureCapability
import io.reactivex.Observable
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import okhttp3.Cache
import org.parceler.Parcels
import java.io.IOException
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@SuppressLint("LongLogTag")
@AutoInjector(NextcloudTalkApplication::class)
class CallNotificationActivity : CallBaseActivity() {
@JvmField
@Inject
var ncApi: NcApi? = null
@JvmField
@Inject
var cache: Cache? = null
private val disposablesList: MutableList<Disposable> = ArrayList()
private var originalBundle: Bundle? = null
private var roomToken: String? = null
private var userBeingCalled: User? = null
private var credentials: String? = null
private var currentConversation: Conversation? = null
private var mediaPlayer: MediaPlayer? = null
private var leavingScreen = false
private var handler: Handler? = null
private var binding: CallNotificationActivityBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
Log.d(TAG, "onCreate")
super.onCreate(savedInstanceState)
sharedApplication!!.componentApplication.inject(this)
binding = CallNotificationActivityBinding.inflate(layoutInflater)
setContentView(binding!!.root)
hideNavigationIfNoPipAvailable()
val extras = intent.extras
roomToken = extras!!.getString(KEY_ROOM_TOKEN, "")
currentConversation = Parcels.unwrap(extras.getParcelable(KEY_ROOM))
userBeingCalled = extras.getParcelable(KEY_USER_ENTITY)
originalBundle = extras
credentials = ApiUtils.getCredentials(userBeingCalled!!.username, userBeingCalled!!.token)
setCallDescriptionText()
if (currentConversation == null) {
handleFromNotification()
} else {
setUpAfterConversationIsKnown()
}
if (shouldPlaySound()) {
playRingtoneSound()
}
initClickListeners()
}
override fun onStart() {
super.onStart()
if (handler == null) {
handler = Handler()
try {
cache!!.evictAll()
} catch (e: IOException) {
Log.e(TAG, "Failed to evict cache")
}
}
}
private fun initClickListeners() {
binding!!.callAnswerVoiceOnlyView.setOnClickListener {
Log.d(TAG, "accept call (voice only)")
originalBundle!!.putBoolean(KEY_CALL_VOICE_ONLY, true)
proceedToCall()
}
binding!!.callAnswerCameraView.setOnClickListener {
Log.d(TAG, "accept call (with video)")
originalBundle!!.putBoolean(KEY_CALL_VOICE_ONLY, false)
proceedToCall()
}
binding!!.hangupButton.setOnClickListener { hangup() }
}
private fun setCallDescriptionText() {
val callDescriptionWithoutTypeInfo = String.format(
resources.getString(R.string.nc_call_unknown),
resources.getString(R.string.nc_app_product_name)
)
binding!!.incomingCallVoiceOrVideoTextView.text = callDescriptionWithoutTypeInfo
}
private fun showAnswerControls() {
binding!!.callAnswerCameraView.visibility = View.VISIBLE
binding!!.callAnswerVoiceOnlyView.visibility = View.VISIBLE
}
private fun hangup() {
leavingScreen = true
dispose()
endMediaNotifications()
finish()
}
private fun proceedToCall() {
originalBundle!!.putString(KEY_ROOM_TOKEN, currentConversation!!.token)
originalBundle!!.putString(KEY_CONVERSATION_NAME, currentConversation!!.displayName)
val participantPermission = ParticipantPermissions(
userBeingCalled!!,
currentConversation!!
)
originalBundle!!.putBoolean(
BundleKeys.KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_AUDIO,
participantPermission.canPublishAudio()
)
originalBundle!!.putBoolean(
BundleKeys.KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_VIDEO,
participantPermission.canPublishVideo()
)
val intent = Intent(this, CallActivity::class.java)
intent.putExtras(originalBundle!!)
startActivity(intent)
}
private fun checkIfAnyParticipantsRemainInRoom() {
val apiVersion = ApiUtils.getCallApiVersion(userBeingCalled, intArrayOf(ApiUtils.APIv4, 1))
ncApi!!.getPeersForCall(
credentials,
ApiUtils.getUrlForCall(
apiVersion,
userBeingCalled!!.baseUrl,
currentConversation!!.token
)
)
.subscribeOn(Schedulers.io())
.repeatWhen { completed: Observable<Any?> ->
completed.zipWith(Observable.range(TIMER_START, TIMER_COUNT)) { _: Any?, i: Int? -> i!! }
.flatMap { Observable.timer(TIMER_DELAY, TimeUnit.SECONDS) }
.takeWhile { !leavingScreen }
}
.subscribe(object : Observer<ParticipantsOverall> {
override fun onSubscribe(d: Disposable) {
disposablesList.add(d)
}
override fun onNext(participantsOverall: ParticipantsOverall) {
val hasParticipantsInCall: Boolean
var inCallOnDifferentDevice = false
val participantList = participantsOverall.ocs!!.data
hasParticipantsInCall = participantList!!.isNotEmpty()
if (hasParticipantsInCall) {
for (participant in participantList) {
if (participant.calculatedActorType === Participant.ActorType.USERS &&
participant.calculatedActorId == userBeingCalled!!.userId
) {
inCallOnDifferentDevice = true
break
}
}
}
if (inCallOnDifferentDevice) {
runOnUiThread { hangup() }
}
if (!hasParticipantsInCall) {
showMissedCallNotification()
runOnUiThread { hangup() }
}
}
override fun onError(e: Throwable) {
Log.e(TAG, "error while getPeersForCall", e)
}
override fun onComplete() {
showMissedCallNotification()
runOnUiThread { hangup() }
}
})
}
private fun showMissedCallNotification() {
val mNotifyManager: NotificationManager?
val mBuilder: NotificationCompat.Builder?
mNotifyManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
mBuilder = NotificationCompat.Builder(
context,
NotificationUtils.NotificationChannels
.NOTIFICATION_CHANNEL_MESSAGES_V4.name
)
val notification: Notification = mBuilder
.setContentTitle(
String.format(resources.getString(R.string.nc_missed_call), currentConversation!!.displayName)
)
.setSmallIcon(R.drawable.ic_baseline_phone_missed_24)
.setOngoing(false)
.setAutoCancel(true)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setContentIntent(getIntentToOpenConversation())
.build()
val notificationId: Int = SystemClock.uptimeMillis().toInt()
mNotifyManager.notify(notificationId, notification)
}
private fun getIntentToOpenConversation(): PendingIntent? {
val bundle = Bundle()
val intent = Intent(context, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
bundle.putString(KEY_ROOM_TOKEN, currentConversation?.token)
bundle.putParcelable(KEY_USER_ENTITY, userBeingCalled)
bundle.putBoolean(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL, false)
intent.putExtras(bundle)
val requestCode = System.currentTimeMillis().toInt()
val intentFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE
} else {
0
}
return PendingIntent.getActivity(context, requestCode, intent, intentFlag)
}
@Suppress("MagicNumber")
private fun handleFromNotification() {
val apiVersion = ApiUtils.getConversationApiVersion(
userBeingCalled,
intArrayOf(
ApiUtils.APIv4,
ApiUtils.APIv3, 1
)
)
ncApi!!.getRoom(credentials, ApiUtils.getUrlForRoom(apiVersion, userBeingCalled!!.baseUrl, roomToken))
.subscribeOn(Schedulers.io())
.retry(GET_ROOM_RETRY_COUNT)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<RoomOverall> {
override fun onSubscribe(d: Disposable) {
disposablesList.add(d)
}
override fun onNext(roomOverall: RoomOverall) {
currentConversation = roomOverall.ocs!!.data
setUpAfterConversationIsKnown()
if (apiVersion >= 3) {
val hasCallFlags = hasSpreedFeatureCapability(
userBeingCalled,
"conversation-call-flags"
)
if (hasCallFlags) {
if (isInCallWithVideo(currentConversation!!.callFlag)) {
binding!!.incomingCallVoiceOrVideoTextView.text = String.format(
resources.getString(R.string.nc_call_video),
resources.getString(R.string.nc_app_product_name)
)
} else {
binding!!.incomingCallVoiceOrVideoTextView.text = String.format(
resources.getString(R.string.nc_call_voice),
resources.getString(R.string.nc_app_product_name)
)
}
}
}
}
override fun onError(e: Throwable) {
Log.e(TAG, e.message, e)
}
override fun onComplete() {
// unused atm
}
})
}
private fun isInCallWithVideo(callFlag: Int): Boolean {
return callFlag >= Participant.InCallFlags.IN_CALL + Participant.InCallFlags.WITH_VIDEO
}
private fun setUpAfterConversationIsKnown() {
binding!!.conversationNameTextView.text = currentConversation!!.displayName
if (currentConversation!!.type === Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) {
setAvatarForOneToOneCall()
} else {
binding!!.avatarImageView.setImageResource(R.drawable.ic_circular_group)
}
checkIfAnyParticipantsRemainInRoom()
showAnswerControls()
}
@Suppress("MagicNumber")
private fun setAvatarForOneToOneCall() {
val imageRequest = DisplayUtils.getImageRequestForUrl(
ApiUtils.getUrlForAvatar(
userBeingCalled!!.baseUrl,
currentConversation!!.name,
true
)
)
val imagePipeline = Fresco.getImagePipeline()
val dataSource = imagePipeline.fetchDecodedImage(imageRequest, null)
dataSource.subscribe(
object : BaseBitmapDataSubscriber() {
override fun onNewResultImpl(bitmap: Bitmap?) {
binding!!.avatarImageView.hierarchy.setImage(
BitmapDrawable(resources, bitmap), 100f,
true
)
}
override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage?>>) {
Log.e(TAG, "failed to load avatar")
}
},
UiThreadImmediateExecutorService.getInstance()
)
}
private fun endMediaNotifications() {
if (mediaPlayer != null) {
if (mediaPlayer!!.isPlaying) {
mediaPlayer!!.stop()
}
mediaPlayer!!.release()
mediaPlayer = null
}
}
public override fun onDestroy() {
leavingScreen = true
if (handler != null) {
handler!!.removeCallbacksAndMessages(null)
handler = null
}
dispose()
endMediaNotifications()
super.onDestroy()
}
private fun dispose() {
for (disposable in disposablesList) {
if (!disposable.isDisposed) {
disposable.dispose()
}
}
}
private fun playRingtoneSound() {
val ringtoneUri = getCallRingtoneUri(applicationContext, appPreferences)
if (ringtoneUri != null) {
mediaPlayer = MediaPlayer()
try {
mediaPlayer!!.setDataSource(this, ringtoneUri)
mediaPlayer!!.isLooping = true
val audioAttributes = AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
.build()
mediaPlayer!!.setAudioAttributes(audioAttributes)
mediaPlayer!!.setOnPreparedListener { mediaPlayer!!.start() }
mediaPlayer!!.prepareAsync()
} catch (e: IOException) {
Log.e(TAG, "Failed to set data source")
}
}
}
@RequiresApi(api = Build.VERSION_CODES.O)
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
isInPipMode = isInPictureInPictureMode
if (isInPictureInPictureMode) {
updateUiForPipMode()
} else {
updateUiForNormalMode()
}
}
public override fun updateUiForPipMode() {
binding!!.callAnswerButtons.visibility = View.INVISIBLE
binding!!.incomingCallRelativeLayout.visibility = View.INVISIBLE
}
public override fun updateUiForNormalMode() {
binding!!.callAnswerButtons.visibility = View.VISIBLE
binding!!.incomingCallRelativeLayout.visibility = View.VISIBLE
}
public override fun suppressFitsSystemWindows() {
binding!!.controllerCallNotificationLayout.fitsSystemWindows = false
}
companion object {
const val TAG = "CallNotificationActivity"
const val TIMER_START = 1
const val TIMER_COUNT = 12
const val TIMER_DELAY: Long = 5
const val GET_ROOM_RETRY_COUNT: Long = 3
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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() {

View File

@ -145,8 +145,8 @@ public class ContactItem extends AbstractFlexibleItem<ContactItem.ContactItemVie
}
if (TextUtils.isEmpty(participant.getDisplayName()) &&
(participant.getType().equals(Participant.ParticipantType.GUEST) ||
participant.getType().equals(Participant.ParticipantType.USER_FOLLOWING_LINK))) {
(participant.getType() == Participant.ParticipantType.GUEST ||
participant.getType() == Participant.ParticipantType.USER_FOLLOWING_LINK)) {
holder.binding.nameText.setText(NextcloudTalkApplication
.Companion
.getSharedApplication()
@ -167,8 +167,8 @@ public class ContactItem extends AbstractFlexibleItem<ContactItem.ContactItemVie
} else if (
participant.getCalculatedActorType() == Participant.ActorType.GUESTS ||
Participant.ParticipantType.GUEST.equals(participant.getType()) ||
Participant.ParticipantType.GUEST_MODERATOR.equals(participant.getType())) {
participant.getType() == Participant.ParticipantType.GUEST ||
participant.getType() == Participant.ParticipantType.GUEST_MODERATOR) {
String displayName;

View File

@ -274,7 +274,7 @@ public class ConversationItem extends AbstractFlexibleItem<ConversationItem.Conv
}
}
if (Conversation.ConversationType.ROOM_SYSTEM.equals(conversation.getType())) {
if (conversation.getType() == Conversation.ConversationType.ROOM_SYSTEM) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Drawable[] layers = new Drawable[2];

View File

@ -140,8 +140,8 @@ public class ParticipantItem extends AbstractFlexibleItem<ParticipantItem.Partic
}
if (TextUtils.isEmpty(participant.getDisplayName()) &&
(participant.getType().equals(Participant.ParticipantType.GUEST) ||
participant.getType().equals(Participant.ParticipantType.USER_FOLLOWING_LINK))) {
(participant.getType() == Participant.ParticipantType.GUEST ||
participant.getType() == Participant.ParticipantType.USER_FOLLOWING_LINK)) {
holder.binding.nameText.setText(NextcloudTalkApplication
.Companion
.getSharedApplication()
@ -170,8 +170,8 @@ public class ParticipantItem extends AbstractFlexibleItem<ParticipantItem.Partic
holder.binding.avatarDraweeView.setImageResource(R.drawable.ic_circular_mail);
}
} else if (participant.getCalculatedActorType() == Participant.ActorType.GUESTS ||
Participant.ParticipantType.GUEST.equals(participant.getType()) ||
Participant.ParticipantType.GUEST_MODERATOR.equals(participant.getType())) {
participant.getType() == Participant.ParticipantType.GUEST ||
participant.getType() == Participant.ParticipantType.GUEST_MODERATOR) {
String displayName = NextcloudTalkApplication.Companion.getSharedApplication()
.getResources().getString(R.string.nc_guest);

View File

@ -40,13 +40,14 @@ import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.annotation.DrawableRes
import androidx.core.net.toFile
import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.RecyclerView
import autodagger.AutoInjector
import com.github.dhaval2404.imagepicker.ImagePicker
import com.github.dhaval2404.imagepicker.ImagePicker.Companion.getError
import com.github.dhaval2404.imagepicker.ImagePicker.Companion.getFile
import com.github.dhaval2404.imagepicker.ImagePicker.Companion.with
import com.github.dhaval2404.imagepicker.constant.ImageProvider
import com.nextcloud.talk.R
import com.nextcloud.talk.activities.TakePhotoActivity
import com.nextcloud.talk.api.NcApi
@ -486,14 +487,13 @@ class ProfileController : BaseController(R.layout.controller_profile) {
}
private fun sendSelectLocalFileIntent() {
val intent = with(activity!!)
.galleryOnly()
with(activity!!)
.provider(ImageProvider.GALLERY)
.crop()
.cropSquare()
.compress(MAX_SIZE)
.maxResultSize(MAX_SIZE, MAX_SIZE)
.prepareIntent()
startActivityForResult(intent, 1)
.createIntent { intent -> 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) {

View File

@ -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())));

View File

@ -1,23 +0,0 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.events
class CallNotificationClick

View File

@ -1,695 +0,0 @@
/*
* Nextcloud Talk application
*
* @author Andy Scherzinger
* @author Mario Danic
* Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.jobs;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.AudioAttributes;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.service.notification.StatusBarNotification;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import com.bluelinelabs.logansquare.LoganSquare;
import com.nextcloud.talk.R;
import com.nextcloud.talk.activities.CallActivity;
import com.nextcloud.talk.activities.MainActivity;
import com.nextcloud.talk.api.NcApi;
import com.nextcloud.talk.application.NextcloudTalkApplication;
import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager;
import com.nextcloud.talk.data.user.model.User;
import com.nextcloud.talk.models.SignatureVerification;
import com.nextcloud.talk.models.json.chat.ChatUtils;
import com.nextcloud.talk.models.json.conversations.Conversation;
import com.nextcloud.talk.models.json.conversations.RoomOverall;
import com.nextcloud.talk.models.json.notifications.NotificationOverall;
import com.nextcloud.talk.models.json.push.DecryptedPushMessage;
import com.nextcloud.talk.models.json.push.NotificationUser;
import com.nextcloud.talk.receivers.DirectReplyReceiver;
import com.nextcloud.talk.receivers.MarkAsReadReceiver;
import com.nextcloud.talk.utils.ApiUtils;
import com.nextcloud.talk.utils.DoNotDisturbUtils;
import com.nextcloud.talk.utils.NotificationUtils;
import com.nextcloud.talk.utils.PushUtils;
import com.nextcloud.talk.utils.UserIdUtils;
import com.nextcloud.talk.utils.bundle.BundleKeys;
import com.nextcloud.talk.utils.preferences.AppPreferences;
import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder;
import org.parceler.Parcels;
import java.io.IOException;
import java.net.CookieManager;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.util.HashMap;
import java.util.Objects;
import java.util.zip.CRC32;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.inject.Inject;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationCompat.MessagingStyle;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.app.Person;
import androidx.core.app.RemoteInput;
import androidx.emoji.text.EmojiCompat;
import androidx.work.Data;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import autodagger.AutoInjector;
import io.reactivex.Maybe;
import io.reactivex.Observer;
import io.reactivex.disposables.Disposable;
import okhttp3.JavaNetCookieJar;
import okhttp3.OkHttpClient;
import retrofit2.Retrofit;
@AutoInjector(NextcloudTalkApplication.class)
public class NotificationWorker extends Worker {
public static final String TAG = "NotificationWorker";
private static final String CHAT = "chat";
private static final String ROOM = "room";
@Inject
AppPreferences appPreferences;
@Inject
ArbitraryStorageManager arbitraryStorageManager;
@Inject
Retrofit retrofit;
@Inject
OkHttpClient okHttpClient;
private NcApi ncApi;
private DecryptedPushMessage decryptedPushMessage;
private Context context;
private SignatureVerification signatureVerification;
private String conversationType = "one2one";
private String credentials;
private boolean muteCall = false;
private boolean importantConversation = false;
public NotificationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
}
private void showNotificationForCallWithNoPing(Intent intent) {
User user = signatureVerification.getUser();
importantConversation = arbitraryStorageManager.getStorageSetting(
UserIdUtils.INSTANCE.getIdForUser(user),
"important_conversation",
intent.getExtras().getString(BundleKeys.KEY_ROOM_TOKEN))
.map(arbitraryStorage -> {
if (arbitraryStorage != null && arbitraryStorage.getValue() != null) {
return Boolean.parseBoolean(arbitraryStorage.getValue());
} else {
return importantConversation;
}
})
.switchIfEmpty(Maybe.just(importantConversation))
.blockingGet();
Log.e(TAG, "showNotificationForCallWithNoPing: importantConversation: " + importantConversation);
int apiVersion = ApiUtils.getConversationApiVersion(user, new int[] {ApiUtils.APIv4, 1});
ncApi.getRoom(credentials, ApiUtils.getUrlForRoom(apiVersion, user.getBaseUrl(),
intent.getExtras().getString(BundleKeys.KEY_ROOM_TOKEN)))
.blockingSubscribe(new Observer<RoomOverall>() {
@Override
public void onSubscribe(Disposable d) {
// unused atm
}
@Override
public void onNext(RoomOverall roomOverall) {
Conversation conversation = roomOverall.getOcs().getData();
intent.putExtra(BundleKeys.KEY_ROOM, Parcels.wrap(conversation));
if (conversation.getType().equals(Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) ||
(!TextUtils.isEmpty(conversation.getObjectType()) && "share:password".equals
(conversation.getObjectType()))) {
context.startActivity(intent);
} else {
if (conversation.getType().equals(Conversation.ConversationType.ROOM_GROUP_CALL)) {
conversationType = "group";
} else {
conversationType = "public";
}
if (decryptedPushMessage.getNotificationId() != Long.MIN_VALUE) {
showNotificationWithObjectData(intent);
} else {
showNotification(intent);
}
}
muteCall = conversation.getNotificationCalls() != 1;
}
@Override
public void onError(Throwable e) {
// unused atm
}
@Override
public void onComplete() {
// unused atm
}
});
}
private void showNotificationWithObjectData(Intent intent) {
User user = signatureVerification.getUser();
ncApi.getNotification(credentials, ApiUtils.getUrlForNotificationWithId(user.getBaseUrl(),
Long.toString(decryptedPushMessage.getNotificationId())))
.blockingSubscribe(new Observer<NotificationOverall>() {
@Override
public void onSubscribe(Disposable d) {
// unused atm
}
@Override
public void onNext(NotificationOverall notificationOverall) {
com.nextcloud.talk.models.json.notifications.Notification notification =
notificationOverall.getOcs().getNotification();
if (notification.getMessageRichParameters() != null &&
notification.getMessageRichParameters().size() > 0) {
decryptedPushMessage.setText(ChatUtils.Companion.getParsedMessage(
notification.getMessageRich(),
notification.getMessageRichParameters()));
} else {
decryptedPushMessage.setText(notification.getMessage());
}
HashMap<String, HashMap<String, String>> subjectRichParameters = notification
.getSubjectRichParameters();
decryptedPushMessage.setTimestamp(notification.getDatetime().getMillis());
if (subjectRichParameters != null && subjectRichParameters.size() > 0) {
HashMap<String, String> callHashMap = subjectRichParameters.get("call");
HashMap<String, String> userHashMap = subjectRichParameters.get("user");
HashMap<String, String> guestHashMap = subjectRichParameters.get("guest");
if (callHashMap != null && callHashMap.size() > 0 && callHashMap.containsKey("name")) {
if (subjectRichParameters.containsKey("reaction")) {
decryptedPushMessage.setSubject("");
decryptedPushMessage.setText(notification.getSubject());
} else if (Objects.equals(notification.getObjectType(), "chat")) {
decryptedPushMessage.setSubject(Objects.requireNonNull(callHashMap.get("name")));
} else {
decryptedPushMessage.setSubject(Objects.requireNonNull(notification.getSubject()));
}
if (callHashMap.containsKey("call-type")) {
conversationType = callHashMap.get("call-type");
}
}
NotificationUser notificationUser = new NotificationUser();
if (userHashMap != null && !userHashMap.isEmpty()) {
notificationUser.setId(userHashMap.get("id"));
notificationUser.setType(userHashMap.get("type"));
notificationUser.setName(userHashMap.get("name"));
decryptedPushMessage.setNotificationUser(notificationUser);
} else if (guestHashMap != null && !guestHashMap.isEmpty()) {
notificationUser.setId(guestHashMap.get("id"));
notificationUser.setType(guestHashMap.get("type"));
notificationUser.setName(guestHashMap.get("name"));
decryptedPushMessage.setNotificationUser(notificationUser);
}
}
decryptedPushMessage.setObjectId(notification.getObjectId());
showNotification(intent);
}
@Override
public void onError(Throwable e) {
// unused atm
}
@Override
public void onComplete() {
// unused atm
}
});
}
private void showNotification(Intent intent) {
int smallIcon;
Bitmap largeIcon;
String category;
int priority = Notification.PRIORITY_HIGH;
smallIcon = R.drawable.ic_logo;
if (CHAT.equals(decryptedPushMessage.getType()) || ROOM.equals(decryptedPushMessage.getType())) {
category = Notification.CATEGORY_MESSAGE;
} else {
category = Notification.CATEGORY_CALL;
}
switch (conversationType) {
case "one2one":
decryptedPushMessage.setSubject("");
case "group":
largeIcon = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_people_group_black_24px);
break;
case "public":
largeIcon = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_link_black_24px);
break;
default:
// assuming one2one
if (CHAT.equals(decryptedPushMessage.getType()) || ROOM.equals(decryptedPushMessage.getType())) {
largeIcon = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_comment);
} else {
largeIcon = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_call_black_24dp);
}
}
// Use unique request code to make sure that a new PendingIntent gets created for each notification
// See https://github.com/nextcloud/talk-android/issues/2111
int requestCode = (int) System.currentTimeMillis();
int intentFlag;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
intentFlag = PendingIntent.FLAG_MUTABLE;
} else {
intentFlag = 0;
}
PendingIntent pendingIntent = PendingIntent.getActivity(context, requestCode, intent, intentFlag);
Uri uri = Uri.parse(signatureVerification.getUser().getBaseUrl());
String baseUrl = uri.getHost();
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context, "1")
.setLargeIcon(largeIcon)
.setSmallIcon(smallIcon)
.setCategory(category)
.setPriority(priority)
.setSubText(baseUrl)
.setWhen(decryptedPushMessage.getTimestamp())
.setShowWhen(true)
.setContentIntent(pendingIntent)
.setAutoCancel(true);
if (!TextUtils.isEmpty(decryptedPushMessage.getSubject())) {
notificationBuilder.setContentTitle(EmojiCompat.get().process(decryptedPushMessage.getSubject()));
}
if (!TextUtils.isEmpty(decryptedPushMessage.getText())) {
notificationBuilder.setContentText(EmojiCompat.get().process(decryptedPushMessage.getText()));
}
if (Build.VERSION.SDK_INT >= 23) {
// This method should exist since API 21, but some phones don't have it
// So as a safeguard, we don't use it until 23
notificationBuilder.setColor(context.getResources().getColor(R.color.colorPrimary));
}
Bundle notificationInfo = new Bundle();
notificationInfo.putLong(BundleKeys.KEY_INTERNAL_USER_ID,
signatureVerification.getUser().getId());
// could be an ID or a TOKEN
notificationInfo.putString(BundleKeys.KEY_ROOM_TOKEN,
decryptedPushMessage.getId());
notificationInfo.putLong(BundleKeys.KEY_NOTIFICATION_ID,
decryptedPushMessage.getNotificationId());
notificationBuilder.setExtras(notificationInfo);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (CHAT.equals(decryptedPushMessage.getType()) || ROOM.equals(decryptedPushMessage.getType())) {
notificationBuilder.setChannelId(NotificationUtils.NotificationChannels.NOTIFICATION_CHANNEL_MESSAGES_V4.name());
}
} else {
// red color for the lights
notificationBuilder.setLights(0xFFFF0000, 200, 200);
}
notificationBuilder.setContentIntent(pendingIntent);
String groupName = signatureVerification.getUser().getId() + "@" + decryptedPushMessage.getId();
notificationBuilder.setGroup(Long.toString(calculateCRC32(groupName)));
StatusBarNotification activeStatusBarNotification =
NotificationUtils.INSTANCE.findNotificationForRoom(context,
signatureVerification.getUser(),
decryptedPushMessage.getId());
// NOTE - systemNotificationId is an internal ID used on the device only.
// It is NOT the same as the notification ID used in communication with the server.
int systemNotificationId;
if (activeStatusBarNotification != null) {
systemNotificationId = activeStatusBarNotification.getId();
} else {
systemNotificationId = (int) calculateCRC32(String.valueOf(System.currentTimeMillis()));
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
CHAT.equals(decryptedPushMessage.getType()) &&
decryptedPushMessage.getNotificationUser() != null) {
prepareChatNotification(notificationBuilder, activeStatusBarNotification, systemNotificationId);
}
sendNotification(systemNotificationId, notificationBuilder.build());
}
private long calculateCRC32(String s) {
CRC32 crc32 = new CRC32();
crc32.update(s.getBytes());
return crc32.getValue();
}
@RequiresApi(api = Build.VERSION_CODES.N)
private void prepareChatNotification(NotificationCompat.Builder notificationBuilder,
StatusBarNotification activeStatusBarNotification,
int systemNotificationId) {
final NotificationUser notificationUser = decryptedPushMessage.getNotificationUser();
final String userType = notificationUser.getType();
MessagingStyle style = null;
if (activeStatusBarNotification != null) {
style = MessagingStyle.extractMessagingStyleFromNotification(activeStatusBarNotification.getNotification());
}
Person.Builder person =
new Person.Builder()
.setKey(signatureVerification.getUser().getId() + "@" + notificationUser.getId())
.setName(EmojiCompat.get().process(notificationUser.getName()))
.setBot("bot".equals(userType));
notificationBuilder.setOnlyAlertOnce(true);
addReplyAction(notificationBuilder, systemNotificationId);
addMarkAsReadAction(notificationBuilder, systemNotificationId);
if ("user".equals(userType) || "guest".equals(userType)) {
String baseUrl = signatureVerification.getUser().getBaseUrl();
String avatarUrl = "user".equals(userType) ?
ApiUtils.getUrlForAvatar(baseUrl, notificationUser.getId(), false) :
ApiUtils.getUrlForGuestAvatar(baseUrl, notificationUser.getName(), false);
person.setIcon(NotificationUtils.INSTANCE.loadAvatarSync(avatarUrl));
}
notificationBuilder.setStyle(getStyle(person.build(), style));
}
private PendingIntent buildIntentForAction(Class<?> cls, int systemNotificationId, int messageId) {
Intent actualIntent = new Intent(context, cls);
// NOTE - systemNotificationId is an internal ID used on the device only.
// It is NOT the same as the notification ID used in communication with the server.
actualIntent.putExtra(BundleKeys.KEY_SYSTEM_NOTIFICATION_ID, systemNotificationId);
actualIntent.putExtra(BundleKeys.KEY_INTERNAL_USER_ID,
Objects.requireNonNull(signatureVerification.getUser()).getId());
actualIntent.putExtra(BundleKeys.KEY_ROOM_TOKEN, decryptedPushMessage.getId());
actualIntent.putExtra(BundleKeys.KEY_MESSAGE_ID, messageId);
int intentFlag;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
intentFlag = PendingIntent.FLAG_MUTABLE|PendingIntent.FLAG_UPDATE_CURRENT;
} else {
intentFlag = PendingIntent.FLAG_UPDATE_CURRENT;
}
return PendingIntent.getBroadcast(context, systemNotificationId, actualIntent, intentFlag);
}
private void addMarkAsReadAction(NotificationCompat.Builder notificationBuilder, int systemNotificationId) {
if (decryptedPushMessage.getObjectId() != null) {
int messageId = 0;
try {
messageId = parseMessageId(decryptedPushMessage.getObjectId());
} catch (NumberFormatException nfe) {
Log.e(TAG, "Failed to parse messageId from objectId, skip adding mark-as-read action.", nfe);
return;
}
// Build a PendingIntent for the mark as read action
PendingIntent pendingIntent = buildIntentForAction(MarkAsReadReceiver.class,
systemNotificationId,
messageId);
NotificationCompat.Action action =
new NotificationCompat.Action.Builder(R.drawable.ic_eye,
context.getResources().getString(R.string.nc_mark_as_read),
pendingIntent)
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
.setShowsUserInterface(false)
.build();
notificationBuilder.addAction(action);
}
}
@RequiresApi(api = Build.VERSION_CODES.N)
private void addReplyAction(NotificationCompat.Builder notificationBuilder, int systemNotificationId) {
String replyLabel = context.getResources().getString(R.string.nc_reply);
RemoteInput remoteInput = new RemoteInput.Builder(NotificationUtils.KEY_DIRECT_REPLY)
.setLabel(replyLabel)
.build();
// Build a PendingIntent for the reply action
PendingIntent replyPendingIntent = buildIntentForAction(DirectReplyReceiver.class, systemNotificationId, 0);
NotificationCompat.Action replyAction =
new NotificationCompat.Action.Builder(R.drawable.ic_reply, replyLabel, replyPendingIntent)
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
.setShowsUserInterface(false)
// Allows system to generate replies by context of conversation.
// https://developer.android.com/reference/androidx/core/app/NotificationCompat.Action.Builder#setAllowGeneratedReplies(boolean)
// Good question is - do we really want it?
.setAllowGeneratedReplies(true)
.addRemoteInput(remoteInput)
.build();
notificationBuilder.addAction(replyAction);
}
@RequiresApi(api = Build.VERSION_CODES.N)
private MessagingStyle getStyle(Person person, @Nullable MessagingStyle style) {
MessagingStyle newStyle = new MessagingStyle(person);
newStyle.setConversationTitle(decryptedPushMessage.getSubject());
newStyle.setGroupConversation(!"one2one".equals(conversationType));
if (style != null) {
style.getMessages().forEach(message -> newStyle.addMessage(
new MessagingStyle.Message(message.getText(),
message.getTimestamp(),
message.getPerson())));
}
newStyle.addMessage(decryptedPushMessage.getText(), decryptedPushMessage.getTimestamp(), person);
return newStyle;
}
private int parseMessageId(@NonNull String objectId) {
String[] objectIdParts = objectId.split("/");
if (objectIdParts.length < 2) {
throw new NumberFormatException("Invalid objectId, doesn't contain at least one '/'");
} else {
return Integer.parseInt(objectIdParts[1]);
}
}
private void sendNotification(int notificationId, Notification notification) {
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
notificationManager.notify(notificationId, notification);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// On devices with Android 8.0 (Oreo) or later, notification sound will be handled by the system
// if notifications have not been disabled by the user.
return;
}
if (!Notification.CATEGORY_CALL.equals(notification.category) || !muteCall) {
Uri soundUri = NotificationUtils.INSTANCE.getMessageRingtoneUri(context, appPreferences);
if (soundUri != null && !ApplicationWideCurrentRoomHolder.getInstance().isInCall() &&
(DoNotDisturbUtils.INSTANCE.shouldPlaySound() || importantConversation)) {
AudioAttributes.Builder audioAttributesBuilder = new AudioAttributes.Builder().setContentType
(AudioAttributes.CONTENT_TYPE_SONIFICATION);
if (CHAT.equals(decryptedPushMessage.getType()) || ROOM.equals(decryptedPushMessage.getType())) {
audioAttributesBuilder.setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT);
} else {
audioAttributesBuilder.setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST);
}
MediaPlayer mediaPlayer = new MediaPlayer();
try {
mediaPlayer.setDataSource(context, soundUri);
mediaPlayer.setAudioAttributes(audioAttributesBuilder.build());
mediaPlayer.setOnPreparedListener(mp -> mediaPlayer.start());
mediaPlayer.setOnCompletionListener(MediaPlayer::release);
mediaPlayer.prepareAsync();
} catch (IOException e) {
Log.e(TAG, "Failed to set data source");
}
}
}
}
@NonNull
@Override
public Result doWork() {
NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
context = getApplicationContext();
Data data = getInputData();
String subject = data.getString(BundleKeys.KEY_NOTIFICATION_SUBJECT);
String signature = data.getString(BundleKeys.KEY_NOTIFICATION_SIGNATURE);
try {
byte[] base64DecodedSubject = Base64.decode(subject, Base64.DEFAULT);
byte[] base64DecodedSignature = Base64.decode(signature, Base64.DEFAULT);
PushUtils pushUtils = new PushUtils();
PrivateKey privateKey = (PrivateKey) pushUtils.readKeyFromFile(false);
try {
signatureVerification = pushUtils.verifySignature(base64DecodedSignature,
base64DecodedSubject);
if (signatureVerification.getSignatureValid()) {
Cipher cipher = Cipher.getInstance("RSA/None/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] decryptedSubject = cipher.doFinal(base64DecodedSubject);
decryptedPushMessage = LoganSquare.parse(new String(decryptedSubject),
DecryptedPushMessage.class);
decryptedPushMessage.setTimestamp(System.currentTimeMillis());
if (decryptedPushMessage.getDelete()) {
NotificationUtils.INSTANCE.cancelExistingNotificationWithId(
context,
signatureVerification.getUser(),
decryptedPushMessage.getNotificationId());
} else if (decryptedPushMessage.getDeleteAll()) {
NotificationUtils.INSTANCE.cancelAllNotificationsForAccount(
context,
signatureVerification.getUser());
} else if (decryptedPushMessage.getDeleteMultiple()) {
for (long notificationId : decryptedPushMessage.getNotificationIds()) {
NotificationUtils.INSTANCE.cancelExistingNotificationWithId(
context,
signatureVerification.getUser(),
notificationId);
}
} else {
credentials = ApiUtils.getCredentials(signatureVerification.getUser().getUsername(),
signatureVerification.getUser().getToken());
ncApi = retrofit.newBuilder().client(okHttpClient.newBuilder().cookieJar(new
JavaNetCookieJar(new CookieManager())).build()).build().create(NcApi.class);
boolean shouldShowNotification = "spreed".equals(decryptedPushMessage.getApp());
if (shouldShowNotification) {
Intent intent;
Bundle bundle = new Bundle();
boolean startACall = "call".equals(decryptedPushMessage.getType());
if (startACall) {
intent = new Intent(context, CallActivity.class);
} else {
intent = new Intent(context, MainActivity.class);
}
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
bundle.putString(BundleKeys.KEY_ROOM_TOKEN, decryptedPushMessage.getId());
bundle.putParcelable(BundleKeys.KEY_USER_ENTITY,
signatureVerification.getUser());
bundle.putBoolean(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL,
startACall);
intent.putExtras(bundle);
Log.e(TAG, "Notification: " + decryptedPushMessage.getType());
switch (decryptedPushMessage.getType()) {
case "call":
if (bundle.containsKey(BundleKeys.KEY_ROOM_TOKEN)) {
showNotificationForCallWithNoPing(intent);
}
break;
case "room":
if (bundle.containsKey(BundleKeys.KEY_ROOM_TOKEN)) {
showNotificationWithObjectData(intent);
}
break;
case "chat":
if (decryptedPushMessage.getNotificationId() != Long.MIN_VALUE) {
showNotificationWithObjectData(intent);
} else {
showNotification(intent);
}
break;
default:
break;
}
}
}
}
} catch (NoSuchAlgorithmException e1) {
Log.d(TAG, "No proper algorithm to decrypt the message " + e1.getLocalizedMessage());
} catch (NoSuchPaddingException e1) {
Log.d(TAG, "No proper padding to decrypt the message " + e1.getLocalizedMessage());
} catch (InvalidKeyException e1) {
Log.d(TAG, "Invalid private key " + e1.getLocalizedMessage());
}
} catch (Exception exception) {
Log.d(TAG, "Something went very wrong " + exception.getLocalizedMessage());
}
return Result.success();
}
}

View File

@ -0,0 +1,849 @@
/*
* Nextcloud Talk application
*
* @author Andy Scherzinger
* @author Mario Danic
* @author Marcel Hibbe
* Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
* Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.jobs
import android.app.Notification
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Context.NOTIFICATION_SERVICE
import android.content.Intent
import android.graphics.Bitmap
import android.media.AudioAttributes
import android.media.MediaPlayer
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.SystemClock
import android.service.notification.StatusBarNotification
import android.text.TextUtils
import android.util.Base64
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.Person
import androidx.core.app.RemoteInput
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import androidx.emoji.text.EmojiCompat
import androidx.work.Data
import androidx.work.Worker
import androidx.work.WorkerParameters
import autodagger.AutoInjector
import com.bluelinelabs.logansquare.LoganSquare
import com.nextcloud.talk.R
import com.nextcloud.talk.activities.CallNotificationActivity
import com.nextcloud.talk.activities.MainActivity
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager
import com.nextcloud.talk.models.SignatureVerification
import com.nextcloud.talk.models.json.chat.ChatUtils.Companion.getParsedMessage
import com.nextcloud.talk.models.json.conversations.RoomOverall
import com.nextcloud.talk.models.json.notifications.NotificationOverall
import com.nextcloud.talk.models.json.participants.Participant
import com.nextcloud.talk.models.json.participants.ParticipantsOverall
import com.nextcloud.talk.models.json.push.DecryptedPushMessage
import com.nextcloud.talk.models.json.push.NotificationUser
import com.nextcloud.talk.receivers.DirectReplyReceiver
import com.nextcloud.talk.receivers.MarkAsReadReceiver
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DoNotDisturbUtils.shouldPlaySound
import com.nextcloud.talk.utils.NotificationUtils
import com.nextcloud.talk.utils.NotificationUtils.cancelAllNotificationsForAccount
import com.nextcloud.talk.utils.NotificationUtils.cancelNotification
import com.nextcloud.talk.utils.NotificationUtils.findNotificationForRoom
import com.nextcloud.talk.utils.NotificationUtils.getCallRingtoneUri
import com.nextcloud.talk.utils.NotificationUtils.getMessageRingtoneUri
import com.nextcloud.talk.utils.NotificationUtils.loadAvatarSync
import com.nextcloud.talk.utils.PushUtils
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FROM_NOTIFICATION_START_CALL
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_MESSAGE_ID
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_NOTIFICATION_ID
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SYSTEM_NOTIFICATION_ID
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY
import com.nextcloud.talk.utils.preferences.AppPreferences
import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder
import io.reactivex.Observable
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import okhttp3.JavaNetCookieJar
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import java.io.IOException
import java.net.CookieManager
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import java.security.PrivateKey
import java.util.concurrent.TimeUnit
import java.util.function.Consumer
import java.util.zip.CRC32
import javax.crypto.Cipher
import javax.crypto.NoSuchPaddingException
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class NotificationWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
@Inject
lateinit var appPreferences: AppPreferences
@JvmField
@Inject
var arbitraryStorageManager: ArbitraryStorageManager? = null
@JvmField
@Inject
var retrofit: Retrofit? = null
@JvmField
@Inject
var okHttpClient: OkHttpClient? = null
private lateinit var credentials: String
private lateinit var ncApi: NcApi
private lateinit var pushMessage: DecryptedPushMessage
private lateinit var signatureVerification: SignatureVerification
private var context: Context? = null
private var conversationType: String? = "one2one"
private var muteCall = false
private var importantConversation = false
private lateinit var notificationManager: NotificationManagerCompat
override fun doWork(): Result {
sharedApplication!!.componentApplication.inject(this)
context = applicationContext
initDecryptedData(inputData)
initNcApiAndCredentials()
notificationManager = NotificationManagerCompat.from(context!!)
pushMessage.timestamp = System.currentTimeMillis()
Log.d(TAG, pushMessage.toString())
Log.d(TAG, "pushMessage.id (=KEY_ROOM_TOKEN): " + pushMessage.id)
Log.d(TAG, "pushMessage.notificationId: " + pushMessage.notificationId)
Log.d(TAG, "pushMessage.notificationIds: " + pushMessage.notificationIds)
Log.d(TAG, "pushMessage.timestamp: " + pushMessage.timestamp)
if (pushMessage.delete) {
cancelNotification(context, signatureVerification.user!!, pushMessage.notificationId)
} else if (pushMessage.deleteAll) {
cancelAllNotificationsForAccount(context, signatureVerification.user!!)
} else if (pushMessage.deleteMultiple) {
for (notificationId in pushMessage.notificationIds!!) {
cancelNotification(context, signatureVerification.user!!, notificationId)
}
} else if (isSpreedNotification()) {
Log.d(TAG, "pushMessage.type: " + pushMessage.type)
when (pushMessage.type) {
"chat" -> handleChatNotification()
"room" -> handleRoomNotification()
"call" -> handleCallNotification()
else -> Log.e(TAG, "unknown pushMessage.type")
}
} else {
Log.d(TAG, "a pushMessage that is not for spreed was received.")
}
return Result.success()
}
private fun handleChatNotification() {
val chatIntent = Intent(context, MainActivity::class.java)
chatIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
val chatBundle = Bundle()
chatBundle.putString(KEY_ROOM_TOKEN, pushMessage.id)
chatBundle.putParcelable(KEY_USER_ENTITY, signatureVerification.user)
chatBundle.putBoolean(KEY_FROM_NOTIFICATION_START_CALL, false)
chatIntent.putExtras(chatBundle)
if (pushMessage.notificationId != Long.MIN_VALUE) {
showNotificationWithObjectData(chatIntent)
} else {
showNotification(chatIntent)
}
}
/**
* handle messages with type 'room', e.g. "xxx invited you to a group conversation"
*/
private fun handleRoomNotification() {
val intent = Intent(context, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
val bundle = Bundle()
bundle.putString(KEY_ROOM_TOKEN, pushMessage.id)
bundle.putParcelable(KEY_USER_ENTITY, signatureVerification.user)
bundle.putBoolean(KEY_FROM_NOTIFICATION_START_CALL, false)
intent.putExtras(bundle)
if (bundle.containsKey(KEY_ROOM_TOKEN)) {
showNotificationWithObjectData(intent)
}
}
private fun handleCallNotification() {
val fullScreenIntent = Intent(context, CallNotificationActivity::class.java)
val bundle = Bundle()
bundle.putString(KEY_ROOM_TOKEN, pushMessage.id)
bundle.putParcelable(KEY_USER_ENTITY, signatureVerification.user)
bundle.putBoolean(KEY_FROM_NOTIFICATION_START_CALL, true)
fullScreenIntent.putExtras(bundle)
fullScreenIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
val requestCode = System.currentTimeMillis().toInt()
val fullScreenPendingIntent = PendingIntent.getActivity(
context,
requestCode,
fullScreenIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
)
val soundUri = getCallRingtoneUri(applicationContext, appPreferences)
val notificationChannelId = NotificationUtils
.NotificationChannels.NOTIFICATION_CHANNEL_CALLS_V4.name
val uri = Uri.parse(signatureVerification.user!!.baseUrl)
val baseUrl = uri.host
val notification =
NotificationCompat.Builder(applicationContext, notificationChannelId)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_CALL)
.setSmallIcon(R.drawable.ic_call_black_24dp)
.setSubText(baseUrl)
.setShowWhen(true)
.setWhen(pushMessage.timestamp)
.setContentTitle(EmojiCompat.get().process(pushMessage.subject))
.setAutoCancel(true)
.setOngoing(true)
.setContentIntent(fullScreenPendingIntent)
.setFullScreenIntent(fullScreenPendingIntent, true)
.setSound(soundUri)
.build()
notification.flags = notification.flags or Notification.FLAG_INSISTENT
sendNotification(pushMessage.timestamp.toInt(), notification)
checkIfCallIsActive(signatureVerification, pushMessage)
}
private fun initNcApiAndCredentials() {
credentials = ApiUtils.getCredentials(
signatureVerification.user!!.username,
signatureVerification.user!!.token
)
ncApi = retrofit!!.newBuilder().client(
okHttpClient!!.newBuilder().cookieJar(
JavaNetCookieJar(
CookieManager()
)
).build()
).build().create(
NcApi::class.java
)
}
@Suppress("TooGenericExceptionCaught", "NestedBlockDepth", "ComplexMethod", "LongMethod")
private fun initDecryptedData(inputData: Data) {
val subject = inputData.getString(BundleKeys.KEY_NOTIFICATION_SUBJECT)
val signature = inputData.getString(BundleKeys.KEY_NOTIFICATION_SIGNATURE)
try {
val base64DecodedSubject = Base64.decode(subject, Base64.DEFAULT)
val base64DecodedSignature = Base64.decode(signature, Base64.DEFAULT)
val pushUtils = PushUtils()
val privateKey = pushUtils.readKeyFromFile(false) as PrivateKey
try {
signatureVerification = pushUtils.verifySignature(
base64DecodedSignature,
base64DecodedSubject
)
if (signatureVerification.signatureValid) {
val cipher = Cipher.getInstance("RSA/None/PKCS1Padding")
cipher.init(Cipher.DECRYPT_MODE, privateKey)
val decryptedSubject = cipher.doFinal(base64DecodedSubject)
pushMessage = LoganSquare.parse(
String(decryptedSubject),
DecryptedPushMessage::class.java
)
}
} catch (e: NoSuchAlgorithmException) {
Log.e(TAG, "No proper algorithm to decrypt the message ", e)
} catch (e: NoSuchPaddingException) {
Log.e(TAG, "No proper padding to decrypt the message ", e)
} catch (e: InvalidKeyException) {
Log.e(TAG, "Invalid private key ", e)
}
} catch (e: Exception) {
Log.e(TAG, "Error occurred while initializing decoded data ", e)
}
}
private fun isSpreedNotification() = SPREED_APP == pushMessage.app
private fun showNotificationWithObjectData(intent: Intent) {
val user = signatureVerification.user
// see https://github.com/nextcloud/notifications/blob/master/docs/ocs-endpoint-v2.md
ncApi.getNotification(
credentials,
ApiUtils.getUrlForNotificationWithId(
user!!.baseUrl,
(pushMessage.notificationId!!).toString()
)
)
.blockingSubscribe(object : Observer<NotificationOverall> {
override fun onSubscribe(d: Disposable) {
// unused atm
}
override fun onNext(notificationOverall: NotificationOverall) {
val ncNotification = notificationOverall.ocs!!.notification
if (ncNotification!!.messageRichParameters != null &&
ncNotification.messageRichParameters!!.size > 0
) {
pushMessage.text = getParsedMessage(
ncNotification.messageRich,
ncNotification.messageRichParameters
)
} else {
pushMessage.text = ncNotification.message
}
val subjectRichParameters = ncNotification.subjectRichParameters
pushMessage.timestamp = ncNotification.datetime!!.millis
if (subjectRichParameters != null && subjectRichParameters.size > 0) {
val callHashMap = subjectRichParameters["call"]
val userHashMap = subjectRichParameters["user"]
val guestHashMap = subjectRichParameters["guest"]
if (callHashMap != null && callHashMap.size > 0 && callHashMap.containsKey("name")) {
if (subjectRichParameters.containsKey("reaction")) {
pushMessage.subject = ""
pushMessage.text = ncNotification.subject
} else if (ncNotification.objectType == "chat") {
pushMessage.subject = callHashMap["name"]!!
} else {
pushMessage.subject = ncNotification.subject!!
}
if (callHashMap.containsKey("call-type")) {
conversationType = callHashMap["call-type"]
}
}
val notificationUser = NotificationUser()
if (userHashMap != null && userHashMap.isNotEmpty()) {
notificationUser.id = userHashMap["id"]
notificationUser.type = userHashMap["type"]
notificationUser.name = userHashMap["name"]
pushMessage.notificationUser = notificationUser
} else if (guestHashMap != null && guestHashMap.isNotEmpty()) {
notificationUser.id = guestHashMap["id"]
notificationUser.type = guestHashMap["type"]
notificationUser.name = guestHashMap["name"]
pushMessage.notificationUser = notificationUser
}
}
pushMessage.objectId = ncNotification.objectId
showNotification(intent)
}
override fun onError(e: Throwable) {
// unused atm
}
override fun onComplete() {
// unused atm
}
})
}
@Suppress("MagicNumber")
private fun showNotification(intent: Intent) {
val largeIcon: Bitmap
val priority = NotificationCompat.PRIORITY_HIGH
val smallIcon: Int = R.drawable.ic_logo
val category: String = if (CHAT == pushMessage.type || ROOM == pushMessage.type) {
Notification.CATEGORY_MESSAGE
} else {
Notification.CATEGORY_CALL
}
when (conversationType) {
"one2one" -> {
pushMessage.subject = ""
largeIcon = ContextCompat.getDrawable(context!!, R.drawable.ic_people_group_black_24px)?.toBitmap()!!
}
"group" ->
largeIcon = ContextCompat.getDrawable(context!!, R.drawable.ic_people_group_black_24px)?.toBitmap()!!
"public" -> largeIcon = ContextCompat.getDrawable(context!!, R.drawable.ic_link_black_24px)?.toBitmap()!!
else -> // assuming one2one
largeIcon = if (CHAT == pushMessage.type || ROOM == pushMessage.type) {
ContextCompat.getDrawable(context!!, R.drawable.ic_comment)?.toBitmap()!!
} else {
ContextCompat.getDrawable(context!!, R.drawable.ic_call_black_24dp)?.toBitmap()!!
}
}
// Use unique request code to make sure that a new PendingIntent gets created for each notification
// See https://github.com/nextcloud/talk-android/issues/2111
val requestCode = System.currentTimeMillis().toInt()
val intentFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE
} else {
0
}
val pendingIntent = PendingIntent.getActivity(context, requestCode, intent, intentFlag)
val uri = Uri.parse(signatureVerification.user!!.baseUrl)
val baseUrl = uri.host
val notificationBuilder = NotificationCompat.Builder(context!!, "1")
.setLargeIcon(largeIcon)
.setSmallIcon(smallIcon)
.setCategory(category)
.setPriority(priority)
.setSubText(baseUrl)
.setWhen(pushMessage.timestamp)
.setShowWhen(true)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
if (!TextUtils.isEmpty(pushMessage.subject)) {
notificationBuilder.setContentTitle(
EmojiCompat.get().process(pushMessage.subject)
)
}
if (!TextUtils.isEmpty(pushMessage.text)) {
notificationBuilder.setContentText(
EmojiCompat.get().process(pushMessage.text!!)
)
}
if (Build.VERSION.SDK_INT >= 23) {
// This method should exist since API 21, but some phones don't have it
// So as a safeguard, we don't use it until 23
notificationBuilder.color = context!!.resources.getColor(R.color.colorPrimary)
}
val notificationInfoBundle = Bundle()
notificationInfoBundle.putLong(KEY_INTERNAL_USER_ID, signatureVerification.user!!.id!!)
// could be an ID or a TOKEN
notificationInfoBundle.putString(KEY_ROOM_TOKEN, pushMessage.id)
notificationInfoBundle.putLong(KEY_NOTIFICATION_ID, pushMessage.notificationId!!)
notificationBuilder.setExtras(notificationInfoBundle)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (CHAT == pushMessage.type || ROOM == pushMessage.type) {
notificationBuilder.setChannelId(
NotificationUtils.NotificationChannels.NOTIFICATION_CHANNEL_MESSAGES_V4.name
)
}
} else {
// red color for the lights
notificationBuilder.setLights(-0x10000, 200, 200)
}
notificationBuilder.setContentIntent(pendingIntent)
val groupName = signatureVerification.user!!.id.toString() + "@" + pushMessage.id
notificationBuilder.setGroup(calculateCRC32(groupName).toString())
val activeStatusBarNotification = findNotificationForRoom(
context,
signatureVerification.user!!,
pushMessage.id!!
)
// NOTE - systemNotificationId is an internal ID used on the device only.
// It is NOT the same as the notification ID used in communication with the server.
val systemNotificationId: Int =
activeStatusBarNotification?.id ?: calculateCRC32(System.currentTimeMillis().toString()).toInt()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && CHAT == pushMessage.type &&
pushMessage.notificationUser != null
) {
prepareChatNotification(notificationBuilder, activeStatusBarNotification, systemNotificationId)
}
sendNotification(systemNotificationId, notificationBuilder.build())
}
private fun calculateCRC32(s: String): Long {
val crc32 = CRC32()
crc32.update(s.toByteArray())
return crc32.value
}
@RequiresApi(api = Build.VERSION_CODES.N)
private fun prepareChatNotification(
notificationBuilder: NotificationCompat.Builder,
activeStatusBarNotification: StatusBarNotification?,
systemNotificationId: Int
) {
val notificationUser = pushMessage.notificationUser
val userType = notificationUser!!.type
var style: NotificationCompat.MessagingStyle? = null
if (activeStatusBarNotification != null) {
style = NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(
activeStatusBarNotification.notification
)
}
val person = Person.Builder()
.setKey(signatureVerification.user!!.id.toString() + "@" + notificationUser.id)
.setName(EmojiCompat.get().process(notificationUser.name!!))
.setBot("bot" == userType)
notificationBuilder.setOnlyAlertOnce(true)
addReplyAction(notificationBuilder, systemNotificationId)
addMarkAsReadAction(notificationBuilder, systemNotificationId)
if ("user" == userType || "guest" == userType) {
val baseUrl = signatureVerification.user!!.baseUrl
val avatarUrl = if ("user" == userType) ApiUtils.getUrlForAvatar(
baseUrl,
notificationUser.id,
false
) else ApiUtils.getUrlForGuestAvatar(baseUrl, notificationUser.name, false)
person.setIcon(loadAvatarSync(avatarUrl))
}
notificationBuilder.setStyle(getStyle(person.build(), style))
}
private fun buildIntentForAction(cls: Class<*>, systemNotificationId: Int, messageId: Int): PendingIntent {
val actualIntent = Intent(context, cls)
// NOTE - systemNotificationId is an internal ID used on the device only.
// It is NOT the same as the notification ID used in communication with the server.
actualIntent.putExtra(KEY_SYSTEM_NOTIFICATION_ID, systemNotificationId)
actualIntent.putExtra(KEY_INTERNAL_USER_ID, signatureVerification.user?.id)
actualIntent.putExtra(KEY_ROOM_TOKEN, pushMessage.id)
actualIntent.putExtra(KEY_MESSAGE_ID, messageId)
val intentFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
return PendingIntent.getBroadcast(context, systemNotificationId, actualIntent, intentFlag)
}
private fun addMarkAsReadAction(notificationBuilder: NotificationCompat.Builder, systemNotificationId: Int) {
if (pushMessage.objectId != null) {
val messageId: Int = try {
parseMessageId(pushMessage.objectId!!)
} catch (nfe: NumberFormatException) {
Log.e(TAG, "Failed to parse messageId from objectId, skip adding mark-as-read action.", nfe)
return
}
val pendingIntent = buildIntentForAction(
MarkAsReadReceiver::class.java,
systemNotificationId,
messageId
)
val action = NotificationCompat.Action.Builder(
R.drawable.ic_eye,
context!!.resources.getString(R.string.nc_mark_as_read),
pendingIntent
)
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
.setShowsUserInterface(false)
.build()
notificationBuilder.addAction(action)
}
}
@RequiresApi(api = Build.VERSION_CODES.N)
private fun addReplyAction(notificationBuilder: NotificationCompat.Builder, systemNotificationId: Int) {
val replyLabel = context!!.resources.getString(R.string.nc_reply)
val remoteInput = RemoteInput.Builder(NotificationUtils.KEY_DIRECT_REPLY)
.setLabel(replyLabel)
.build()
val replyPendingIntent = buildIntentForAction(DirectReplyReceiver::class.java, systemNotificationId, 0)
val replyAction = NotificationCompat.Action.Builder(R.drawable.ic_reply, replyLabel, replyPendingIntent)
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
.setShowsUserInterface(false)
.setAllowGeneratedReplies(true)
.addRemoteInput(remoteInput)
.build()
notificationBuilder.addAction(replyAction)
}
@RequiresApi(api = Build.VERSION_CODES.N)
private fun getStyle(person: Person, style: NotificationCompat.MessagingStyle?): NotificationCompat.MessagingStyle {
val newStyle = NotificationCompat.MessagingStyle(person)
newStyle.conversationTitle = pushMessage.subject
newStyle.isGroupConversation = "one2one" != conversationType
style?.messages?.forEach(
Consumer { message: NotificationCompat.MessagingStyle.Message ->
newStyle.addMessage(
NotificationCompat.MessagingStyle.Message(
message.text,
message.timestamp,
message.person
)
)
}
)
newStyle.addMessage(pushMessage.text, pushMessage.timestamp, person)
return newStyle
}
@Throws(NumberFormatException::class)
private fun parseMessageId(objectId: String): Int {
val objectIdParts = objectId.split("/".toRegex()).toTypedArray()
return if (objectIdParts.size < 2) {
throw NumberFormatException("Invalid objectId, doesn't contain at least one '/'")
} else {
objectIdParts[1].toInt()
}
}
private fun sendNotification(notificationId: Int, notification: Notification) {
Log.d(TAG, "show notification with id $notificationId")
notificationManager.notify(notificationId, notification)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// On devices with Android 8.0 (Oreo) or later, notification sound will be handled by the system
// if notifications have not been disabled by the user.
return
}
if (Notification.CATEGORY_CALL != notification.category || !muteCall) {
val soundUri = getMessageRingtoneUri(context!!, appPreferences)
if (soundUri != null && !ApplicationWideCurrentRoomHolder.getInstance().isInCall &&
(shouldPlaySound() || importantConversation)
) {
val audioAttributesBuilder =
AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
if (CHAT == pushMessage.type || ROOM == pushMessage.type) {
audioAttributesBuilder.setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT)
} else {
audioAttributesBuilder.setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST)
}
val mediaPlayer = MediaPlayer()
try {
mediaPlayer.setDataSource(context!!, soundUri)
mediaPlayer.setAudioAttributes(audioAttributesBuilder.build())
mediaPlayer.setOnPreparedListener { mediaPlayer.start() }
mediaPlayer.setOnCompletionListener { obj: MediaPlayer -> obj.release() }
mediaPlayer.prepareAsync()
} catch (e: IOException) {
Log.e(TAG, "Failed to set data source")
}
}
}
}
private fun removeNotification(notificationId: Int) {
Log.d(TAG, "removed notification with id $notificationId")
notificationManager.cancel(notificationId)
}
private fun checkIfCallIsActive(
signatureVerification: SignatureVerification,
decryptedPushMessage: DecryptedPushMessage
) {
Log.d(TAG, "checkIfCallIsActive")
var hasParticipantsInCall = true
var inCallOnDifferentDevice = false
val apiVersion = ApiUtils.getConversationApiVersion(
signatureVerification.user,
intArrayOf(ApiUtils.APIv4, 1)
)
var isCallNotificationVisible = true
ncApi.getPeersForCall(
credentials,
ApiUtils.getUrlForCall(
apiVersion,
signatureVerification.user!!.baseUrl,
decryptedPushMessage.id
)
)
.repeatWhen { completed ->
completed.zipWith(Observable.range(TIMER_START, TIMER_COUNT)) { _, i -> i }
.flatMap { Observable.timer(TIMER_DELAY, TimeUnit.SECONDS) }
.takeWhile { isCallNotificationVisible && hasParticipantsInCall && !inCallOnDifferentDevice }
}
.subscribeOn(Schedulers.io())
.subscribe(object : Observer<ParticipantsOverall> {
override fun onSubscribe(d: Disposable) = Unit
@RequiresApi(Build.VERSION_CODES.M)
override fun onNext(participantsOverall: ParticipantsOverall) {
val participantList: List<Participant> = participantsOverall.ocs!!.data!!
hasParticipantsInCall = participantList.isNotEmpty()
if (hasParticipantsInCall) {
for (participant in participantList) {
if (participant.actorId == signatureVerification.user!!.userId &&
participant.actorType == Participant.ActorType.USERS
) {
inCallOnDifferentDevice = true
break
}
}
}
if (inCallOnDifferentDevice) {
Log.d(TAG, "inCallOnDifferentDevice is true")
removeNotification(decryptedPushMessage.timestamp.toInt())
}
if (!hasParticipantsInCall) {
showMissedCallNotification()
Log.d(TAG, "no participants in call")
removeNotification(decryptedPushMessage.timestamp.toInt())
}
isCallNotificationVisible = isCallNotificationVisible(decryptedPushMessage)
}
override fun onError(e: Throwable) {
Log.e(TAG, "Error in getPeersForCall", e)
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onComplete() {
if (isCallNotificationVisible) {
// this state can be reached when call timeout is reached.
showMissedCallNotification()
}
removeNotification(decryptedPushMessage.timestamp.toInt())
}
})
}
fun showMissedCallNotification() {
val apiVersion = ApiUtils.getConversationApiVersion(
signatureVerification.user,
intArrayOf(
ApiUtils.APIv4,
ApiUtils.APIv3, 1
)
)
ncApi.getRoom(
credentials,
ApiUtils.getUrlForRoom(
apiVersion, signatureVerification.user?.baseUrl,
pushMessage.id
)
)
.subscribeOn(Schedulers.io())
.retry(GET_ROOM_RETRY_COUNT)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<RoomOverall> {
override fun onSubscribe(d: Disposable) {
// unused atm
}
override fun onNext(roomOverall: RoomOverall) {
val currentConversation = roomOverall.ocs!!.data
val notificationBuilder: NotificationCompat.Builder?
notificationBuilder = NotificationCompat.Builder(
context!!,
NotificationUtils.NotificationChannels
.NOTIFICATION_CHANNEL_MESSAGES_V4.name
)
val notification: Notification = notificationBuilder
.setContentTitle(
String.format(
context!!.resources.getString(R.string.nc_missed_call),
currentConversation!!.displayName
)
)
.setSmallIcon(R.drawable.ic_baseline_phone_missed_24)
.setOngoing(false)
.setAutoCancel(true)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setContentIntent(getIntentToOpenConversation())
.build()
val notificationId: Int = SystemClock.uptimeMillis().toInt()
notificationManager.notify(notificationId, notification)
Log.d(TAG, "'you missed a call' notification was created")
}
override fun onError(e: Throwable) {
Log.e(TAG, "An error occurred while fetching room for the 'missed call' notification", e)
}
override fun onComplete() {
// unused atm
}
})
}
private fun getIntentToOpenConversation(): PendingIntent? {
val bundle = Bundle()
val intent = Intent(context, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
bundle.putString(KEY_ROOM_TOKEN, pushMessage.id)
bundle.putParcelable(KEY_USER_ENTITY, signatureVerification.user)
bundle.putBoolean(KEY_FROM_NOTIFICATION_START_CALL, false)
intent.putExtras(bundle)
val requestCode = System.currentTimeMillis().toInt()
val intentFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE
} else {
0
}
return PendingIntent.getActivity(context, requestCode, intent, intentFlag)
}
@RequiresApi(Build.VERSION_CODES.M)
private fun isCallNotificationVisible(decryptedPushMessage: DecryptedPushMessage): Boolean {
var isVisible = false
val notificationManager = context!!.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
val notifications = notificationManager.activeNotifications
for (notification in notifications) {
if (notification.id == decryptedPushMessage.timestamp.toInt()) {
isVisible = true
break
}
}
return isVisible
}
companion object {
val TAG = NotificationWorker::class.simpleName
private const val CHAT = "chat"
private const val ROOM = "room"
private const val SPREED_APP = "spreed"
private const val TIMER_START = 1
private const val TIMER_COUNT = 12
private const val TIMER_DELAY: Long = 5
private const val GET_ROOM_RETRY_COUNT: Long = 3
}
}

View File

@ -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,

View File

@ -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 {

View File

@ -56,7 +56,7 @@ data class Notification(
@JsonField(name = ["messageRich"])
var messageRich: String?,
@JsonField(name = ["messageRichParameters"])
var messageRichParameters: HashMap<String, HashMap<String, String>>?,
var messageRichParameters: HashMap<String?, HashMap<String?, String?>>?,
@JsonField(name = ["link"])
var link: String?,
@JsonField(name = ["actions"])

View File

@ -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"])

View File

@ -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

View File

@ -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)

View File

@ -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;
}

View File

@ -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)

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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)) {

View File

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="#000000" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M6.5,5.5L12,11l7,-7 -1,-1 -6,6 -4.5,-4.5L11,4.5L11,3L5,3v6h1.5L6.5,5.5zM23.71,16.67C20.66,13.78 16.54,12 12,12 7.46,12 3.34,13.78 0.29,16.67c-0.18,0.18 -0.29,0.43 -0.29,0.71s0.11,0.53 0.29,0.71l2.48,2.48c0.18,0.18 0.43,0.29 0.71,0.29 0.27,0 0.52,-0.11 0.7,-0.28 0.79,-0.74 1.69,-1.36 2.66,-1.85 0.33,-0.16 0.56,-0.5 0.56,-0.9v-3.1c1.45,-0.48 3,-0.73 4.6,-0.73 1.6,0 3.15,0.25 4.6,0.72v3.1c0,0.39 0.23,0.74 0.56,0.9 0.98,0.49 1.87,1.12 2.67,1.85 0.18,0.18 0.43,0.28 0.7,0.28 0.28,0 0.53,-0.11 0.71,-0.29l2.48,-2.48c0.18,-0.18 0.29,-0.43 0.29,-0.71s-0.12,-0.52 -0.3,-0.7z"/>
</vector>

View File

@ -173,7 +173,6 @@
<string name="nc_new_mention">إشارات غير مقروءة</string>
<string name="nc_new_messages">رسائل غير مقروءة</string>
<string name="nc_new_password">كلمة سرية جديدة</string>
<string name="nc_nextcloud_talk_app_not_installed">تطبيق %1$s غير مثبت على الخادم، جارِ الإلغاء</string>
<string name="nc_nick_guest">ضيف</string>
<string name="nc_no">لا</string>
<string name="nc_no_messages_yet">ليس لديك رسائل بعد</string>

View File

@ -118,7 +118,6 @@
<string name="nc_never">Never joined</string>
<string name="nc_new_conversation">New conversation</string>
<string name="nc_new_password">New password</string>
<string name="nc_nextcloud_talk_app_not_installed">%1$s app not installed on the server, aborting</string>
<string name="nc_nick_guest">Guest</string>
<string name="nc_no">No</string>
<string name="nc_no_messages_yet">No messages yet</string>

View File

@ -187,13 +187,13 @@
<string name="nc_message_read">Съобщението е прочетено</string>
<string name="nc_message_sent">Съобщението е изпратено</string>
<string name="nc_microphone_permission_permanently_denied">За активиране на гласова комуникация, моля, дайте право на „Микрофон“ в системните настройки.</string>
<string name="nc_missed_call">Пропуснахте обаждане от %s</string>
<string name="nc_moderator">Модератор</string>
<string name="nc_never">Никога не е присъединяван</string>
<string name="nc_new_conversation">Нов разговор</string>
<string name="nc_new_mention">Непрочетени споменавания</string>
<string name="nc_new_messages">Непрочетени съобщения.</string>
<string name="nc_new_password">Нова парола</string>
<string name="nc_nextcloud_talk_app_not_installed">Приложението %1$s не е инсталирано на сървъра, прекратяване</string>
<string name="nc_nick_guest">Гост</string>
<string name="nc_no">Не</string>
<string name="nc_no_messages_yet">Няма съобщения.</string>

View File

@ -4,6 +4,8 @@
<string name="appbar_search_in">Cerca a %s</string>
<string name="audio_output_dialog_headline">Sortida d\'àudio</string>
<string name="audio_output_phone">Telèfon</string>
<string name="audio_output_speaker">Altaveu</string>
<string name="audio_output_wired_headset">Auriculars per cable</string>
<string name="avatar">Avatar</string>
<string name="away">Absent</string>
<string name="clear_status_message">Esborrar el missatge d\'estat</string>
@ -28,12 +30,15 @@
<string name="menu_item_sort_by_size_biggest_first">Més gran primer</string>
<string name="menu_item_sort_by_size_smallest_first">Més petit primer</string>
<string name="message_search_begin_empty">No s\'han trobat resultats</string>
<string name="message_search_begin_typing">Comença a escriure per cercar ...</string>
<string name="message_search_hint">Cerca ...</string>
<string name="messages">Missatges</string>
<string name="nc_Server_account_imported">El compte que heu seleccionat s\'ha importat i ja és disponible</string>
<string name="nc_about">Quant a</string>
<string name="nc_account_chooser_active_user">Usuari actiu</string>
<string name="nc_account_chooser_add_account">Afegeix un compte</string>
<string name="nc_account_scheduled_for_deletion">El compte està planificat per ser suprimit i no es pot canviar</string>
<string name="nc_action_open_main_menu">Obre el menú principal</string>
<string name="nc_add_attachment">Afegeix un adjunt</string>
<string name="nc_add_emojis">Afegeix emoji</string>
<string name="nc_add_participants">Afegeix participants</string>
@ -62,6 +67,7 @@
<string name="nc_common_set">Estableix</string>
<string name="nc_common_skip">Omet</string>
<string name="nc_configure_cert_auth">Selecciona el certificat dautenticació</string>
<string name="nc_connecting_call">S\'està connectant …</string>
<string name="nc_contacts_done">Fet</string>
<string name="nc_conversation_link">Enllaç de conversa</string>
<string name="nc_conversation_menu_conversation_info">Informació de la conversa</string>
@ -85,6 +91,8 @@
<string name="nc_display_name_not_fetched">No sha pogut obtenir el nom de visualització, s\'està avortant</string>
<string name="nc_display_name_not_stored">No s\'ha pogut emmagatzemar el nom de visualització, s\'està avortant</string>
<string name="nc_email">Correu</string>
<string name="nc_expire_message_eight_hours">8 hores</string>
<string name="nc_expire_message_four_weeks">4 setmanes</string>
<string name="nc_expire_message_off">Desactivada</string>
<string name="nc_expire_message_one_day">1 dia</string>
<string name="nc_expire_message_one_hour">1 hora</string>
@ -105,6 +113,7 @@
<string name="nc_guest_access_password_dialog_hint">Introduïu una contrasenya</string>
<string name="nc_guest_access_password_title">Protecció amb contrasenya</string>
<string name="nc_guest_access_password_weak_alert_title">Contrasenya feble</string>
<string name="nc_hint_enter_a_message">Introduïu un missatge …</string>
<string name="nc_important_conversation">Conversa important</string>
<string name="nc_important_conversation_desc">Les notificacions d\'aquesta conversa anul·laran els paràmetres de no destorbar.</string>
<string name="nc_join_via_link">Uniu-vos amb un enllaç</string>
@ -118,6 +127,7 @@
<string name="nc_limit_hit">S\'ha arribat al límit de %s caràcters </string>
<string name="nc_lobby">Vestíbul</string>
<string name="nc_lobby_waiting">Esteu esperant al vestíbul.</string>
<string name="nc_location_current_position_description">La vostra ubicació actual</string>
<string name="nc_locked_tap_to_unlock">Toca per desblocar</string>
<string name="nc_make_call_private">Fes que la conversa sigui privada</string>
<string name="nc_make_call_public">Fes que la conversa sigui pública</string>
@ -132,7 +142,6 @@
<string name="nc_new_conversation">Nova conversa</string>
<string name="nc_new_messages">Missatges sense llegir</string>
<string name="nc_new_password">Nova contrasenya</string>
<string name="nc_nextcloud_talk_app_not_installed">L\'aplicació %1$s no està instal·lada al servidor, s\'està avortant</string>
<string name="nc_nick_guest">Convidat</string>
<string name="nc_no">No</string>
<string name="nc_no_messages_yet">No hi ha cap missatge encara</string>

View File

@ -187,13 +187,14 @@
<string name="nc_message_read">Zpráva přečtena</string>
<string name="nc_message_sent">Zpráva odeslána</string>
<string name="nc_microphone_permission_permanently_denied">Pro zapnutí hlasové komunikace udělte v nastavení systému oprávnění „Mikrofon“ .</string>
<string name="nc_missed_call">Zmeškali jste hovor od %s</string>
<string name="nc_moderator">Moderátor</string>
<string name="nc_never">Nikdy nepřipojeno</string>
<string name="nc_new_conversation">Nová konverzace</string>
<string name="nc_new_mention">Nepřečtená zmínění</string>
<string name="nc_new_messages">Nepřečtené zprávy</string>
<string name="nc_new_password">Nové heslo</string>
<string name="nc_nextcloud_talk_app_not_installed">Aplikace %1$s není na serveru nainstalována, přerušuje se</string>
<string name="nc_nextcloud_talk_app_not_installed">%1$s není k dispozici (nenainstalováno nebo administrativně omezen přístup k)</string>
<string name="nc_nick_guest">Host</string>
<string name="nc_no">Ne</string>
<string name="nc_no_messages_yet">Zatím žádné zprávy</string>

View File

@ -129,7 +129,6 @@
<string name="nc_new_conversation">Ny samtale</string>
<string name="nc_new_messages">Ulæste beskedder</string>
<string name="nc_new_password">Ny adgangskode</string>
<string name="nc_nextcloud_talk_app_not_installed">%1$s appen er ikke installeret på serveren, afbryder</string>
<string name="nc_nick_guest">Gæst</string>
<string name="nc_no">Nej</string>
<string name="nc_no_messages_yet">Ingen beskeder endnu</string>

View File

@ -187,13 +187,14 @@
<string name="nc_message_read">Nachricht gelesen</string>
<string name="nc_message_sent">Nachricht gesendet</string>
<string name="nc_microphone_permission_permanently_denied">Um Audioanrufe zu ermöglichen, gewähren Sie die Berechtigung für das \"Mikrofon\" in den Systemeinstellungen</string>
<string name="nc_missed_call">Sie haben einen Anruf von %s verpasst</string>
<string name="nc_moderator">Moderator</string>
<string name="nc_never">Nie beigetreten</string>
<string name="nc_new_conversation">Neue Unterhaltung</string>
<string name="nc_new_mention">Ungelesene Erwähnungen</string>
<string name="nc_new_messages">Ungelesene Nachrichten</string>
<string name="nc_new_password">Neues Passwort</string>
<string name="nc_nextcloud_talk_app_not_installed">%1$s App nicht auf dem Server installiert, Abbruch</string>
<string name="nc_nextcloud_talk_app_not_installed">%1$s nicht verfügbar (nicht installiert oder von der Administration eingeschränkt)</string>
<string name="nc_nick_guest">Gast</string>
<string name="nc_no">Nein</string>
<string name="nc_no_messages_yet">Noch keine Nachrichten</string>

View File

@ -165,7 +165,6 @@
<string name="nc_new_conversation">Νέα συνομιλία</string>
<string name="nc_new_messages">Μη αναγνωσμένα μηνύματα</string>
<string name="nc_new_password">Νέο συνθηματικό</string>
<string name="nc_nextcloud_talk_app_not_installed">Η εφαρμογή %1$s δεν εγκαταστάθηκε στον διακομιστή, ματαίωση</string>
<string name="nc_nick_guest">Επισκέπτης</string>
<string name="nc_no">Όχι</string>
<string name="nc_no_messages_yet">Κανένα μήνυμα ακόμα</string>

View File

@ -187,13 +187,14 @@
<string name="nc_message_read">Mensajes leídos</string>
<string name="nc_message_sent">Mensaje enviado</string>
<string name="nc_microphone_permission_permanently_denied">Para permitir la comunicación de voz, concede el permiso de \"Micrófono\" en la configuración del sistema.</string>
<string name="nc_missed_call">Perdiste una llamada de %s</string>
<string name="nc_moderator">Moderador</string>
<string name="nc_never">Nunca unido</string>
<string name="nc_new_conversation">Nueva conversación</string>
<string name="nc_new_mention">Menciones sin leer</string>
<string name="nc_new_messages">Mensajes no leídos</string>
<string name="nc_new_password">Nueva contraseña</string>
<string name="nc_nextcloud_talk_app_not_installed">La app %1$s no está instalada en el servidor. Abortando</string>
<string name="nc_nextcloud_talk_app_not_installed">%1$s no está disponible (no se encuentra instalado o está restringido por el administrador)</string>
<string name="nc_nick_guest">Invitado</string>
<string name="nc_no">No</string>
<string name="nc_no_messages_yet">Aún no hay mensajes</string>
@ -354,7 +355,7 @@
<string name="nc_upload_confirm_send_multiple">¿Enviar estos archivos a %1$s?</string>
<string name="nc_upload_confirm_send_single">¿Enviar este archivo a %1$s?</string>
<string name="nc_upload_failed">Lo siento, error en la subida</string>
<string name="nc_upload_failed_notification_text">Imposible subir %1$s</string>
<string name="nc_upload_failed_notification_text">Fallo al subir %1$s</string>
<string name="nc_upload_failed_notification_title">Falla</string>
<string name="nc_upload_from_cloud">Compartido por %1$s</string>
<string name="nc_upload_from_device">Subir desde dispositivo</string>

View File

@ -193,7 +193,6 @@
<string name="nc_new_mention">Irakurri gabeko aipamenak</string>
<string name="nc_new_messages">Irakurri gabeko mezuak</string>
<string name="nc_new_password">Pasahitz berria</string>
<string name="nc_nextcloud_talk_app_not_installed">%1$s app-a ez dago instalatuta zerbitzarian, bertan behera uzten</string>
<string name="nc_nick_guest">Gonbidatua</string>
<string name="nc_no">Ez</string>
<string name="nc_no_messages_yet">Ez dago mezurik oraindik</string>

View File

@ -20,6 +20,7 @@
<string name="file_list_loading">بارگذاری …</string>
<string name="fourHours">۴ ساعت</string>
<string name="invisible">نامرئی</string>
<string name="load_more_results">بار کردن نتیحه‌های بیش‌تر</string>
<string name="lock_symbol">نماد قفل</string>
<string name="menu_item_sort_by_date_newest_first">تازه‌ترین‌ها اول </string>
<string name="menu_item_sort_by_date_oldest_first">قدیمی‌ترین‌ها اول </string>
@ -132,7 +133,6 @@
<string name="nc_new_conversation">مکالمه جدید</string>
<string name="nc_new_messages">پیام‌های خوانده نشده</string>
<string name="nc_new_password">گذرواژه جدید</string>
<string name="nc_nextcloud_talk_app_not_installed">برنامه %1$s روی سرور نصب نمی‌باشد. درحال لغو</string>
<string name="nc_nick_guest">مهمان</string>
<string name="nc_no">نه</string>
<string name="nc_no_messages_yet">هنوز پیامی ارسال نشده است</string>

View File

@ -164,7 +164,6 @@
<string name="nc_new_mention">Lukemattomat maininnat</string>
<string name="nc_new_messages">Lukemattomat viestit</string>
<string name="nc_new_password">Uusi salasana</string>
<string name="nc_nextcloud_talk_app_not_installed">Sovellusta %1$s ei ole asennettu, keskeytetään</string>
<string name="nc_nick_guest">Vieras</string>
<string name="nc_no">Ei</string>
<string name="nc_no_messages_yet">Ei viestejä vielä</string>

View File

@ -187,13 +187,13 @@
<string name="nc_message_read">Message lu</string>
<string name="nc_message_sent">Message envoyé</string>
<string name="nc_microphone_permission_permanently_denied">Pour établir une communication audio, veuillez autoriser lutilisation du microphone dans les paramètres du système.</string>
<string name="nc_missed_call">Vous avez manqué un appel de %s</string>
<string name="nc_moderator">Modérateur</string>
<string name="nc_never">Jamais contacté</string>
<string name="nc_new_conversation">Nouvelle conversation</string>
<string name="nc_new_mention">Mentions non lues</string>
<string name="nc_new_messages">Messages non lus</string>
<string name="nc_new_password">Nouveau mot de passe</string>
<string name="nc_nextcloud_talk_app_not_installed">Application %1$s non installée sur le serveur, abandon</string>
<string name="nc_nick_guest">Invité</string>
<string name="nc_no">Non</string>
<string name="nc_no_messages_yet">Pas de messages</string>

View File

@ -140,7 +140,6 @@
<string name="nc_new_conversation">Nova conversa</string>
<string name="nc_new_messages">Mensaxes sen ler</string>
<string name="nc_new_password">Novo contrasinal</string>
<string name="nc_nextcloud_talk_app_not_installed">A aplicación %1$s non está instalado no servidor, interrompendo</string>
<string name="nc_nick_guest">Convidado</string>
<string name="nc_no">Non</string>
<string name="nc_no_messages_yet">Aínda non hai mensaxes</string>

View File

@ -172,7 +172,6 @@
<string name="nc_new_mention">Nepročitana spominjanja</string>
<string name="nc_new_messages">Nove poruke</string>
<string name="nc_new_password">Nova zaporka</string>
<string name="nc_nextcloud_talk_app_not_installed">Aplikacija %1$s nije instalirana na poslužitelju, prekid</string>
<string name="nc_nick_guest">Gost</string>
<string name="nc_no">Ne</string>
<string name="nc_no_messages_yet">Još nema poruka</string>

View File

@ -10,6 +10,7 @@
<string name="avatar">Profilkép</string>
<string name="away">Távol</string>
<string name="call_without_notification">Hívás értesítés nélkül</string>
<string name="camera_permission_granted">Kamera engedély megadva. Válassza újra a kamerát.</string>
<string name="choose_avatar_from_cloud">Profilkép választása a felhőből</string>
<string name="clear_status_message">Állapotüzenet törlése</string>
<string name="clear_status_message_after">Állapotüzenet törlése ennyi idő után:</string>
@ -23,6 +24,7 @@
<string name="emoji_category_recent">Legutóbbiak</string>
<string name="emoji_search">Emodzsi keresése</string>
<string name="encrypted">Titkosított</string>
<string name="error_loading_chats">Hiba történt a csevegések betöltése során</string>
<string name="failed_to_save">Sikertelen mentés: %1$s</string>
<string name="file_list_folder">mappa</string>
<string name="file_list_loading">Betöltés…</string>
@ -185,23 +187,26 @@
<string name="nc_message_read">Üzenet elolvasva</string>
<string name="nc_message_sent">Üzenet elküldve</string>
<string name="nc_microphone_permission_permanently_denied">A hanghívás engedélyezéséhez a rendszerbeállításokban meg kell adnia a „Mikrofon” engedélyt.</string>
<string name="nc_missed_call">Nem fogadott hívás a következőtől: %s</string>
<string name="nc_moderator">Moderátor</string>
<string name="nc_never">Soha nem csatlakozott</string>
<string name="nc_new_conversation">Új beszélgetés</string>
<string name="nc_new_mention">Olvasatlan említések</string>
<string name="nc_new_messages">Olvasatlan üzenetek</string>
<string name="nc_new_password">Új jelszó</string>
<string name="nc_nextcloud_talk_app_not_installed">A(z) %1$s alkalmazás nincs telepítve a kiszolgálón, megszakítás</string>
<string name="nc_nick_guest">Vendég</string>
<string name="nc_no">Nem</string>
<string name="nc_no_messages_yet">Nincs még üzenet</string>
<string name="nc_no_proxy">Nincs proxy</string>
<string name="nc_not_allowed_to_activate_audio">Nem kapcsolhatja be a hangot.</string>
<string name="nc_not_allowed_to_activate_video">Nem kapcsolhatja be a videót.</string>
<string name="nc_notification_channel">%1$s a(z) %2$s értesítési csatornán</string>
<string name="nc_notification_channel_calls">Hívások</string>
<string name="nc_notification_channel_calls_description">Értesítés a bejövő hívásokról</string>
<string name="nc_notification_channel_messages">Üzenetek</string>
<string name="nc_notification_channel_messages_description">Értesítés a bejövő üzenetekről</string>
<string name="nc_notification_channel_uploads">Feltöltések</string>
<string name="nc_notification_channel_uploads_description">Értesítés a feltöltési folyamatról</string>
<string name="nc_notification_settings">Értesítési beállítások</string>
<string name="nc_notify_me_always">Mindig értesítsen</string>
<string name="nc_notify_me_mention">Értesítsen, ha megemlítik Önt</string>
@ -348,11 +353,16 @@
<string name="nc_upload_confirm_send_multiple">Fájlok küldése ide: %1$s</string>
<string name="nc_upload_confirm_send_single">Ezen fájl küldése ide: %1$s</string>
<string name="nc_upload_failed">A feltöltés sikertelen</string>
<string name="nc_upload_failed_notification_text">A(z) %1$s feltöltése sikertelen</string>
<string name="nc_upload_failed_notification_title">Sikertelen</string>
<string name="nc_upload_from_cloud">Megosztás innen: %1$s</string>
<string name="nc_upload_from_device">Feltöltés az eszközről</string>
<string name="nc_upload_in_progess">Feltöltés</string>
<string name="nc_upload_notification_text">%1$s → %2$s %3$s\%%</string>
<string name="nc_upload_picture_from_cam">Fénykép készítése</string>
<string name="nc_upload_video_from_cam">Videó készítése</string>
<string name="nc_user">Felhasználó</string>
<string name="nc_video_filename">Videórögzítés innen: %1$s</string>
<string name="nc_voice_message_filename">Beszédrögzítés innen: %1$s (%2$s)</string>
<string name="nc_voice_message_hold_to_record_info">Tartsa a rögzítéshez, engedje el a küldéshez.</string>
<string name="nc_voice_message_missing_audio_permission">Hangrögzítési engedély szükséges</string>

View File

@ -127,7 +127,6 @@
<string name="nc_new_conversation">Nýtt samtal</string>
<string name="nc_new_messages">Ólesin skilaboð</string>
<string name="nc_new_password">Nýtt lykilorð</string>
<string name="nc_nextcloud_talk_app_not_installed">%1$s forritið ekki uppsett á þjóninu, hætti við</string>
<string name="nc_nick_guest">Gestur</string>
<string name="nc_no">Nei</string>
<string name="nc_no_messages_yet">Engin skilaboð ennþá</string>

View File

@ -178,7 +178,6 @@
<string name="nc_new_mention">Menzioni non lette</string>
<string name="nc_new_messages">Messaggi non letti</string>
<string name="nc_new_password">Nuova password</string>
<string name="nc_nextcloud_talk_app_not_installed">Applicazione %1$s non installata, interruzione in corso</string>
<string name="nc_nick_guest">Ospite</string>
<string name="nc_no">No</string>
<string name="nc_no_messages_yet">Ancora nessun messaggio</string>

View File

@ -127,7 +127,6 @@
<string name="nc_new_conversation">דיון חדש</string>
<string name="nc_new_messages">הודעות שלא נקראו</string>
<string name="nc_new_password">ססמה חדשה</string>
<string name="nc_nextcloud_talk_app_not_installed">היישומון %1$s אינו מותקן על השרת, הפעולה מבוטלת</string>
<string name="nc_nick_guest">אורח/ת</string>
<string name="nc_no">לא</string>
<string name="nc_no_messages_yet">אין הודעות עדיין</string>

View File

@ -179,7 +179,6 @@
<string name="nc_new_mention">未読の返信</string>
<string name="nc_new_messages">未読のメッセージ</string>
<string name="nc_new_password">新しいパスワード</string>
<string name="nc_nextcloud_talk_app_not_installed">%1$s アプリがサーバーにインストールされていません。中断します。</string>
<string name="nc_nick_guest">ゲスト</string>
<string name="nc_no">いいえ</string>
<string name="nc_no_messages_yet">メッセージはまだありません</string>

View File

@ -147,7 +147,7 @@
<string name="nc_new_mention">읽지 않은 언급</string>
<string name="nc_new_messages">읽지 않은 메세지</string>
<string name="nc_new_password">새 암호</string>
<string name="nc_nextcloud_talk_app_not_installed">서버에 %1$s 앱이 설치되어 있지 않음, 중단함</string>
<string name="nc_nextcloud_talk_app_not_installed">%1$s을(를) 사용할 수 없음 (설치되지 않았거나 관리자에 의해 제한됨)</string>
<string name="nc_nick_guest">손님</string>
<string name="nc_no">아니요</string>
<string name="nc_no_messages_yet">아직 메시지 없음</string>

View File

@ -125,7 +125,6 @@
<string name="nc_new_conversation">Naujas pokalbis</string>
<string name="nc_new_messages">Neskaitytos žinutės</string>
<string name="nc_new_password">Naujas slaptažodis</string>
<string name="nc_nextcloud_talk_app_not_installed">Serveryje nėra įdiegta programėlė %1$s, nutraukiama</string>
<string name="nc_nick_guest">Svečias</string>
<string name="nc_no">Ne</string>
<string name="nc_no_messages_yet">Kol kas žinučių nėra</string>

View File

@ -140,7 +140,6 @@
<string name="nc_new_mention">Uleste nevner</string>
<string name="nc_new_messages">Uleste meldinger</string>
<string name="nc_new_password">Nytt passord</string>
<string name="nc_nextcloud_talk_app_not_installed">%1$s-appen er ikke installert på serveren, avbryter</string>
<string name="nc_nick_guest">Gjest</string>
<string name="nc_no">Nei</string>
<string name="nc_no_messages_yet">Ingen meldiner enda</string>
@ -279,6 +278,7 @@
<string name="scope_federated_description">Synkroniser kun til betrodde servere</string>
<string name="scope_federated_title">Sammenknyttet</string>
<string name="scope_local_title">Lokal</string>
<string name="scope_private_description">Kun synlig for personer som matches via telefonnummerintegrasjon via Talk på mobil</string>
<string name="scope_private_title">Privat</string>
<string name="scope_published_description">Synkroniser til betrodde servere og den globale og offentlige adresseboken</string>
<string name="scope_published_title">Publisert</string>

View File

@ -176,7 +176,6 @@ Kies er eentje van een provider.</string>
<string name="nc_new_mention">Ongelezen vermeldingen</string>
<string name="nc_new_messages">Ongelezen berichten</string>
<string name="nc_new_password">Nieuw wachtwoord</string>
<string name="nc_nextcloud_talk_app_not_installed">%1$s app is niet geïnstalleerd op de server, afbreken</string>
<string name="nc_nick_guest">Gast</string>
<string name="nc_no">Nee</string>
<string name="nc_no_messages_yet">Nog geen berichten</string>

View File

@ -137,7 +137,6 @@
<string name="nc_new_conversation">Ny samtale</string>
<string name="nc_new_messages">Meldingar er ikkje lest</string>
<string name="nc_new_password">Nytt passord</string>
<string name="nc_nextcloud_talk_app_not_installed">%1$s applikasjon er ikkje installert på denne server, avbryt</string>
<string name="nc_nick_guest">Gjest</string>
<string name="nc_no">Nei</string>
<string name="nc_no_messages_yet">Ingen meldingar til no</string>

View File

@ -193,7 +193,6 @@
<string name="nc_new_mention">Nieprzeczytane wzmianki</string>
<string name="nc_new_messages">Nieprzeczytane wiadomości</string>
<string name="nc_new_password">Nowe hasło</string>
<string name="nc_nextcloud_talk_app_not_installed">Aplikacja %1$s nie została zainstalowana na serwerze, przerwano żądanie</string>
<string name="nc_nick_guest">Gość</string>
<string name="nc_no">Nie</string>
<string name="nc_no_messages_yet">Nie ma nowych wiadomości</string>

View File

@ -193,7 +193,6 @@
<string name="nc_new_mention">Menções não lidas</string>
<string name="nc_new_messages">Mensagens não lidas</string>
<string name="nc_new_password">Nova senha</string>
<string name="nc_nextcloud_talk_app_not_installed">Aplicativo %1$s não instalado no servidor, cancelando</string>
<string name="nc_nick_guest">Convidado</string>
<string name="nc_no">Não</string>
<string name="nc_no_messages_yet">Sem mensagens ainda</string>
@ -248,7 +247,7 @@
<string name="nc_screen_lock_timeout_sixty">60</string>
<string name="nc_screen_lock_timeout_thirty">30</string>
<string name="nc_screen_lock_timeout_three_hundred">300</string>
<string name="nc_search">Persquisar</string>
<string name="nc_search">Pesquisar</string>
<string name="nc_select_an_account">Selecionar uma conta</string>
<string name="nc_select_participants">Selecionar participantes</string>
<string name="nc_sent_a_gif" formatted="true">%1$s enviou um GIF.</string>

View File

@ -193,7 +193,7 @@
<string name="nc_new_mention">Непрочитанные упоминания</string>
<string name="nc_new_messages">Непрочитанные сообщения</string>
<string name="nc_new_password">Новый пароль</string>
<string name="nc_nextcloud_talk_app_not_installed">Приложение %1$s не установлено на сервере. Действие отменено.</string>
<string name="nc_nextcloud_talk_app_not_installed">Приложение %1$s недоступно (не установлено, либо использование приложения ограничено администратором)</string>
<string name="nc_nick_guest">Гость</string>
<string name="nc_no">Нет</string>
<string name="nc_no_messages_yet">Сообщений еще нет</string>

View File

@ -161,7 +161,6 @@
<string name="nc_new_conversation">Resonada noa</string>
<string name="nc_new_messages">Messàgios non lèghidos</string>
<string name="nc_new_password">Crae noa</string>
<string name="nc_nextcloud_talk_app_not_installed">%1$s s\'aplicatzione no est installada in su serbidore, annullende</string>
<string name="nc_nick_guest">Persone invitada</string>
<string name="nc_no">No</string>
<string name="nc_no_messages_yet">Ancora perunu messàgiu</string>

View File

@ -193,7 +193,6 @@
<string name="nc_new_mention">Neprečítané upozornenia</string>
<string name="nc_new_messages">Neprečítané správy</string>
<string name="nc_new_password">Nové heslo</string>
<string name="nc_nextcloud_talk_app_not_installed">Aplikácia %1$s nie je na serveri nainštalovaná, ruší sa</string>
<string name="nc_nick_guest">Hosť</string>
<string name="nc_no">Nie</string>
<string name="nc_no_messages_yet">Ešte žiadne správy</string>

View File

@ -9,6 +9,7 @@
<string name="audio_output_wired_headset">Ožičene slušalke</string>
<string name="avatar">Podoba</string>
<string name="away">Ne spremljam</string>
<string name="call_without_notification">Klic brez obvestila</string>
<string name="choose_avatar_from_cloud">Izbor pogodbe iz oblaka</string>
<string name="clear_status_message">Počisti sporočilo stanja</string>
<string name="clear_status_message_after">Počisti sporočilo stanja po</string>
@ -181,7 +182,6 @@
<string name="nc_new_mention">Neprebrane omembe</string>
<string name="nc_new_messages">Neprebrana sporočila</string>
<string name="nc_new_password">Novo geslo</string>
<string name="nc_nextcloud_talk_app_not_installed">Program %1$s na strežniku ni nameščen, zahteva bo preklicana.</string>
<string name="nc_nick_guest">Gost</string>
<string name="nc_no">Ne</string>
<string name="nc_no_messages_yet">Ni še sporočil</string>

View File

@ -121,7 +121,6 @@
<string name="nc_new_conversation">Нови разговор</string>
<string name="nc_new_messages">Непрочитане поруке</string>
<string name="nc_new_password">Нова лозинка</string>
<string name="nc_nextcloud_talk_app_not_installed">%1$s апликација није инсталирана на серверу, прекидам</string>
<string name="nc_nick_guest">Гост</string>
<string name="nc_no">Не</string>
<string name="nc_no_messages_yet">Још нема порука</string>

View File

@ -141,7 +141,6 @@
<string name="nc_new_conversation">Ny konversation</string>
<string name="nc_new_messages">Olästa meddelanden</string>
<string name="nc_new_password">Nytt lösenord</string>
<string name="nc_nextcloud_talk_app_not_installed">%1$s app inte installerad på servern, avbryter</string>
<string name="nc_nick_guest">Gäst</string>
<string name="nc_no">Nej</string>
<string name="nc_no_messages_yet">Inga meddelanden än</string>
@ -284,6 +283,7 @@
<string name="online_status">Online-status</string>
<string name="polls_add_option">Lägg till alternativ</string>
<string name="polls_options">Alternativ</string>
<string name="polls_private_poll">Privat omröstning</string>
<string name="polls_results_subtitle">Resultat</string>
<string name="polls_settings">Inställningar</string>
<string name="polls_submit_vote">Rösta</string>

View File

@ -187,13 +187,14 @@
<string name="nc_message_read">İleti okundu</string>
<string name="nc_message_sent">İleti gönderildi</string>
<string name="nc_microphone_permission_permanently_denied">Sesli iletişim kurabilmek için sistem ayarlarından \"Mikrofon\" erişme iznini verin.</string>
<string name="nc_missed_call">%s sizi aramış</string>
<string name="nc_moderator">Sorumlu</string>
<string name="nc_never">Hiç katılmadı</string>
<string name="nc_new_conversation">Yeni görüşme</string>
<string name="nc_new_mention">Okunmamış anmalar</string>
<string name="nc_new_messages">Okunmamış iletiler</string>
<string name="nc_new_password">Yeni parola</string>
<string name="nc_nextcloud_talk_app_not_installed">%1$s uygulaması sunucu üzerinde kurulu değil, vazgeçiliyor</string>
<string name="nc_nextcloud_talk_app_not_installed">%1$s kullanılamıyor (kurulmamış ya da yönetici tarafından engellenmiş)</string>
<string name="nc_nick_guest">Konuk</string>
<string name="nc_no">Hayır</string>
<string name="nc_no_messages_yet">Henüz bir ileti yok</string>

View File

@ -166,7 +166,6 @@
<string name="nc_new_mention">Непрочитані згадки</string>
<string name="nc_new_messages">Непрочитані повідомлення</string>
<string name="nc_new_password">Новий пароль</string>
<string name="nc_nextcloud_talk_app_not_installed">Застосунок %1$s не встановлено на сервері, скасування</string>
<string name="nc_nick_guest">Гість</string>
<string name="nc_no">Ні</string>
<string name="nc_no_messages_yet">Повідомлень немає</string>
@ -289,7 +288,7 @@
<string name="nc_share_to_choose_account">Оберіть обліковий запис</string>
<string name="nc_shared_items_location">Місце</string>
<string name="nc_shared_location">Місце у спільному доступі</string>
<string name="nc_sort_by">Сортувати по</string>
<string name="nc_sort_by">Впорядкувати за</string>
<string name="nc_upload_choose_local_files">Виберіть файли</string>
<string name="nc_upload_failed">Вибачте, помилка завантаження</string>
<string name="nc_upload_from_device">Завантажити з пристрою</string>
@ -319,7 +318,7 @@
<string name="selected_list_item">Selected</string>
<string name="set_status">Встановити статус</string>
<string name="set_status_message">Встановити повідомлення про стан</string>
<string name="share">Поділитися</string>
<string name="share">Спільний доступ</string>
<string name="shared_items_audio">Аудіо</string>
<string name="shared_items_file">Файл</string>
<string name="shared_items_media">Зображення та відео</string>

View File

@ -126,7 +126,6 @@
<string name="nc_new_conversation">Tạo đàm thoại mới</string>
<string name="nc_new_messages">Tin nhắn chưa đọc</string>
<string name="nc_new_password">Mật khẩu mới</string>
<string name="nc_nextcloud_talk_app_not_installed">%1$s ứng dụng chưa cài đặt trên hệ thống, hủy bỏ</string>
<string name="nc_nick_guest">Khách</string>
<string name="nc_no">Không</string>
<string name="nc_no_messages_yet">Chưa có thông điệp nào</string>

View File

@ -51,6 +51,7 @@
<string name="nc_call_name">会话名称</string>
<string name="nc_call_name_is_same">您输入的名称已经存在</string>
<string name="nc_call_notifications">呼叫通知</string>
<string name="nc_call_reconnecting">正在重新连接 ...</string>
<string name="nc_call_ringing">响铃</string>
<string name="nc_call_state_in_call">%1$s 在通话中</string>
<string name="nc_call_state_with_phone">%1$s 通过手机</string>
@ -112,6 +113,7 @@
<string name="nc_expire_message_one_day">1 天</string>
<string name="nc_expire_message_one_hour">1 小时</string>
<string name="nc_expire_message_one_week">1 周</string>
<string name="nc_expire_messages_explanation">可指定聊天消息在一定时间后过期。注意:聊天中分享的文件不会被从所有者一方删除,但是会被从会话中取消分享。</string>
<string name="nc_external_server_failed">获取网络设置失败</string>
<string name="nc_failed_signaling_settings">目标服务器不支持通过移动电话加入公共对话。你可以尝试通过浏览器加入对话。</string>
<string name="nc_failed_to_perform_operation">抱歉,有地方出错了!</string>
@ -132,6 +134,7 @@
<string name="nc_guest_access_password_weak_alert_title">弱密码</string>
<string name="nc_guest_access_resend_invitations">重发邀请</string>
<string name="nc_guest_access_share_link">共享会话链接</string>
<string name="nc_hint_enter_a_message">输入消息…</string>
<string name="nc_important_conversation">重要会话</string>
<string name="nc_important_conversation_desc">此会话中的通知将覆盖免打扰设置</string>
<string name="nc_join_via_link">使用链接加入</string>
@ -140,6 +143,7 @@
<string name="nc_last_moderator_title">无法离开会话</string>
<string name="nc_last_modified">%1$s 我上一次更改: %2$s</string>
<string name="nc_leave">离开会话</string>
<string name="nc_leaving_call">正在离开通话 ...</string>
<string name="nc_license_summary">GNU 通用公共许可证第3版</string>
<string name="nc_license_title">许可证</string>
<string name="nc_limit_hit">已达到%s字符限制</string>
@ -167,7 +171,6 @@
<string name="nc_new_mention">未读的提及</string>
<string name="nc_new_messages">未读消息</string>
<string name="nc_new_password">新密码</string>
<string name="nc_nextcloud_talk_app_not_installed">服务器未安装 %1$s 应用,中止</string>
<string name="nc_nick_guest">来宾</string>
<string name="nc_no"></string>
<string name="nc_no_messages_yet">暂无消息</string>
@ -309,7 +312,9 @@
<string name="nc_share_text_pass">\n密码 %1$s</string>
<string name="nc_share_this_location">分享这个位置</string>
<string name="nc_share_to_choose_account">选择一个账户</string>
<string name="nc_shared_items">已分享项目</string>
<string name="nc_shared_items_deck_card">Deck 卡片</string>
<string name="nc_shared_items_empty">沒有已分享的项目</string>
<string name="nc_shared_items_location">位置</string>
<string name="nc_shared_location">共享的位置</string>
<string name="nc_sort_by">排序依据</string>

View File

@ -187,13 +187,14 @@
<string name="nc_message_read">訊息已讀</string>
<string name="nc_message_sent">訊息已傳送</string>
<string name="nc_microphone_permission_permanently_denied">為了開啟聲音的通訊請在系統設定內同意\"麥克風\"的需求。</string>
<string name="nc_missed_call">您錯過了 %s 的來電</string>
<string name="nc_moderator">主持人</string>
<string name="nc_never">從未加入</string>
<string name="nc_new_conversation">新對話</string>
<string name="nc_new_mention">未讀的提及</string>
<string name="nc_new_messages">未讀郵件</string>
<string name="nc_new_password">新密碼</string>
<string name="nc_nextcloud_talk_app_not_installed">此伺服器並未安裝%1$s應用程式操作中斷。</string>
<string name="nc_nextcloud_talk_app_not_installed">%1$s 不可用(管理員未安裝或限制)</string>
<string name="nc_nick_guest">訪客</string>
<string name="nc_no"></string>
<string name="nc_no_messages_yet">目前無任何訊息</string>

View File

@ -175,7 +175,6 @@
<string name="nc_new_mention">未讀的提及</string>
<string name="nc_new_messages">未讀訊息</string>
<string name="nc_new_password">新密碼</string>
<string name="nc_nextcloud_talk_app_not_installed">此伺服器並未安裝%1$s應用操作中斷。</string>
<string name="nc_nick_guest">訪客</string>
<string name="nc_no"></string>
<string name="nc_no_messages_yet">目前無任何訊息</string>

View File

@ -58,7 +58,7 @@
<string name="nc_capabilities_failed">Failed to fetch capabilities, aborting</string>
<string name="nc_external_server_failed">Failed to fetch signaling settings</string>
<string name="nc_display_name_not_fetched">Display name couldn\'t be fetched, aborting</string>
<string name="nc_nextcloud_talk_app_not_installed">%1$s app not installed on the server, aborting</string>
<string name="nc_nextcloud_talk_app_not_installed">%1$s not available (not installed or restricted by admin)</string>
<string name="nc_display_name_not_stored">Could not store display name, aborting</string>
<string name="nc_never">Never joined</string>
@ -210,6 +210,7 @@
<string name="nc_call_state_in_call">%1$s in call</string>
<string name="nc_call_state_with_phone">%1$s with phone</string>
<string name="nc_call_state_with_video">%1$s with video</string>
<string name="nc_missed_call">You missed a call from %s</string>
<!-- Picture in Picture -->
<string name="nc_pip_microphone_mute">Mute microphone</string>
@ -246,7 +247,6 @@
<string name="nc_share_subject">%1$s invitation</string>
<string name="nc_share_text_pass">\nPassword: %1$s</string>
<!-- Magical stuff -->
<string name="nc_push_to_talk">Push-to-talk</string>
<string name="nc_push_to_talk_desc">With microphone disabled, click&amp;hold to use Push-to-talk</string>
<string name="nc_configure_cert_auth">Select authentication certificate</string>

View File

@ -24,7 +24,7 @@
buildscript {
ext {
kotlinVersion = '1.7.20'
kotlinVersion = '1.7.21'
}
repositories {

View File

@ -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() {

View File

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