mirror of
https://github.com/nextcloud/talk-android
synced 2025-06-20 20:19:42 +01:00
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:
commit
dd46c28568
@ -36,7 +36,7 @@ apply plugin: "org.jlleitschuh.gradle.ktlint"
|
|||||||
apply plugin: 'kotlinx-serialization'
|
apply plugin: 'kotlinx-serialization'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 31
|
compileSdkVersion 32
|
||||||
buildToolsVersion '33.0.0'
|
buildToolsVersion '33.0.0'
|
||||||
|
|
||||||
namespace 'com.nextcloud.talk'
|
namespace 'com.nextcloud.talk'
|
||||||
@ -48,8 +48,8 @@ android {
|
|||||||
|
|
||||||
// mayor.minor.hotfix.increment (for increment: 01-50=Alpha / 51-89=RC / 90-99=stable)
|
// mayor.minor.hotfix.increment (for increment: 01-50=Alpha / 51-89=RC / 90-99=stable)
|
||||||
// xx .xxx .xx .xx
|
// xx .xxx .xx .xx
|
||||||
versionCode 150010007
|
versionCode 150010008
|
||||||
versionName "15.1.0 Alpha 07"
|
versionName "15.1.0 Alpha 08"
|
||||||
|
|
||||||
flavorDimensions "default"
|
flavorDimensions "default"
|
||||||
renderscriptTargetApi 19
|
renderscriptTargetApi 19
|
||||||
@ -142,7 +142,7 @@ android {
|
|||||||
ext {
|
ext {
|
||||||
androidxCameraVersion = "1.1.0"
|
androidxCameraVersion = "1.1.0"
|
||||||
coilKtVersion = "2.2.2"
|
coilKtVersion = "2.2.2"
|
||||||
daggerVersion = "2.44"
|
daggerVersion = "2.44.2"
|
||||||
lifecycleVersion = '2.5.1'
|
lifecycleVersion = '2.5.1'
|
||||||
okhttpVersion = "4.10.0"
|
okhttpVersion = "4.10.0"
|
||||||
materialDialogsVersion = "3.3.0"
|
materialDialogsVersion = "3.3.0"
|
||||||
@ -151,10 +151,10 @@ ext {
|
|||||||
roomVersion = "2.4.3"
|
roomVersion = "2.4.3"
|
||||||
workVersion = "2.7.1"
|
workVersion = "2.7.1"
|
||||||
markwonVersion = "4.6.2"
|
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){
|
tasks.register('downloadWebRtc', DownloadWebRtcTask){
|
||||||
version = webRtcVersion
|
version = webRtcVersion
|
||||||
}
|
}
|
||||||
@ -175,7 +175,7 @@ dependencies {
|
|||||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1"
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1"
|
||||||
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.5.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 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation "com.vanniktech:emoji-google:0.15.0"
|
implementation "com.vanniktech:emoji-google:0.15.0"
|
||||||
implementation group: 'androidx.emoji', name: 'emoji-bundled', version: '1.1.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:rxandroid:2.1.1'
|
||||||
implementation "io.reactivex.rxjava2:rxjava:2.2.21"
|
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:${okhttpVersion}"
|
||||||
implementation "com.squareup.okhttp3:okhttp-urlconnection:${okhttpVersion}"
|
implementation "com.squareup.okhttp3:okhttp-urlconnection:${okhttpVersion}"
|
||||||
implementation "com.squareup.okhttp3:logging-interceptor:${okhttpVersion}"
|
implementation "com.squareup.okhttp3:logging-interceptor:${okhttpVersion}"
|
||||||
|
|
||||||
implementation 'com.bluelinelabs:logansquare:1.3.7'
|
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'
|
kapt 'com.bluelinelabs:logansquare-compiler:1.3.7'
|
||||||
|
|
||||||
implementation "com.squareup.retrofit2:retrofit:${retrofit2Version}"
|
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:webpsupport:v111'
|
||||||
implementation 'com.github.nextcloud-deps.fresco:animated-gif:v111'
|
implementation 'com.github.nextcloud-deps.fresco:animated-gif:v111'
|
||||||
implementation 'com.github.nextcloud-deps.fresco:imagepipeline-okhttp3: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:${coilKtVersion}"
|
||||||
implementation "io.coil-kt:coil-gif:${coilKtVersion}"
|
implementation "io.coil-kt:coil-gif:${coilKtVersion}"
|
||||||
implementation "io.coil-kt:coil-svg:${coilKtVersion}"
|
implementation "io.coil-kt:coil-svg:${coilKtVersion}"
|
||||||
@ -280,8 +280,7 @@ dependencies {
|
|||||||
|
|
||||||
implementation "io.noties.markwon:core:$markwonVersion"
|
implementation "io.noties.markwon:core:$markwonVersion"
|
||||||
|
|
||||||
//implementation 'com.github.dhaval2404:imagepicker:1.8'
|
implementation 'com.github.nextcloud-deps:ImagePicker:2.1.0.2'
|
||||||
implementation 'com.github.nextcloud-deps:ImagePicker:1.8.0.2'
|
|
||||||
implementation 'com.elyeproj.libraries:loaderviewlibrary:2.0.0'
|
implementation 'com.elyeproj.libraries:loaderviewlibrary:2.0.0'
|
||||||
|
|
||||||
implementation 'org.osmdroid:osmdroid-android:6.1.14'
|
implementation 'org.osmdroid:osmdroid-android:6.1.14'
|
||||||
@ -293,9 +292,9 @@ dependencies {
|
|||||||
implementation 'androidx.core:core-ktx:1.8.0'
|
implementation 'androidx.core:core-ktx:1.8.0'
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
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
|
// Espresso core
|
||||||
androidTestImplementation ("androidx.test.espresso:espresso-core:$espressoVersion", {
|
androidTestImplementation ("androidx.test.espresso:espresso-core:$espressoVersion", {
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
<meta-data android:name="google_analytics_adid_collection_enabled" android:value="false" />
|
<meta-data android:name="google_analytics_adid_collection_enabled" android:value="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".services.firebase.ChatAndCallMessagingService"
|
android:name=".services.firebase.NCFirebaseMessagingService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="phoneCall">
|
android:foregroundServiceType="phoneCall">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
@ -138,6 +138,7 @@ import java.util.Map;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
@ -439,7 +440,7 @@ public class CallActivity extends CallBaseActivity {
|
|||||||
binding.gridview.setOnItemClickListener((parent, view, position, id) -> animateCallControls(true, 0));
|
binding.gridview.setOnItemClickListener((parent, view, position, id) -> animateCallControls(true, 0));
|
||||||
|
|
||||||
binding.callStates.callStateRelativeLayout.setOnClickListener(l -> {
|
binding.callStates.callStateRelativeLayout.setOnClickListener(l -> {
|
||||||
if (currentCallStatus.equals(CallStatus.CALLING_TIMEOUT)) {
|
if (currentCallStatus == CallStatus.CALLING_TIMEOUT) {
|
||||||
setCallState(CallStatus.RECONNECTING);
|
setCallState(CallStatus.RECONNECTING);
|
||||||
hangupNetworkCalls(false);
|
hangupNetworkCalls(false);
|
||||||
}
|
}
|
||||||
@ -746,7 +747,7 @@ public class CallActivity extends CallBaseActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isConnectionEstablished() {
|
private boolean isConnectionEstablished() {
|
||||||
return (currentCallStatus.equals(CallStatus.JOINED) || currentCallStatus.equals(CallStatus.IN_CONVERSATION));
|
return (currentCallStatus == CallStatus.JOINED || currentCallStatus == CallStatus.IN_CONVERSATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterPermissionGranted(100)
|
@AfterPermissionGranted(100)
|
||||||
@ -837,9 +838,9 @@ public class CallActivity extends CallBaseActivity {
|
|||||||
Log.d(TAG, "onAudioManagerDevicesChanged: " + availableDevices + ", "
|
Log.d(TAG, "onAudioManagerDevicesChanged: " + availableDevices + ", "
|
||||||
+ "currentDevice: " + currentDevice);
|
+ "currentDevice: " + currentDevice);
|
||||||
|
|
||||||
final boolean shouldDisableProximityLock = (currentDevice.equals(WebRtcAudioManager.AudioDevice.WIRED_HEADSET)
|
final boolean shouldDisableProximityLock = (currentDevice == WebRtcAudioManager.AudioDevice.WIRED_HEADSET
|
||||||
|| currentDevice.equals(WebRtcAudioManager.AudioDevice.SPEAKER_PHONE)
|
|| currentDevice == WebRtcAudioManager.AudioDevice.SPEAKER_PHONE
|
||||||
|| currentDevice.equals(WebRtcAudioManager.AudioDevice.BLUETOOTH));
|
|| currentDevice == WebRtcAudioManager.AudioDevice.BLUETOOTH);
|
||||||
|
|
||||||
if (shouldDisableProximityLock) {
|
if (shouldDisableProximityLock) {
|
||||||
powerManagerUtils.updatePhoneState(PowerManagerUtils.PhoneState.WITHOUT_PROXIMITY_SENSOR_LOCK);
|
powerManagerUtils.updatePhoneState(PowerManagerUtils.PhoneState.WITHOUT_PROXIMITY_SENSOR_LOCK);
|
||||||
@ -1229,7 +1230,7 @@ public class CallActivity extends CallBaseActivity {
|
|||||||
Log.d(TAG, "localStream is null");
|
Log.d(TAG, "localStream is null");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currentCallStatus.equals(CallStatus.LEAVING)) {
|
if (currentCallStatus != CallStatus.LEAVING) {
|
||||||
hangup(true);
|
hangup(true);
|
||||||
}
|
}
|
||||||
powerManagerUtils.updatePhoneState(PowerManagerUtils.PhoneState.IDLE);
|
powerManagerUtils.updatePhoneState(PowerManagerUtils.PhoneState.IDLE);
|
||||||
@ -1456,7 +1457,7 @@ public class CallActivity extends CallBaseActivity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNext(@io.reactivex.annotations.NonNull GenericOverall genericOverall) {
|
public void onNext(@io.reactivex.annotations.NonNull GenericOverall genericOverall) {
|
||||||
if (!currentCallStatus.equals(CallStatus.LEAVING)) {
|
if (currentCallStatus != CallStatus.LEAVING) {
|
||||||
setCallState(CallStatus.JOINED);
|
setCallState(CallStatus.JOINED);
|
||||||
|
|
||||||
ApplicationWideCurrentRoomHolder.getInstance().setInCall(true);
|
ApplicationWideCurrentRoomHolder.getInstance().setInCall(true);
|
||||||
@ -1472,6 +1473,8 @@ public class CallActivity extends CallBaseActivity {
|
|||||||
int apiVersion = ApiUtils.getSignalingApiVersion(conversationUser,
|
int apiVersion = ApiUtils.getSignalingApiVersion(conversationUser,
|
||||||
new int[]{ApiUtils.APIv3, 2, 1});
|
new int[]{ApiUtils.APIv3, 2, 1});
|
||||||
|
|
||||||
|
AtomicInteger delayOnError = new AtomicInteger(0);
|
||||||
|
|
||||||
ncApi.pullSignalingMessages(credentials,
|
ncApi.pullSignalingMessages(credentials,
|
||||||
ApiUtils.getUrlForSignaling(apiVersion,
|
ApiUtils.getUrlForSignaling(apiVersion,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
@ -1480,7 +1483,22 @@ public class CallActivity extends CallBaseActivity {
|
|||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.repeatWhen(observable -> observable)
|
.repeatWhen(observable -> observable)
|
||||||
.takeWhile(observable -> isConnectionEstablished())
|
.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>() {
|
.subscribe(new Observer<SignalingOverall>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
|
public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
|
||||||
@ -1531,7 +1549,7 @@ public class CallActivity extends CallBaseActivity {
|
|||||||
conversationUser, externalSignalingServer.getExternalSignalingTicket(),
|
conversationUser, externalSignalingServer.getExternalSignalingTicket(),
|
||||||
TextUtils.isEmpty(credentials));
|
TextUtils.isEmpty(credentials));
|
||||||
} else {
|
} else {
|
||||||
if (webSocketClient.isConnected() && currentCallStatus.equals(CallStatus.PUBLISHER_FAILED)) {
|
if (webSocketClient.isConnected() && currentCallStatus == CallStatus.PUBLISHER_FAILED) {
|
||||||
webSocketClient.restartWebSocket();
|
webSocketClient.restartWebSocket();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1549,7 +1567,7 @@ public class CallActivity extends CallBaseActivity {
|
|||||||
|
|
||||||
@Subscribe(threadMode = ThreadMode.BACKGROUND)
|
@Subscribe(threadMode = ThreadMode.BACKGROUND)
|
||||||
public void onMessageEvent(WebSocketCommunicationEvent webSocketCommunicationEvent) {
|
public void onMessageEvent(WebSocketCommunicationEvent webSocketCommunicationEvent) {
|
||||||
if (CallStatus.LEAVING.equals(currentCallStatus)) {
|
if (currentCallStatus == CallStatus.LEAVING) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1557,7 +1575,7 @@ public class CallActivity extends CallBaseActivity {
|
|||||||
case "hello":
|
case "hello":
|
||||||
Log.d(TAG, "onMessageEvent 'hello'");
|
Log.d(TAG, "onMessageEvent 'hello'");
|
||||||
if (!webSocketCommunicationEvent.getHashMap().containsKey("oldResumeId")) {
|
if (!webSocketCommunicationEvent.getHashMap().containsKey("oldResumeId")) {
|
||||||
if (currentCallStatus.equals(CallStatus.RECONNECTING)) {
|
if (currentCallStatus == CallStatus.RECONNECTING) {
|
||||||
hangup(false);
|
hangup(false);
|
||||||
} else {
|
} else {
|
||||||
initiateCall();
|
initiateCall();
|
||||||
@ -1642,7 +1660,7 @@ public class CallActivity extends CallBaseActivity {
|
|||||||
private void receivedSignalingMessage(Signaling signaling) throws IOException {
|
private void receivedSignalingMessage(Signaling signaling) throws IOException {
|
||||||
String messageType = signaling.getType();
|
String messageType = signaling.getType();
|
||||||
|
|
||||||
if (!isConnectionEstablished() && !currentCallStatus.equals(CallStatus.CONNECTING)) {
|
if (!isConnectionEstablished() && currentCallStatus != CallStatus.CONNECTING) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1871,7 +1889,7 @@ public class CallActivity extends CallBaseActivity {
|
|||||||
userIdsBySessionId.put(participant.get("sessionId").toString(), userId);
|
userIdsBySessionId.put(participant.get("sessionId").toString(), userId);
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, " inCallFlag of currentSessionId: " + inCallFlag);
|
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.");
|
Log.d(TAG, "Most probably a moderator ended the call for all.");
|
||||||
hangup(true);
|
hangup(true);
|
||||||
}
|
}
|
||||||
@ -1891,7 +1909,7 @@ public class CallActivity extends CallBaseActivity {
|
|||||||
// Calculate sessions that join the call
|
// Calculate sessions that join the call
|
||||||
newSessions.removeAll(oldSessions);
|
newSessions.removeAll(oldSessions);
|
||||||
|
|
||||||
if (!isConnectionEstablished() && !currentCallStatus.equals(CallStatus.CONNECTING)) {
|
if (!isConnectionEstablished() && currentCallStatus != CallStatus.CONNECTING) {
|
||||||
return;
|
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);
|
setCallState(CallStatus.IN_CONVERSATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2069,8 +2087,9 @@ public class CallActivity extends CallBaseActivity {
|
|||||||
if (!(peerConnectionWrappers = getPeerConnectionWrapperListForSessionId(sessionId)).isEmpty()) {
|
if (!(peerConnectionWrappers = getPeerConnectionWrapperListForSessionId(sessionId)).isEmpty()) {
|
||||||
for (PeerConnectionWrapper peerConnectionWrapper : peerConnectionWrappers) {
|
for (PeerConnectionWrapper peerConnectionWrapper : peerConnectionWrappers) {
|
||||||
if (peerConnectionWrapper.getSessionId().equals(sessionId)) {
|
if (peerConnectionWrapper.getSessionId().equals(sessionId)) {
|
||||||
if (VIDEO_STREAM_TYPE_SCREEN.equals(peerConnectionWrapper.getVideoStreamType()) || !justScreen) {
|
String videoStreamType = peerConnectionWrapper.getVideoStreamType();
|
||||||
runOnUiThread(() -> removeMediaStream(sessionId));
|
if (VIDEO_STREAM_TYPE_SCREEN.equals(videoStreamType) || !justScreen) {
|
||||||
|
runOnUiThread(() -> removeMediaStream(sessionId, videoStreamType));
|
||||||
deletePeerConnection(peerConnectionWrapper);
|
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");
|
Log.d(TAG, "removeMediaStream");
|
||||||
participantDisplayItems.remove(sessionId);
|
participantDisplayItems.remove(sessionId + "-" + videoStreamType);
|
||||||
|
|
||||||
if (!isDestroyed()) {
|
if (!isDestroyed()) {
|
||||||
initGridAdapter();
|
initGridAdapter();
|
||||||
@ -2145,21 +2164,22 @@ public class CallActivity extends CallBaseActivity {
|
|||||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||||
public void onMessageEvent(PeerConnectionEvent peerConnectionEvent) {
|
public void onMessageEvent(PeerConnectionEvent peerConnectionEvent) {
|
||||||
String sessionId = peerConnectionEvent.getSessionId();
|
String sessionId = peerConnectionEvent.getSessionId();
|
||||||
|
String participantDisplayItemId = sessionId + "-" + peerConnectionEvent.getVideoStreamType();
|
||||||
|
|
||||||
if (peerConnectionEvent.getPeerConnectionEventType() ==
|
if (peerConnectionEvent.getPeerConnectionEventType() ==
|
||||||
PeerConnectionEvent.PeerConnectionEventType.PEER_CONNECTED) {
|
PeerConnectionEvent.PeerConnectionEventType.PEER_CONNECTED) {
|
||||||
if (webSocketClient != null && webSocketClient.getSessionId() == sessionId) {
|
if (webSocketClient != null && webSocketClient.getSessionId() != null && webSocketClient.getSessionId().equals(sessionId)) {
|
||||||
updateSelfVideoViewConnected(true);
|
updateSelfVideoViewConnected(true);
|
||||||
} else if (participantDisplayItems.get(sessionId) != null) {
|
} else if (participantDisplayItems.get(participantDisplayItemId) != null) {
|
||||||
participantDisplayItems.get(sessionId).setConnected(true);
|
participantDisplayItems.get(participantDisplayItemId).setConnected(true);
|
||||||
participantsAdapter.notifyDataSetChanged();
|
participantsAdapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
} else if (peerConnectionEvent.getPeerConnectionEventType() ==
|
} else if (peerConnectionEvent.getPeerConnectionEventType() ==
|
||||||
PeerConnectionEvent.PeerConnectionEventType.PEER_DISCONNECTED) {
|
PeerConnectionEvent.PeerConnectionEventType.PEER_DISCONNECTED) {
|
||||||
if (webSocketClient != null && webSocketClient.getSessionId() == sessionId) {
|
if (webSocketClient != null && webSocketClient.getSessionId() != null && webSocketClient.getSessionId().equals(sessionId)) {
|
||||||
updateSelfVideoViewConnected(false);
|
updateSelfVideoViewConnected(false);
|
||||||
} else if (participantDisplayItems.get(sessionId) != null) {
|
} else if (participantDisplayItems.get(participantDisplayItemId) != null) {
|
||||||
participantDisplayItems.get(sessionId).setConnected(false);
|
participantDisplayItems.get(participantDisplayItemId).setConnected(false);
|
||||||
participantsAdapter.notifyDataSetChanged();
|
participantsAdapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
} else if (peerConnectionEvent.getPeerConnectionEventType() ==
|
} else if (peerConnectionEvent.getPeerConnectionEventType() ==
|
||||||
@ -2174,27 +2194,27 @@ public class CallActivity extends CallBaseActivity {
|
|||||||
boolean enableVideo = peerConnectionEvent.getPeerConnectionEventType() ==
|
boolean enableVideo = peerConnectionEvent.getPeerConnectionEventType() ==
|
||||||
PeerConnectionEvent.PeerConnectionEventType.SENSOR_FAR && videoOn;
|
PeerConnectionEvent.PeerConnectionEventType.SENSOR_FAR && videoOn;
|
||||||
if (EffortlessPermissions.hasPermissions(this, PERMISSIONS_CAMERA) &&
|
if (EffortlessPermissions.hasPermissions(this, PERMISSIONS_CAMERA) &&
|
||||||
(currentCallStatus.equals(CallStatus.CONNECTING) || isConnectionEstablished()) && videoOn
|
(currentCallStatus == CallStatus.CONNECTING || isConnectionEstablished()) && videoOn
|
||||||
&& enableVideo != localVideoTrack.enabled()) {
|
&& enableVideo != localVideoTrack.enabled()) {
|
||||||
toggleMedia(enableVideo, true);
|
toggleMedia(enableVideo, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (peerConnectionEvent.getPeerConnectionEventType() ==
|
} else if (peerConnectionEvent.getPeerConnectionEventType() ==
|
||||||
PeerConnectionEvent.PeerConnectionEventType.NICK_CHANGE) {
|
PeerConnectionEvent.PeerConnectionEventType.NICK_CHANGE) {
|
||||||
if (participantDisplayItems.get(sessionId) != null) {
|
if (participantDisplayItems.get(participantDisplayItemId) != null) {
|
||||||
participantDisplayItems.get(sessionId).setNick(peerConnectionEvent.getNick());
|
participantDisplayItems.get(participantDisplayItemId).setNick(peerConnectionEvent.getNick());
|
||||||
participantsAdapter.notifyDataSetChanged();
|
participantsAdapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
} else if (peerConnectionEvent.getPeerConnectionEventType() ==
|
} else if (peerConnectionEvent.getPeerConnectionEventType() ==
|
||||||
PeerConnectionEvent.PeerConnectionEventType.VIDEO_CHANGE && !isVoiceOnlyCall) {
|
PeerConnectionEvent.PeerConnectionEventType.VIDEO_CHANGE && !isVoiceOnlyCall) {
|
||||||
if (participantDisplayItems.get(sessionId) != null) {
|
if (participantDisplayItems.get(participantDisplayItemId) != null) {
|
||||||
participantDisplayItems.get(sessionId).setStreamEnabled(peerConnectionEvent.getChangeValue());
|
participantDisplayItems.get(participantDisplayItemId).setStreamEnabled(peerConnectionEvent.getChangeValue());
|
||||||
participantsAdapter.notifyDataSetChanged();
|
participantsAdapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
} else if (peerConnectionEvent.getPeerConnectionEventType() ==
|
} else if (peerConnectionEvent.getPeerConnectionEventType() ==
|
||||||
PeerConnectionEvent.PeerConnectionEventType.AUDIO_CHANGE) {
|
PeerConnectionEvent.PeerConnectionEventType.AUDIO_CHANGE) {
|
||||||
if (participantDisplayItems.get(sessionId) != null) {
|
if (participantDisplayItems.get(participantDisplayItemId) != null) {
|
||||||
participantDisplayItems.get(sessionId).setAudioEnabled(peerConnectionEvent.getChangeValue());
|
participantDisplayItems.get(participantDisplayItemId).setAudioEnabled(peerConnectionEvent.getChangeValue());
|
||||||
participantsAdapter.notifyDataSetChanged();
|
participantsAdapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
} else if (peerConnectionEvent.getPeerConnectionEventType() ==
|
} else if (peerConnectionEvent.getPeerConnectionEventType() ==
|
||||||
@ -2382,33 +2402,22 @@ public class CallActivity extends CallBaseActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String urlForAvatar;
|
ParticipantDisplayItem participantDisplayItem = new ParticipantDisplayItem(baseUrl,
|
||||||
if (!TextUtils.isEmpty(userId4Usage)) {
|
|
||||||
urlForAvatar = ApiUtils.getUrlForAvatar(baseUrl,
|
|
||||||
userId4Usage,
|
userId4Usage,
|
||||||
true);
|
|
||||||
} else {
|
|
||||||
urlForAvatar = ApiUtils.getUrlForGuestAvatar(baseUrl,
|
|
||||||
nick,
|
|
||||||
true);
|
|
||||||
}
|
|
||||||
|
|
||||||
ParticipantDisplayItem participantDisplayItem = new ParticipantDisplayItem(userId4Usage,
|
|
||||||
session,
|
session,
|
||||||
connected,
|
connected,
|
||||||
nick,
|
nick,
|
||||||
urlForAvatar,
|
|
||||||
mediaStream,
|
mediaStream,
|
||||||
videoStreamType,
|
videoStreamType,
|
||||||
videoStreamEnabled,
|
videoStreamEnabled,
|
||||||
rootEglBase);
|
rootEglBase);
|
||||||
participantDisplayItems.put(session, participantDisplayItem);
|
participantDisplayItems.put(session + "-" + videoStreamType, participantDisplayItem);
|
||||||
|
|
||||||
initGridAdapter();
|
initGridAdapter();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setCallState(CallStatus callState) {
|
private void setCallState(CallStatus callState) {
|
||||||
if (currentCallStatus == null || !currentCallStatus.equals(callState)) {
|
if (currentCallStatus == null || currentCallStatus != callState) {
|
||||||
currentCallStatus = callState;
|
currentCallStatus = callState;
|
||||||
if (handler == null) {
|
if (handler == null) {
|
||||||
handler = new Handler(Looper.getMainLooper());
|
handler = new Handler(Looper.getMainLooper());
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -46,7 +46,7 @@ class FullScreenImageActivity : AppCompatActivity() {
|
|||||||
private lateinit var path: String
|
private lateinit var path: String
|
||||||
private var showFullscreen = false
|
private var showFullscreen = false
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
menuInflater.inflate(R.menu.menu_preview, menu)
|
menuInflater.inflate(R.menu.menu_preview, menu)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ class FullScreenMediaActivity : AppCompatActivity(), Player.Listener {
|
|||||||
private lateinit var path: String
|
private lateinit var path: String
|
||||||
private lateinit var player: SimpleExoPlayer
|
private lateinit var player: SimpleExoPlayer
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
menuInflater.inflate(R.menu.menu_preview, menu)
|
menuInflater.inflate(R.menu.menu_preview, menu)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ class FullScreenTextViewerActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private lateinit var path: String
|
private lateinit var path: String
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
menuInflater.inflate(R.menu.menu_preview, menu)
|
menuInflater.inflate(R.menu.menu_preview, menu)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
package com.nextcloud.talk.adapters;
|
package com.nextcloud.talk.adapters;
|
||||||
|
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import com.nextcloud.talk.utils.ApiUtils;
|
||||||
|
|
||||||
import org.webrtc.EglBase;
|
import org.webrtc.EglBase;
|
||||||
import org.webrtc.MediaStream;
|
import org.webrtc.MediaStream;
|
||||||
|
|
||||||
public class ParticipantDisplayItem {
|
public class ParticipantDisplayItem {
|
||||||
|
private String baseUrl;
|
||||||
private String userId;
|
private String userId;
|
||||||
private String session;
|
private String session;
|
||||||
private boolean connected;
|
private boolean connected;
|
||||||
@ -15,16 +20,18 @@ public class ParticipantDisplayItem {
|
|||||||
private EglBase rootEglBase;
|
private EglBase rootEglBase;
|
||||||
private boolean isAudioEnabled;
|
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.userId = userId;
|
||||||
this.session = session;
|
this.session = session;
|
||||||
this.connected = connected;
|
this.connected = connected;
|
||||||
this.nick = nick;
|
this.nick = nick;
|
||||||
this.urlForAvatar = urlForAvatar;
|
|
||||||
this.mediaStream = mediaStream;
|
this.mediaStream = mediaStream;
|
||||||
this.streamType = streamType;
|
this.streamType = streamType;
|
||||||
this.streamEnabled = streamEnabled;
|
this.streamEnabled = streamEnabled;
|
||||||
this.rootEglBase = rootEglBase;
|
this.rootEglBase = rootEglBase;
|
||||||
|
|
||||||
|
this.updateUrlForAvatar();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUserId() {
|
public String getUserId() {
|
||||||
@ -33,6 +40,8 @@ public class ParticipantDisplayItem {
|
|||||||
|
|
||||||
public void setUserId(String userId) {
|
public void setUserId(String userId) {
|
||||||
this.userId = userId;
|
this.userId = userId;
|
||||||
|
|
||||||
|
this.updateUrlForAvatar();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getSession() {
|
public String getSession() {
|
||||||
@ -57,14 +66,20 @@ public class ParticipantDisplayItem {
|
|||||||
|
|
||||||
public void setNick(String nick) {
|
public void setNick(String nick) {
|
||||||
this.nick = nick;
|
this.nick = nick;
|
||||||
|
|
||||||
|
this.updateUrlForAvatar();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUrlForAvatar() {
|
public String getUrlForAvatar() {
|
||||||
return urlForAvatar;
|
return urlForAvatar;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUrlForAvatar(String urlForAvatar) {
|
private void updateUrlForAvatar() {
|
||||||
this.urlForAvatar = urlForAvatar;
|
if (!TextUtils.isEmpty(userId)) {
|
||||||
|
urlForAvatar = ApiUtils.getUrlForAvatar(baseUrl, userId, true);
|
||||||
|
} else {
|
||||||
|
urlForAvatar = ApiUtils.getUrlForGuestAvatar(baseUrl, nick, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public MediaStream getMediaStream() {
|
public MediaStream getMediaStream() {
|
||||||
|
@ -145,8 +145,8 @@ public class ContactItem extends AbstractFlexibleItem<ContactItem.ContactItemVie
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (TextUtils.isEmpty(participant.getDisplayName()) &&
|
if (TextUtils.isEmpty(participant.getDisplayName()) &&
|
||||||
(participant.getType().equals(Participant.ParticipantType.GUEST) ||
|
(participant.getType() == Participant.ParticipantType.GUEST ||
|
||||||
participant.getType().equals(Participant.ParticipantType.USER_FOLLOWING_LINK))) {
|
participant.getType() == Participant.ParticipantType.USER_FOLLOWING_LINK)) {
|
||||||
holder.binding.nameText.setText(NextcloudTalkApplication
|
holder.binding.nameText.setText(NextcloudTalkApplication
|
||||||
.Companion
|
.Companion
|
||||||
.getSharedApplication()
|
.getSharedApplication()
|
||||||
@ -167,8 +167,8 @@ public class ContactItem extends AbstractFlexibleItem<ContactItem.ContactItemVie
|
|||||||
|
|
||||||
} else if (
|
} else if (
|
||||||
participant.getCalculatedActorType() == Participant.ActorType.GUESTS ||
|
participant.getCalculatedActorType() == Participant.ActorType.GUESTS ||
|
||||||
Participant.ParticipantType.GUEST.equals(participant.getType()) ||
|
participant.getType() == Participant.ParticipantType.GUEST ||
|
||||||
Participant.ParticipantType.GUEST_MODERATOR.equals(participant.getType())) {
|
participant.getType() == Participant.ParticipantType.GUEST_MODERATOR) {
|
||||||
|
|
||||||
String displayName;
|
String displayName;
|
||||||
|
|
||||||
|
@ -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) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
|
||||||
Drawable[] layers = new Drawable[2];
|
Drawable[] layers = new Drawable[2];
|
||||||
|
@ -140,8 +140,8 @@ public class ParticipantItem extends AbstractFlexibleItem<ParticipantItem.Partic
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (TextUtils.isEmpty(participant.getDisplayName()) &&
|
if (TextUtils.isEmpty(participant.getDisplayName()) &&
|
||||||
(participant.getType().equals(Participant.ParticipantType.GUEST) ||
|
(participant.getType() == Participant.ParticipantType.GUEST ||
|
||||||
participant.getType().equals(Participant.ParticipantType.USER_FOLLOWING_LINK))) {
|
participant.getType() == Participant.ParticipantType.USER_FOLLOWING_LINK)) {
|
||||||
holder.binding.nameText.setText(NextcloudTalkApplication
|
holder.binding.nameText.setText(NextcloudTalkApplication
|
||||||
.Companion
|
.Companion
|
||||||
.getSharedApplication()
|
.getSharedApplication()
|
||||||
@ -170,8 +170,8 @@ public class ParticipantItem extends AbstractFlexibleItem<ParticipantItem.Partic
|
|||||||
holder.binding.avatarDraweeView.setImageResource(R.drawable.ic_circular_mail);
|
holder.binding.avatarDraweeView.setImageResource(R.drawable.ic_circular_mail);
|
||||||
}
|
}
|
||||||
} else if (participant.getCalculatedActorType() == Participant.ActorType.GUESTS ||
|
} else if (participant.getCalculatedActorType() == Participant.ActorType.GUESTS ||
|
||||||
Participant.ParticipantType.GUEST.equals(participant.getType()) ||
|
participant.getType() == Participant.ParticipantType.GUEST ||
|
||||||
Participant.ParticipantType.GUEST_MODERATOR.equals(participant.getType())) {
|
participant.getType() == Participant.ParticipantType.GUEST_MODERATOR) {
|
||||||
|
|
||||||
String displayName = NextcloudTalkApplication.Companion.getSharedApplication()
|
String displayName = NextcloudTalkApplication.Companion.getSharedApplication()
|
||||||
.getResources().getString(R.string.nc_guest);
|
.getResources().getString(R.string.nc_guest);
|
||||||
|
@ -40,13 +40,14 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.core.net.toFile
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import autodagger.AutoInjector
|
import autodagger.AutoInjector
|
||||||
import com.github.dhaval2404.imagepicker.ImagePicker
|
import com.github.dhaval2404.imagepicker.ImagePicker
|
||||||
import com.github.dhaval2404.imagepicker.ImagePicker.Companion.getError
|
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.ImagePicker.Companion.with
|
||||||
|
import com.github.dhaval2404.imagepicker.constant.ImageProvider
|
||||||
import com.nextcloud.talk.R
|
import com.nextcloud.talk.R
|
||||||
import com.nextcloud.talk.activities.TakePhotoActivity
|
import com.nextcloud.talk.activities.TakePhotoActivity
|
||||||
import com.nextcloud.talk.api.NcApi
|
import com.nextcloud.talk.api.NcApi
|
||||||
@ -486,14 +487,13 @@ class ProfileController : BaseController(R.layout.controller_profile) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun sendSelectLocalFileIntent() {
|
private fun sendSelectLocalFileIntent() {
|
||||||
val intent = with(activity!!)
|
with(activity!!)
|
||||||
.galleryOnly()
|
.provider(ImageProvider.GALLERY)
|
||||||
.crop()
|
.crop()
|
||||||
.cropSquare()
|
.cropSquare()
|
||||||
.compress(MAX_SIZE)
|
.compress(MAX_SIZE)
|
||||||
.maxResultSize(MAX_SIZE, MAX_SIZE)
|
.maxResultSize(MAX_SIZE, MAX_SIZE)
|
||||||
.prepareIntent()
|
.createIntent { intent -> startActivityForResult(intent, REQUEST_CODE_IMAGE_PICKER) }
|
||||||
startActivityForResult(intent, 1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showBrowserScreen() {
|
private fun showBrowserScreen() {
|
||||||
@ -584,21 +584,21 @@ class ProfileController : BaseController(R.layout.controller_profile) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun openImageWithPicker(file: File) {
|
private fun openImageWithPicker(file: File) {
|
||||||
val intent = with(activity!!)
|
with(activity!!)
|
||||||
.fileOnly()
|
.provider(ImageProvider.URI)
|
||||||
.crop()
|
.crop()
|
||||||
.cropSquare()
|
.cropSquare()
|
||||||
.compress(MAX_SIZE)
|
.compress(MAX_SIZE)
|
||||||
.maxResultSize(MAX_SIZE, MAX_SIZE)
|
.maxResultSize(MAX_SIZE, MAX_SIZE)
|
||||||
.prepareIntent()
|
.setUri(Uri.fromFile(file))
|
||||||
intent.putExtra("extra.file", file)
|
.createIntent { intent -> startActivityForResult(intent, REQUEST_CODE_IMAGE_PICKER) }
|
||||||
startActivityForResult(intent, REQUEST_CODE_IMAGE_PICKER)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
if (resultCode == Activity.RESULT_OK) {
|
||||||
if (requestCode == REQUEST_CODE_IMAGE_PICKER) {
|
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) {
|
} else if (requestCode == REQUEST_CODE_SELECT_REMOTE_FILES) {
|
||||||
val pathList = data?.getStringArrayListExtra(RemoteFileBrowserActivity.EXTRA_SELECTED_PATHS)
|
val pathList = data?.getStringArrayListExtra(RemoteFileBrowserActivity.EXTRA_SELECTED_PATHS)
|
||||||
if (pathList?.size!! >= 1) {
|
if (pathList?.size!! >= 1) {
|
||||||
|
@ -299,7 +299,7 @@ public class RestModule {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
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()),
|
proxy = new Proxy(Proxy.Type.valueOf(appPreferences.getProxyType()),
|
||||||
InetSocketAddress.createUnresolved(appPreferences.getProxyHost(), Integer.parseInt(
|
InetSocketAddress.createUnresolved(appPreferences.getProxyHost(), Integer.parseInt(
|
||||||
appPreferences.getProxyPort())));
|
appPreferences.getProxyPort())));
|
||||||
|
@ -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
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
849
app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt
Normal file
849
app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@ -128,7 +128,7 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
|
|||||||
val mimeType = context.contentResolver.getType(sourceFileUri)?.toMediaTypeOrNull()
|
val mimeType = context.contentResolver.getType(sourceFileUri)?.toMediaTypeOrNull()
|
||||||
|
|
||||||
uploadSuccess = ChunkedFileUploader(
|
uploadSuccess = ChunkedFileUploader(
|
||||||
okHttpClient!!,
|
okHttpClient,
|
||||||
currentUser,
|
currentUser,
|
||||||
roomToken,
|
roomToken,
|
||||||
metaData,
|
metaData,
|
||||||
|
@ -210,13 +210,13 @@ class MessageSearchActivity : BaseActivity() {
|
|||||||
binding.emptyContainer.emptyListView.visibility = View.VISIBLE
|
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)
|
menuInflater.inflate(R.menu.menu_search, menu)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
||||||
val menuItem = menu!!.findItem(R.id.action_search)
|
val menuItem = menu.findItem(R.id.action_search)
|
||||||
searchView = menuItem.actionView as SearchView
|
searchView = menuItem.actionView as SearchView
|
||||||
setupSearchView()
|
setupSearchView()
|
||||||
menuItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
menuItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||||
|
@ -56,7 +56,7 @@ data class Notification(
|
|||||||
@JsonField(name = ["messageRich"])
|
@JsonField(name = ["messageRich"])
|
||||||
var messageRich: String?,
|
var messageRich: String?,
|
||||||
@JsonField(name = ["messageRichParameters"])
|
@JsonField(name = ["messageRichParameters"])
|
||||||
var messageRichParameters: HashMap<String, HashMap<String, String>>?,
|
var messageRichParameters: HashMap<String?, HashMap<String?, String?>>?,
|
||||||
@JsonField(name = ["link"])
|
@JsonField(name = ["link"])
|
||||||
var link: String?,
|
var link: String?,
|
||||||
@JsonField(name = ["actions"])
|
@JsonField(name = ["actions"])
|
||||||
|
@ -24,6 +24,8 @@ package com.nextcloud.talk.models.json.notifications
|
|||||||
import com.bluelinelabs.logansquare.annotation.JsonField
|
import com.bluelinelabs.logansquare.annotation.JsonField
|
||||||
import com.bluelinelabs.logansquare.annotation.JsonObject
|
import com.bluelinelabs.logansquare.annotation.JsonObject
|
||||||
|
|
||||||
|
// see https://github.com/nextcloud/notifications/blob/master/docs/ocs-endpoint-v2.md
|
||||||
|
|
||||||
@JsonObject
|
@JsonObject
|
||||||
data class NotificationOverall(
|
data class NotificationOverall(
|
||||||
@JsonField(name = ["ocs"])
|
@JsonField(name = ["ocs"])
|
||||||
|
@ -70,6 +70,7 @@ data class DecryptedPushMessage(
|
|||||||
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
|
// 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)
|
constructor() : this(null, null, "", null, 0, null, false, false, false, null, null, 0, null)
|
||||||
|
|
||||||
|
@Suppress("Detekt.ComplexMethod")
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
|
@ -180,7 +180,7 @@ class RemoteFileBrowserActivity : AppCompatActivity(), SelectionInterface, Swipe
|
|||||||
showList()
|
showList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
menuInflater.inflate(R.menu.menu_share_files, menu)
|
menuInflater.inflate(R.menu.menu_share_files, menu)
|
||||||
filesSelectionDoneMenuItem = menu?.findItem(R.id.files_selection_done)
|
filesSelectionDoneMenuItem = menu?.findItem(R.id.files_selection_done)
|
||||||
|
@ -391,6 +391,7 @@ public class ApiUtils {
|
|||||||
getApplicationContext().getResources().getString(R.string.nc_push_server_url) + "/devices";
|
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) {
|
public static String getUrlForNotificationWithId(String baseUrl, String notificationId) {
|
||||||
return baseUrl + ocsApiVersion + "/apps/notifications/api/v2/notifications/" + notificationId;
|
return baseUrl + ocsApiVersion + "/apps/notifications/api/v2/notifications/" + notificationId;
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,7 @@ import com.nextcloud.talk.utils.bundle.BundleKeys
|
|||||||
import com.nextcloud.talk.utils.preferences.AppPreferences
|
import com.nextcloud.talk.utils.preferences.AppPreferences
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
|
@Suppress("TooManyFunctions")
|
||||||
object NotificationUtils {
|
object NotificationUtils {
|
||||||
|
|
||||||
enum class NotificationChannels {
|
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 ->
|
scanNotifications(context, conversationUser) { notificationManager, statusBarNotification, notification ->
|
||||||
if (notificationId == notification.extras.getLong(BundleKeys.KEY_NOTIFICATION_ID)) {
|
if (notificationId == notification.extras.getLong(BundleKeys.KEY_NOTIFICATION_ID)) {
|
||||||
notificationManager.cancel(statusBarNotification.id)
|
notificationManager.cancel(statusBarNotification.id)
|
||||||
|
@ -467,7 +467,7 @@ public class MagicWebSocketInstance extends WebSocketListener {
|
|||||||
|
|
||||||
@Subscribe(threadMode = ThreadMode.BACKGROUND)
|
@Subscribe(threadMode = ThreadMode.BACKGROUND)
|
||||||
public void onMessageEvent(NetworkEvent networkEvent) {
|
public void onMessageEvent(NetworkEvent networkEvent) {
|
||||||
if (networkEvent.getNetworkConnectionEvent().equals(NetworkEvent.NetworkConnectionEvent.NETWORK_CONNECTED) && !isConnected()) {
|
if (networkEvent.getNetworkConnectionEvent() == NetworkEvent.NetworkConnectionEvent.NETWORK_CONNECTED && !isConnected()) {
|
||||||
restartWebSocket();
|
restartWebSocket();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -276,7 +276,7 @@ public class PeerConnectionWrapper {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStateChange() {
|
public void onStateChange() {
|
||||||
if (dataChannel != null && dataChannel.state().equals(DataChannel.State.OPEN) &&
|
if (dataChannel != null && dataChannel.state() == DataChannel.State.OPEN &&
|
||||||
dataChannel.label().equals("status")) {
|
dataChannel.label().equals("status")) {
|
||||||
sendInitialMediaStatus();
|
sendInitialMediaStatus();
|
||||||
}
|
}
|
||||||
@ -343,9 +343,9 @@ public class PeerConnectionWrapper {
|
|||||||
public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
|
public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
|
||||||
|
|
||||||
Log.d("iceConnectionChangeTo: ", iceConnectionState.name() + " over " + peerConnection.hashCode() + " " + sessionId);
|
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,
|
EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType.PEER_CONNECTED,
|
||||||
sessionId, null, null, null));
|
sessionId, null, null, videoStreamType));
|
||||||
|
|
||||||
if (!isMCUPublisher) {
|
if (!isMCUPublisher) {
|
||||||
EventBus.getDefault().post(new MediaStreamEvent(remoteStream, sessionId, videoStreamType));
|
EventBus.getDefault().post(new MediaStreamEvent(remoteStream, sessionId, videoStreamType));
|
||||||
@ -354,22 +354,22 @@ public class PeerConnectionWrapper {
|
|||||||
if (hasInitiated) {
|
if (hasInitiated) {
|
||||||
sendInitialMediaStatus();
|
sendInitialMediaStatus();
|
||||||
}
|
}
|
||||||
} else if (iceConnectionState.equals(PeerConnection.IceConnectionState.COMPLETED)) {
|
} else if (iceConnectionState == PeerConnection.IceConnectionState.COMPLETED) {
|
||||||
EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType.PEER_CONNECTED,
|
EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType.PEER_CONNECTED,
|
||||||
sessionId, null, null, null));
|
sessionId, null, null, videoStreamType));
|
||||||
} else if (iceConnectionState.equals(PeerConnection.IceConnectionState.CLOSED)) {
|
} else if (iceConnectionState == PeerConnection.IceConnectionState.CLOSED) {
|
||||||
EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType
|
EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType
|
||||||
.PEER_CLOSED, sessionId, null, null, videoStreamType));
|
.PEER_CLOSED, sessionId, null, null, videoStreamType));
|
||||||
} else if (iceConnectionState.equals(PeerConnection.IceConnectionState.DISCONNECTED) ||
|
} else if (iceConnectionState == PeerConnection.IceConnectionState.DISCONNECTED ||
|
||||||
iceConnectionState.equals(PeerConnection.IceConnectionState.NEW) ||
|
iceConnectionState == PeerConnection.IceConnectionState.NEW ||
|
||||||
iceConnectionState.equals(PeerConnection.IceConnectionState.CHECKING)) {
|
iceConnectionState == PeerConnection.IceConnectionState.CHECKING) {
|
||||||
EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType.PEER_DISCONNECTED,
|
EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType.PEER_DISCONNECTED,
|
||||||
sessionId, null, null, null));
|
sessionId, null, null, videoStreamType));
|
||||||
} else if (iceConnectionState.equals(PeerConnection.IceConnectionState.FAILED)) {
|
} else if (iceConnectionState == PeerConnection.IceConnectionState.FAILED) {
|
||||||
EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType.PEER_DISCONNECTED,
|
EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType.PEER_DISCONNECTED,
|
||||||
sessionId, null, null, null));
|
sessionId, null, null, videoStreamType));
|
||||||
if (isMCUPublisher) {
|
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()) {
|
if (shouldNotReceiveVideo()) {
|
||||||
for (RtpTransceiver t : peerConnection.getTransceivers()) {
|
for (RtpTransceiver t : peerConnection.getTransceivers()) {
|
||||||
if (t.getMediaType().equals(MediaStreamTrack.MediaType.MEDIA_TYPE_VIDEO)) {
|
if (t.getMediaType() == MediaStreamTrack.MediaType.MEDIA_TYPE_VIDEO) {
|
||||||
t.stop();
|
t.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -129,7 +129,7 @@ public class WebRtcAudioManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userSelectedAudioDevice.equals(AudioDevice.SPEAKER_PHONE)
|
if (userSelectedAudioDevice == AudioDevice.SPEAKER_PHONE
|
||||||
&& audioDevices.contains(AudioDevice.EARPIECE)
|
&& audioDevices.contains(AudioDevice.EARPIECE)
|
||||||
&& audioDevices.contains(AudioDevice.SPEAKER_PHONE)) {
|
&& audioDevices.contains(AudioDevice.SPEAKER_PHONE)) {
|
||||||
|
|
||||||
|
@ -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>
|
@ -173,7 +173,6 @@
|
|||||||
<string name="nc_new_mention">إشارات غير مقروءة</string>
|
<string name="nc_new_mention">إشارات غير مقروءة</string>
|
||||||
<string name="nc_new_messages">رسائل غير مقروءة</string>
|
<string name="nc_new_messages">رسائل غير مقروءة</string>
|
||||||
<string name="nc_new_password">كلمة سرية جديدة</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_nick_guest">ضيف</string>
|
||||||
<string name="nc_no">لا</string>
|
<string name="nc_no">لا</string>
|
||||||
<string name="nc_no_messages_yet">ليس لديك رسائل بعد</string>
|
<string name="nc_no_messages_yet">ليس لديك رسائل بعد</string>
|
||||||
|
@ -118,7 +118,6 @@
|
|||||||
<string name="nc_never">Never joined</string>
|
<string name="nc_never">Never joined</string>
|
||||||
<string name="nc_new_conversation">New conversation</string>
|
<string name="nc_new_conversation">New conversation</string>
|
||||||
<string name="nc_new_password">New password</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_nick_guest">Guest</string>
|
||||||
<string name="nc_no">No</string>
|
<string name="nc_no">No</string>
|
||||||
<string name="nc_no_messages_yet">No messages yet</string>
|
<string name="nc_no_messages_yet">No messages yet</string>
|
||||||
|
@ -187,13 +187,13 @@
|
|||||||
<string name="nc_message_read">Съобщението е прочетено</string>
|
<string name="nc_message_read">Съобщението е прочетено</string>
|
||||||
<string name="nc_message_sent">Съобщението е изпратено</string>
|
<string name="nc_message_sent">Съобщението е изпратено</string>
|
||||||
<string name="nc_microphone_permission_permanently_denied">За активиране на гласова комуникация, моля, дайте право на „Микрофон“ в системните настройки.</string>
|
<string name="nc_microphone_permission_permanently_denied">За активиране на гласова комуникация, моля, дайте право на „Микрофон“ в системните настройки.</string>
|
||||||
|
<string name="nc_missed_call">Пропуснахте обаждане от %s</string>
|
||||||
<string name="nc_moderator">Модератор</string>
|
<string name="nc_moderator">Модератор</string>
|
||||||
<string name="nc_never">Никога не е присъединяван</string>
|
<string name="nc_never">Никога не е присъединяван</string>
|
||||||
<string name="nc_new_conversation">Нов разговор</string>
|
<string name="nc_new_conversation">Нов разговор</string>
|
||||||
<string name="nc_new_mention">Непрочетени споменавания</string>
|
<string name="nc_new_mention">Непрочетени споменавания</string>
|
||||||
<string name="nc_new_messages">Непрочетени съобщения.</string>
|
<string name="nc_new_messages">Непрочетени съобщения.</string>
|
||||||
<string name="nc_new_password">Нова парола</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_nick_guest">Гост</string>
|
||||||
<string name="nc_no">Не</string>
|
<string name="nc_no">Не</string>
|
||||||
<string name="nc_no_messages_yet">Няма съобщения.</string>
|
<string name="nc_no_messages_yet">Няма съобщения.</string>
|
||||||
|
@ -4,6 +4,8 @@
|
|||||||
<string name="appbar_search_in">Cerca a %s</string>
|
<string name="appbar_search_in">Cerca a %s</string>
|
||||||
<string name="audio_output_dialog_headline">Sortida d\'àudio</string>
|
<string name="audio_output_dialog_headline">Sortida d\'àudio</string>
|
||||||
<string name="audio_output_phone">Telèfon</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="avatar">Avatar</string>
|
||||||
<string name="away">Absent</string>
|
<string name="away">Absent</string>
|
||||||
<string name="clear_status_message">Esborrar el missatge d\'estat</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_biggest_first">Més gran primer</string>
|
||||||
<string name="menu_item_sort_by_size_smallest_first">Més petit 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_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="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_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_about">Quant a</string>
|
||||||
<string name="nc_account_chooser_active_user">Usuari actiu</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_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_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_attachment">Afegeix un adjunt</string>
|
||||||
<string name="nc_add_emojis">Afegeix emoji</string>
|
<string name="nc_add_emojis">Afegeix emoji</string>
|
||||||
<string name="nc_add_participants">Afegeix participants</string>
|
<string name="nc_add_participants">Afegeix participants</string>
|
||||||
@ -62,6 +67,7 @@
|
|||||||
<string name="nc_common_set">Estableix</string>
|
<string name="nc_common_set">Estableix</string>
|
||||||
<string name="nc_common_skip">Omet</string>
|
<string name="nc_common_skip">Omet</string>
|
||||||
<string name="nc_configure_cert_auth">Selecciona el certificat d’autenticació</string>
|
<string name="nc_configure_cert_auth">Selecciona el certificat d’autenticació</string>
|
||||||
|
<string name="nc_connecting_call">S\'està connectant …</string>
|
||||||
<string name="nc_contacts_done">Fet</string>
|
<string name="nc_contacts_done">Fet</string>
|
||||||
<string name="nc_conversation_link">Enllaç de conversa</string>
|
<string name="nc_conversation_link">Enllaç de conversa</string>
|
||||||
<string name="nc_conversation_menu_conversation_info">Informació de la 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 s’ha pogut obtenir el nom de visualització, s\'està avortant</string>
|
<string name="nc_display_name_not_fetched">No s’ha 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_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_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_off">Desactivada</string>
|
||||||
<string name="nc_expire_message_one_day">1 dia</string>
|
<string name="nc_expire_message_one_day">1 dia</string>
|
||||||
<string name="nc_expire_message_one_hour">1 hora</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_dialog_hint">Introduïu una contrasenya</string>
|
||||||
<string name="nc_guest_access_password_title">Protecció amb 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_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">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_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>
|
<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_limit_hit">S\'ha arribat al límit de %s caràcters </string>
|
||||||
<string name="nc_lobby">Vestíbul</string>
|
<string name="nc_lobby">Vestíbul</string>
|
||||||
<string name="nc_lobby_waiting">Esteu esperant al 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_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_private">Fes que la conversa sigui privada</string>
|
||||||
<string name="nc_make_call_public">Fes que la conversa sigui pública</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_conversation">Nova conversa</string>
|
||||||
<string name="nc_new_messages">Missatges sense llegir</string>
|
<string name="nc_new_messages">Missatges sense llegir</string>
|
||||||
<string name="nc_new_password">Nova contrasenya</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_nick_guest">Convidat</string>
|
||||||
<string name="nc_no">No</string>
|
<string name="nc_no">No</string>
|
||||||
<string name="nc_no_messages_yet">No hi ha cap missatge encara</string>
|
<string name="nc_no_messages_yet">No hi ha cap missatge encara</string>
|
||||||
|
@ -187,13 +187,14 @@
|
|||||||
<string name="nc_message_read">Zpráva přečtena</string>
|
<string name="nc_message_read">Zpráva přečtena</string>
|
||||||
<string name="nc_message_sent">Zpráva odeslána</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_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_moderator">Moderátor</string>
|
||||||
<string name="nc_never">Nikdy nepřipojeno</string>
|
<string name="nc_never">Nikdy nepřipojeno</string>
|
||||||
<string name="nc_new_conversation">Nová konverzace</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_mention">Nepřečtená zmínění</string>
|
||||||
<string name="nc_new_messages">Nepřečtené zprávy</string>
|
<string name="nc_new_messages">Nepřečtené zprávy</string>
|
||||||
<string name="nc_new_password">Nové heslo</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_nick_guest">Host</string>
|
||||||
<string name="nc_no">Ne</string>
|
<string name="nc_no">Ne</string>
|
||||||
<string name="nc_no_messages_yet">Zatím žádné zprávy</string>
|
<string name="nc_no_messages_yet">Zatím žádné zprávy</string>
|
||||||
|
@ -129,7 +129,6 @@
|
|||||||
<string name="nc_new_conversation">Ny samtale</string>
|
<string name="nc_new_conversation">Ny samtale</string>
|
||||||
<string name="nc_new_messages">Ulæste beskedder</string>
|
<string name="nc_new_messages">Ulæste beskedder</string>
|
||||||
<string name="nc_new_password">Ny adgangskode</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_nick_guest">Gæst</string>
|
||||||
<string name="nc_no">Nej</string>
|
<string name="nc_no">Nej</string>
|
||||||
<string name="nc_no_messages_yet">Ingen beskeder endnu</string>
|
<string name="nc_no_messages_yet">Ingen beskeder endnu</string>
|
||||||
|
@ -187,13 +187,14 @@
|
|||||||
<string name="nc_message_read">Nachricht gelesen</string>
|
<string name="nc_message_read">Nachricht gelesen</string>
|
||||||
<string name="nc_message_sent">Nachricht gesendet</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_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_moderator">Moderator</string>
|
||||||
<string name="nc_never">Nie beigetreten</string>
|
<string name="nc_never">Nie beigetreten</string>
|
||||||
<string name="nc_new_conversation">Neue Unterhaltung</string>
|
<string name="nc_new_conversation">Neue Unterhaltung</string>
|
||||||
<string name="nc_new_mention">Ungelesene Erwähnungen</string>
|
<string name="nc_new_mention">Ungelesene Erwähnungen</string>
|
||||||
<string name="nc_new_messages">Ungelesene Nachrichten</string>
|
<string name="nc_new_messages">Ungelesene Nachrichten</string>
|
||||||
<string name="nc_new_password">Neues Passwort</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_nick_guest">Gast</string>
|
||||||
<string name="nc_no">Nein</string>
|
<string name="nc_no">Nein</string>
|
||||||
<string name="nc_no_messages_yet">Noch keine Nachrichten</string>
|
<string name="nc_no_messages_yet">Noch keine Nachrichten</string>
|
||||||
|
@ -165,7 +165,6 @@
|
|||||||
<string name="nc_new_conversation">Νέα συνομιλία</string>
|
<string name="nc_new_conversation">Νέα συνομιλία</string>
|
||||||
<string name="nc_new_messages">Μη αναγνωσμένα μηνύματα</string>
|
<string name="nc_new_messages">Μη αναγνωσμένα μηνύματα</string>
|
||||||
<string name="nc_new_password">Νέο συνθηματικό</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_nick_guest">Επισκέπτης</string>
|
||||||
<string name="nc_no">Όχι</string>
|
<string name="nc_no">Όχι</string>
|
||||||
<string name="nc_no_messages_yet">Κανένα μήνυμα ακόμα</string>
|
<string name="nc_no_messages_yet">Κανένα μήνυμα ακόμα</string>
|
||||||
|
@ -187,13 +187,14 @@
|
|||||||
<string name="nc_message_read">Mensajes leídos</string>
|
<string name="nc_message_read">Mensajes leídos</string>
|
||||||
<string name="nc_message_sent">Mensaje enviado</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_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_moderator">Moderador</string>
|
||||||
<string name="nc_never">Nunca unido</string>
|
<string name="nc_never">Nunca unido</string>
|
||||||
<string name="nc_new_conversation">Nueva conversación</string>
|
<string name="nc_new_conversation">Nueva conversación</string>
|
||||||
<string name="nc_new_mention">Menciones sin leer</string>
|
<string name="nc_new_mention">Menciones sin leer</string>
|
||||||
<string name="nc_new_messages">Mensajes no leídos</string>
|
<string name="nc_new_messages">Mensajes no leídos</string>
|
||||||
<string name="nc_new_password">Nueva contraseña</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_nick_guest">Invitado</string>
|
||||||
<string name="nc_no">No</string>
|
<string name="nc_no">No</string>
|
||||||
<string name="nc_no_messages_yet">Aún no hay mensajes</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_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_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">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_failed_notification_title">Falla</string>
|
||||||
<string name="nc_upload_from_cloud">Compartido por %1$s</string>
|
<string name="nc_upload_from_cloud">Compartido por %1$s</string>
|
||||||
<string name="nc_upload_from_device">Subir desde dispositivo</string>
|
<string name="nc_upload_from_device">Subir desde dispositivo</string>
|
||||||
|
@ -193,7 +193,6 @@
|
|||||||
<string name="nc_new_mention">Irakurri gabeko aipamenak</string>
|
<string name="nc_new_mention">Irakurri gabeko aipamenak</string>
|
||||||
<string name="nc_new_messages">Irakurri gabeko mezuak</string>
|
<string name="nc_new_messages">Irakurri gabeko mezuak</string>
|
||||||
<string name="nc_new_password">Pasahitz berria</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_nick_guest">Gonbidatua</string>
|
||||||
<string name="nc_no">Ez</string>
|
<string name="nc_no">Ez</string>
|
||||||
<string name="nc_no_messages_yet">Ez dago mezurik oraindik</string>
|
<string name="nc_no_messages_yet">Ez dago mezurik oraindik</string>
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
<string name="file_list_loading">بارگذاری …</string>
|
<string name="file_list_loading">بارگذاری …</string>
|
||||||
<string name="fourHours">۴ ساعت</string>
|
<string name="fourHours">۴ ساعت</string>
|
||||||
<string name="invisible">نامرئی</string>
|
<string name="invisible">نامرئی</string>
|
||||||
|
<string name="load_more_results">بار کردن نتیحههای بیشتر</string>
|
||||||
<string name="lock_symbol">نماد قفل</string>
|
<string name="lock_symbol">نماد قفل</string>
|
||||||
<string name="menu_item_sort_by_date_newest_first">تازهترینها اول </string>
|
<string name="menu_item_sort_by_date_newest_first">تازهترینها اول </string>
|
||||||
<string name="menu_item_sort_by_date_oldest_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_conversation">مکالمه جدید</string>
|
||||||
<string name="nc_new_messages">پیامهای خوانده نشده</string>
|
<string name="nc_new_messages">پیامهای خوانده نشده</string>
|
||||||
<string name="nc_new_password">گذرواژه جدید</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_nick_guest">مهمان</string>
|
||||||
<string name="nc_no">نه</string>
|
<string name="nc_no">نه</string>
|
||||||
<string name="nc_no_messages_yet">هنوز پیامی ارسال نشده است</string>
|
<string name="nc_no_messages_yet">هنوز پیامی ارسال نشده است</string>
|
||||||
|
@ -164,7 +164,6 @@
|
|||||||
<string name="nc_new_mention">Lukemattomat maininnat</string>
|
<string name="nc_new_mention">Lukemattomat maininnat</string>
|
||||||
<string name="nc_new_messages">Lukemattomat viestit</string>
|
<string name="nc_new_messages">Lukemattomat viestit</string>
|
||||||
<string name="nc_new_password">Uusi salasana</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_nick_guest">Vieras</string>
|
||||||
<string name="nc_no">Ei</string>
|
<string name="nc_no">Ei</string>
|
||||||
<string name="nc_no_messages_yet">Ei viestejä vielä</string>
|
<string name="nc_no_messages_yet">Ei viestejä vielä</string>
|
||||||
|
@ -187,13 +187,13 @@
|
|||||||
<string name="nc_message_read">Message lu</string>
|
<string name="nc_message_read">Message lu</string>
|
||||||
<string name="nc_message_sent">Message envoyé</string>
|
<string name="nc_message_sent">Message envoyé</string>
|
||||||
<string name="nc_microphone_permission_permanently_denied">Pour établir une communication audio, veuillez autoriser l’utilisation du microphone dans les paramètres du système.</string>
|
<string name="nc_microphone_permission_permanently_denied">Pour établir une communication audio, veuillez autoriser l’utilisation 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_moderator">Modérateur</string>
|
||||||
<string name="nc_never">Jamais contacté</string>
|
<string name="nc_never">Jamais contacté</string>
|
||||||
<string name="nc_new_conversation">Nouvelle conversation</string>
|
<string name="nc_new_conversation">Nouvelle conversation</string>
|
||||||
<string name="nc_new_mention">Mentions non lues</string>
|
<string name="nc_new_mention">Mentions non lues</string>
|
||||||
<string name="nc_new_messages">Messages non lus</string>
|
<string name="nc_new_messages">Messages non lus</string>
|
||||||
<string name="nc_new_password">Nouveau mot de passe</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_nick_guest">Invité</string>
|
||||||
<string name="nc_no">Non</string>
|
<string name="nc_no">Non</string>
|
||||||
<string name="nc_no_messages_yet">Pas de messages</string>
|
<string name="nc_no_messages_yet">Pas de messages</string>
|
||||||
|
@ -140,7 +140,6 @@
|
|||||||
<string name="nc_new_conversation">Nova conversa</string>
|
<string name="nc_new_conversation">Nova conversa</string>
|
||||||
<string name="nc_new_messages">Mensaxes sen ler</string>
|
<string name="nc_new_messages">Mensaxes sen ler</string>
|
||||||
<string name="nc_new_password">Novo contrasinal</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_nick_guest">Convidado</string>
|
||||||
<string name="nc_no">Non</string>
|
<string name="nc_no">Non</string>
|
||||||
<string name="nc_no_messages_yet">Aínda non hai mensaxes</string>
|
<string name="nc_no_messages_yet">Aínda non hai mensaxes</string>
|
||||||
|
@ -172,7 +172,6 @@
|
|||||||
<string name="nc_new_mention">Nepročitana spominjanja</string>
|
<string name="nc_new_mention">Nepročitana spominjanja</string>
|
||||||
<string name="nc_new_messages">Nove poruke</string>
|
<string name="nc_new_messages">Nove poruke</string>
|
||||||
<string name="nc_new_password">Nova zaporka</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_nick_guest">Gost</string>
|
||||||
<string name="nc_no">Ne</string>
|
<string name="nc_no">Ne</string>
|
||||||
<string name="nc_no_messages_yet">Još nema poruka</string>
|
<string name="nc_no_messages_yet">Još nema poruka</string>
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
<string name="avatar">Profilkép</string>
|
<string name="avatar">Profilkép</string>
|
||||||
<string name="away">Távol</string>
|
<string name="away">Távol</string>
|
||||||
<string name="call_without_notification">Hívás értesítés nélkül</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="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">Állapotüzenet törlése</string>
|
||||||
<string name="clear_status_message_after">Állapotüzenet törlése ennyi idő után:</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_category_recent">Legutóbbiak</string>
|
||||||
<string name="emoji_search">Emodzsi keresése</string>
|
<string name="emoji_search">Emodzsi keresése</string>
|
||||||
<string name="encrypted">Titkosított</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="failed_to_save">Sikertelen mentés: %1$s</string>
|
||||||
<string name="file_list_folder">mappa</string>
|
<string name="file_list_folder">mappa</string>
|
||||||
<string name="file_list_loading">Betöltés…</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_read">Üzenet elolvasva</string>
|
||||||
<string name="nc_message_sent">Üzenet elküldve</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_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_moderator">Moderátor</string>
|
||||||
<string name="nc_never">Soha nem csatlakozott</string>
|
<string name="nc_never">Soha nem csatlakozott</string>
|
||||||
<string name="nc_new_conversation">Új beszélgetés</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_mention">Olvasatlan említések</string>
|
||||||
<string name="nc_new_messages">Olvasatlan üzenetek</string>
|
<string name="nc_new_messages">Olvasatlan üzenetek</string>
|
||||||
<string name="nc_new_password">Új jelszó</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_nick_guest">Vendég</string>
|
||||||
<string name="nc_no">Nem</string>
|
<string name="nc_no">Nem</string>
|
||||||
<string name="nc_no_messages_yet">Nincs még üzenet</string>
|
<string name="nc_no_messages_yet">Nincs még üzenet</string>
|
||||||
<string name="nc_no_proxy">Nincs proxy</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">%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">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_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">Üzenetek</string>
|
||||||
<string name="nc_notification_channel_messages_description">Értesítés a bejövő üzenetekről</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">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_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_always">Mindig értesítsen</string>
|
||||||
<string name="nc_notify_me_mention">Értesítsen, ha megemlítik Önt</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_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_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">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_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_from_device">Feltöltés az eszközről</string>
|
||||||
<string name="nc_upload_in_progess">Feltöltés</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_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_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_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_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>
|
<string name="nc_voice_message_missing_audio_permission">Hangrögzítési engedély szükséges</string>
|
||||||
|
@ -127,7 +127,6 @@
|
|||||||
<string name="nc_new_conversation">Nýtt samtal</string>
|
<string name="nc_new_conversation">Nýtt samtal</string>
|
||||||
<string name="nc_new_messages">Ólesin skilaboð</string>
|
<string name="nc_new_messages">Ólesin skilaboð</string>
|
||||||
<string name="nc_new_password">Nýtt lykilorð</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_nick_guest">Gestur</string>
|
||||||
<string name="nc_no">Nei</string>
|
<string name="nc_no">Nei</string>
|
||||||
<string name="nc_no_messages_yet">Engin skilaboð ennþá</string>
|
<string name="nc_no_messages_yet">Engin skilaboð ennþá</string>
|
||||||
|
@ -178,7 +178,6 @@
|
|||||||
<string name="nc_new_mention">Menzioni non lette</string>
|
<string name="nc_new_mention">Menzioni non lette</string>
|
||||||
<string name="nc_new_messages">Messaggi non letti</string>
|
<string name="nc_new_messages">Messaggi non letti</string>
|
||||||
<string name="nc_new_password">Nuova password</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_nick_guest">Ospite</string>
|
||||||
<string name="nc_no">No</string>
|
<string name="nc_no">No</string>
|
||||||
<string name="nc_no_messages_yet">Ancora nessun messaggio</string>
|
<string name="nc_no_messages_yet">Ancora nessun messaggio</string>
|
||||||
|
@ -127,7 +127,6 @@
|
|||||||
<string name="nc_new_conversation">דיון חדש</string>
|
<string name="nc_new_conversation">דיון חדש</string>
|
||||||
<string name="nc_new_messages">הודעות שלא נקראו</string>
|
<string name="nc_new_messages">הודעות שלא נקראו</string>
|
||||||
<string name="nc_new_password">ססמה חדשה</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_nick_guest">אורח/ת</string>
|
||||||
<string name="nc_no">לא</string>
|
<string name="nc_no">לא</string>
|
||||||
<string name="nc_no_messages_yet">אין הודעות עדיין</string>
|
<string name="nc_no_messages_yet">אין הודעות עדיין</string>
|
||||||
|
@ -179,7 +179,6 @@
|
|||||||
<string name="nc_new_mention">未読の返信</string>
|
<string name="nc_new_mention">未読の返信</string>
|
||||||
<string name="nc_new_messages">未読のメッセージ</string>
|
<string name="nc_new_messages">未読のメッセージ</string>
|
||||||
<string name="nc_new_password">新しいパスワード</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_nick_guest">ゲスト</string>
|
||||||
<string name="nc_no">いいえ</string>
|
<string name="nc_no">いいえ</string>
|
||||||
<string name="nc_no_messages_yet">メッセージはまだありません</string>
|
<string name="nc_no_messages_yet">メッセージはまだありません</string>
|
||||||
|
@ -147,7 +147,7 @@
|
|||||||
<string name="nc_new_mention">읽지 않은 언급</string>
|
<string name="nc_new_mention">읽지 않은 언급</string>
|
||||||
<string name="nc_new_messages">읽지 않은 메세지</string>
|
<string name="nc_new_messages">읽지 않은 메세지</string>
|
||||||
<string name="nc_new_password">새 암호</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_nick_guest">손님</string>
|
||||||
<string name="nc_no">아니요</string>
|
<string name="nc_no">아니요</string>
|
||||||
<string name="nc_no_messages_yet">아직 메시지 없음</string>
|
<string name="nc_no_messages_yet">아직 메시지 없음</string>
|
||||||
|
@ -125,7 +125,6 @@
|
|||||||
<string name="nc_new_conversation">Naujas pokalbis</string>
|
<string name="nc_new_conversation">Naujas pokalbis</string>
|
||||||
<string name="nc_new_messages">Neskaitytos žinutės</string>
|
<string name="nc_new_messages">Neskaitytos žinutės</string>
|
||||||
<string name="nc_new_password">Naujas slaptažodis</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_nick_guest">Svečias</string>
|
||||||
<string name="nc_no">Ne</string>
|
<string name="nc_no">Ne</string>
|
||||||
<string name="nc_no_messages_yet">Kol kas žinučių nėra</string>
|
<string name="nc_no_messages_yet">Kol kas žinučių nėra</string>
|
||||||
|
@ -140,7 +140,6 @@
|
|||||||
<string name="nc_new_mention">Uleste nevner</string>
|
<string name="nc_new_mention">Uleste nevner</string>
|
||||||
<string name="nc_new_messages">Uleste meldinger</string>
|
<string name="nc_new_messages">Uleste meldinger</string>
|
||||||
<string name="nc_new_password">Nytt passord</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_nick_guest">Gjest</string>
|
||||||
<string name="nc_no">Nei</string>
|
<string name="nc_no">Nei</string>
|
||||||
<string name="nc_no_messages_yet">Ingen meldiner enda</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_description">Synkroniser kun til betrodde servere</string>
|
||||||
<string name="scope_federated_title">Sammenknyttet</string>
|
<string name="scope_federated_title">Sammenknyttet</string>
|
||||||
<string name="scope_local_title">Lokal</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_private_title">Privat</string>
|
||||||
<string name="scope_published_description">Synkroniser til betrodde servere og den globale og offentlige adresseboken</string>
|
<string name="scope_published_description">Synkroniser til betrodde servere og den globale og offentlige adresseboken</string>
|
||||||
<string name="scope_published_title">Publisert</string>
|
<string name="scope_published_title">Publisert</string>
|
||||||
|
@ -176,7 +176,6 @@ Kies er eentje van een provider.</string>
|
|||||||
<string name="nc_new_mention">Ongelezen vermeldingen</string>
|
<string name="nc_new_mention">Ongelezen vermeldingen</string>
|
||||||
<string name="nc_new_messages">Ongelezen berichten</string>
|
<string name="nc_new_messages">Ongelezen berichten</string>
|
||||||
<string name="nc_new_password">Nieuw wachtwoord</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_nick_guest">Gast</string>
|
||||||
<string name="nc_no">Nee</string>
|
<string name="nc_no">Nee</string>
|
||||||
<string name="nc_no_messages_yet">Nog geen berichten</string>
|
<string name="nc_no_messages_yet">Nog geen berichten</string>
|
||||||
|
@ -137,7 +137,6 @@
|
|||||||
<string name="nc_new_conversation">Ny samtale</string>
|
<string name="nc_new_conversation">Ny samtale</string>
|
||||||
<string name="nc_new_messages">Meldingar er ikkje lest</string>
|
<string name="nc_new_messages">Meldingar er ikkje lest</string>
|
||||||
<string name="nc_new_password">Nytt passord</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_nick_guest">Gjest</string>
|
||||||
<string name="nc_no">Nei</string>
|
<string name="nc_no">Nei</string>
|
||||||
<string name="nc_no_messages_yet">Ingen meldingar til no</string>
|
<string name="nc_no_messages_yet">Ingen meldingar til no</string>
|
||||||
|
@ -193,7 +193,6 @@
|
|||||||
<string name="nc_new_mention">Nieprzeczytane wzmianki</string>
|
<string name="nc_new_mention">Nieprzeczytane wzmianki</string>
|
||||||
<string name="nc_new_messages">Nieprzeczytane wiadomości</string>
|
<string name="nc_new_messages">Nieprzeczytane wiadomości</string>
|
||||||
<string name="nc_new_password">Nowe hasło</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_nick_guest">Gość</string>
|
||||||
<string name="nc_no">Nie</string>
|
<string name="nc_no">Nie</string>
|
||||||
<string name="nc_no_messages_yet">Nie ma nowych wiadomości</string>
|
<string name="nc_no_messages_yet">Nie ma nowych wiadomości</string>
|
||||||
|
@ -193,7 +193,6 @@
|
|||||||
<string name="nc_new_mention">Menções não lidas</string>
|
<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_messages">Mensagens não lidas</string>
|
||||||
<string name="nc_new_password">Nova senha</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_nick_guest">Convidado</string>
|
||||||
<string name="nc_no">Não</string>
|
<string name="nc_no">Não</string>
|
||||||
<string name="nc_no_messages_yet">Sem mensagens ainda</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_sixty">60</string>
|
||||||
<string name="nc_screen_lock_timeout_thirty">30</string>
|
<string name="nc_screen_lock_timeout_thirty">30</string>
|
||||||
<string name="nc_screen_lock_timeout_three_hundred">300</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_an_account">Selecionar uma conta</string>
|
||||||
<string name="nc_select_participants">Selecionar participantes</string>
|
<string name="nc_select_participants">Selecionar participantes</string>
|
||||||
<string name="nc_sent_a_gif" formatted="true">%1$s enviou um GIF.</string>
|
<string name="nc_sent_a_gif" formatted="true">%1$s enviou um GIF.</string>
|
||||||
|
@ -193,7 +193,7 @@
|
|||||||
<string name="nc_new_mention">Непрочитанные упоминания</string>
|
<string name="nc_new_mention">Непрочитанные упоминания</string>
|
||||||
<string name="nc_new_messages">Непрочитанные сообщения</string>
|
<string name="nc_new_messages">Непрочитанные сообщения</string>
|
||||||
<string name="nc_new_password">Новый пароль</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_nick_guest">Гость</string>
|
||||||
<string name="nc_no">Нет</string>
|
<string name="nc_no">Нет</string>
|
||||||
<string name="nc_no_messages_yet">Сообщений еще нет</string>
|
<string name="nc_no_messages_yet">Сообщений еще нет</string>
|
||||||
|
@ -161,7 +161,6 @@
|
|||||||
<string name="nc_new_conversation">Resonada noa</string>
|
<string name="nc_new_conversation">Resonada noa</string>
|
||||||
<string name="nc_new_messages">Messàgios non lèghidos</string>
|
<string name="nc_new_messages">Messàgios non lèghidos</string>
|
||||||
<string name="nc_new_password">Crae noa</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_nick_guest">Persone invitada</string>
|
||||||
<string name="nc_no">No</string>
|
<string name="nc_no">No</string>
|
||||||
<string name="nc_no_messages_yet">Ancora perunu messàgiu</string>
|
<string name="nc_no_messages_yet">Ancora perunu messàgiu</string>
|
||||||
|
@ -193,7 +193,6 @@
|
|||||||
<string name="nc_new_mention">Neprečítané upozornenia</string>
|
<string name="nc_new_mention">Neprečítané upozornenia</string>
|
||||||
<string name="nc_new_messages">Neprečítané správy</string>
|
<string name="nc_new_messages">Neprečítané správy</string>
|
||||||
<string name="nc_new_password">Nové heslo</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_nick_guest">Hosť</string>
|
||||||
<string name="nc_no">Nie</string>
|
<string name="nc_no">Nie</string>
|
||||||
<string name="nc_no_messages_yet">Ešte žiadne správy</string>
|
<string name="nc_no_messages_yet">Ešte žiadne správy</string>
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
<string name="audio_output_wired_headset">Ožičene slušalke</string>
|
<string name="audio_output_wired_headset">Ožičene slušalke</string>
|
||||||
<string name="avatar">Podoba</string>
|
<string name="avatar">Podoba</string>
|
||||||
<string name="away">Ne spremljam</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="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">Počisti sporočilo stanja</string>
|
||||||
<string name="clear_status_message_after">Počisti sporočilo stanja po</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_mention">Neprebrane omembe</string>
|
||||||
<string name="nc_new_messages">Neprebrana sporočila</string>
|
<string name="nc_new_messages">Neprebrana sporočila</string>
|
||||||
<string name="nc_new_password">Novo geslo</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_nick_guest">Gost</string>
|
||||||
<string name="nc_no">Ne</string>
|
<string name="nc_no">Ne</string>
|
||||||
<string name="nc_no_messages_yet">Ni še sporočil</string>
|
<string name="nc_no_messages_yet">Ni še sporočil</string>
|
||||||
|
@ -121,7 +121,6 @@
|
|||||||
<string name="nc_new_conversation">Нови разговор</string>
|
<string name="nc_new_conversation">Нови разговор</string>
|
||||||
<string name="nc_new_messages">Непрочитане поруке</string>
|
<string name="nc_new_messages">Непрочитане поруке</string>
|
||||||
<string name="nc_new_password">Нова лозинка</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_nick_guest">Гост</string>
|
||||||
<string name="nc_no">Не</string>
|
<string name="nc_no">Не</string>
|
||||||
<string name="nc_no_messages_yet">Још нема порука</string>
|
<string name="nc_no_messages_yet">Још нема порука</string>
|
||||||
|
@ -141,7 +141,6 @@
|
|||||||
<string name="nc_new_conversation">Ny konversation</string>
|
<string name="nc_new_conversation">Ny konversation</string>
|
||||||
<string name="nc_new_messages">Olästa meddelanden</string>
|
<string name="nc_new_messages">Olästa meddelanden</string>
|
||||||
<string name="nc_new_password">Nytt lösenord</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_nick_guest">Gäst</string>
|
||||||
<string name="nc_no">Nej</string>
|
<string name="nc_no">Nej</string>
|
||||||
<string name="nc_no_messages_yet">Inga meddelanden än</string>
|
<string name="nc_no_messages_yet">Inga meddelanden än</string>
|
||||||
@ -284,6 +283,7 @@
|
|||||||
<string name="online_status">Online-status</string>
|
<string name="online_status">Online-status</string>
|
||||||
<string name="polls_add_option">Lägg till alternativ</string>
|
<string name="polls_add_option">Lägg till alternativ</string>
|
||||||
<string name="polls_options">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_results_subtitle">Resultat</string>
|
||||||
<string name="polls_settings">Inställningar</string>
|
<string name="polls_settings">Inställningar</string>
|
||||||
<string name="polls_submit_vote">Rösta</string>
|
<string name="polls_submit_vote">Rösta</string>
|
||||||
|
@ -187,13 +187,14 @@
|
|||||||
<string name="nc_message_read">İleti okundu</string>
|
<string name="nc_message_read">İleti okundu</string>
|
||||||
<string name="nc_message_sent">İleti gönderildi</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_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_moderator">Sorumlu</string>
|
||||||
<string name="nc_never">Hiç katılmadı</string>
|
<string name="nc_never">Hiç katılmadı</string>
|
||||||
<string name="nc_new_conversation">Yeni görüşme</string>
|
<string name="nc_new_conversation">Yeni görüşme</string>
|
||||||
<string name="nc_new_mention">Okunmamış anmalar</string>
|
<string name="nc_new_mention">Okunmamış anmalar</string>
|
||||||
<string name="nc_new_messages">Okunmamış iletiler</string>
|
<string name="nc_new_messages">Okunmamış iletiler</string>
|
||||||
<string name="nc_new_password">Yeni parola</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_nick_guest">Konuk</string>
|
||||||
<string name="nc_no">Hayır</string>
|
<string name="nc_no">Hayır</string>
|
||||||
<string name="nc_no_messages_yet">Henüz bir ileti yok</string>
|
<string name="nc_no_messages_yet">Henüz bir ileti yok</string>
|
||||||
|
@ -166,7 +166,6 @@
|
|||||||
<string name="nc_new_mention">Непрочитані згадки</string>
|
<string name="nc_new_mention">Непрочитані згадки</string>
|
||||||
<string name="nc_new_messages">Непрочитані повідомлення</string>
|
<string name="nc_new_messages">Непрочитані повідомлення</string>
|
||||||
<string name="nc_new_password">Новий пароль</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_nick_guest">Гість</string>
|
||||||
<string name="nc_no">Ні</string>
|
<string name="nc_no">Ні</string>
|
||||||
<string name="nc_no_messages_yet">Повідомлень немає</string>
|
<string name="nc_no_messages_yet">Повідомлень немає</string>
|
||||||
@ -289,7 +288,7 @@
|
|||||||
<string name="nc_share_to_choose_account">Оберіть обліковий запис</string>
|
<string name="nc_share_to_choose_account">Оберіть обліковий запис</string>
|
||||||
<string name="nc_shared_items_location">Місце</string>
|
<string name="nc_shared_items_location">Місце</string>
|
||||||
<string name="nc_shared_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_choose_local_files">Виберіть файли</string>
|
||||||
<string name="nc_upload_failed">Вибачте, помилка завантаження</string>
|
<string name="nc_upload_failed">Вибачте, помилка завантаження</string>
|
||||||
<string name="nc_upload_from_device">Завантажити з пристрою</string>
|
<string name="nc_upload_from_device">Завантажити з пристрою</string>
|
||||||
@ -319,7 +318,7 @@
|
|||||||
<string name="selected_list_item">Selected</string>
|
<string name="selected_list_item">Selected</string>
|
||||||
<string name="set_status">Встановити статус</string>
|
<string name="set_status">Встановити статус</string>
|
||||||
<string name="set_status_message">Встановити повідомлення про стан</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_audio">Аудіо</string>
|
||||||
<string name="shared_items_file">Файл</string>
|
<string name="shared_items_file">Файл</string>
|
||||||
<string name="shared_items_media">Зображення та відео</string>
|
<string name="shared_items_media">Зображення та відео</string>
|
||||||
|
@ -126,7 +126,6 @@
|
|||||||
<string name="nc_new_conversation">Tạo đàm thoại mới</string>
|
<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_messages">Tin nhắn chưa đọc</string>
|
||||||
<string name="nc_new_password">Mật khẩu mới</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_nick_guest">Khách</string>
|
||||||
<string name="nc_no">Không</string>
|
<string name="nc_no">Không</string>
|
||||||
<string name="nc_no_messages_yet">Chưa có thông điệp nào</string>
|
<string name="nc_no_messages_yet">Chưa có thông điệp nào</string>
|
||||||
|
@ -51,6 +51,7 @@
|
|||||||
<string name="nc_call_name">会话名称</string>
|
<string name="nc_call_name">会话名称</string>
|
||||||
<string name="nc_call_name_is_same">您输入的名称已经存在</string>
|
<string name="nc_call_name_is_same">您输入的名称已经存在</string>
|
||||||
<string name="nc_call_notifications">呼叫通知</string>
|
<string name="nc_call_notifications">呼叫通知</string>
|
||||||
|
<string name="nc_call_reconnecting">正在重新连接 ...</string>
|
||||||
<string name="nc_call_ringing">响铃</string>
|
<string name="nc_call_ringing">响铃</string>
|
||||||
<string name="nc_call_state_in_call">%1$s 在通话中</string>
|
<string name="nc_call_state_in_call">%1$s 在通话中</string>
|
||||||
<string name="nc_call_state_with_phone">%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_day">1 天</string>
|
||||||
<string name="nc_expire_message_one_hour">1 小时</string>
|
<string name="nc_expire_message_one_hour">1 小时</string>
|
||||||
<string name="nc_expire_message_one_week">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_external_server_failed">获取网络设置失败</string>
|
||||||
<string name="nc_failed_signaling_settings">目标服务器不支持通过移动电话加入公共对话。你可以尝试通过浏览器加入对话。</string>
|
<string name="nc_failed_signaling_settings">目标服务器不支持通过移动电话加入公共对话。你可以尝试通过浏览器加入对话。</string>
|
||||||
<string name="nc_failed_to_perform_operation">抱歉,有地方出错了!</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_password_weak_alert_title">弱密码</string>
|
||||||
<string name="nc_guest_access_resend_invitations">重发邀请</string>
|
<string name="nc_guest_access_resend_invitations">重发邀请</string>
|
||||||
<string name="nc_guest_access_share_link">共享会话链接</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">重要会话</string>
|
||||||
<string name="nc_important_conversation_desc">此会话中的通知将覆盖免打扰设置</string>
|
<string name="nc_important_conversation_desc">此会话中的通知将覆盖免打扰设置</string>
|
||||||
<string name="nc_join_via_link">使用链接加入</string>
|
<string name="nc_join_via_link">使用链接加入</string>
|
||||||
@ -140,6 +143,7 @@
|
|||||||
<string name="nc_last_moderator_title">无法离开会话</string>
|
<string name="nc_last_moderator_title">无法离开会话</string>
|
||||||
<string name="nc_last_modified">%1$s 我上一次更改: %2$s</string>
|
<string name="nc_last_modified">%1$s 我上一次更改: %2$s</string>
|
||||||
<string name="nc_leave">离开会话</string>
|
<string name="nc_leave">离开会话</string>
|
||||||
|
<string name="nc_leaving_call">正在离开通话 ...</string>
|
||||||
<string name="nc_license_summary">GNU 通用公共许可证,第3版</string>
|
<string name="nc_license_summary">GNU 通用公共许可证,第3版</string>
|
||||||
<string name="nc_license_title">许可证</string>
|
<string name="nc_license_title">许可证</string>
|
||||||
<string name="nc_limit_hit">已达到%s字符限制</string>
|
<string name="nc_limit_hit">已达到%s字符限制</string>
|
||||||
@ -167,7 +171,6 @@
|
|||||||
<string name="nc_new_mention">未读的提及</string>
|
<string name="nc_new_mention">未读的提及</string>
|
||||||
<string name="nc_new_messages">未读消息</string>
|
<string name="nc_new_messages">未读消息</string>
|
||||||
<string name="nc_new_password">新密码</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_nick_guest">来宾</string>
|
||||||
<string name="nc_no">不</string>
|
<string name="nc_no">不</string>
|
||||||
<string name="nc_no_messages_yet">暂无消息</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_text_pass">\n密码: %1$s</string>
|
||||||
<string name="nc_share_this_location">分享这个位置</string>
|
<string name="nc_share_this_location">分享这个位置</string>
|
||||||
<string name="nc_share_to_choose_account">选择一个账户</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_deck_card">Deck 卡片</string>
|
||||||
|
<string name="nc_shared_items_empty">沒有已分享的项目</string>
|
||||||
<string name="nc_shared_items_location">位置</string>
|
<string name="nc_shared_items_location">位置</string>
|
||||||
<string name="nc_shared_location">共享的位置</string>
|
<string name="nc_shared_location">共享的位置</string>
|
||||||
<string name="nc_sort_by">排序依据</string>
|
<string name="nc_sort_by">排序依据</string>
|
||||||
|
@ -187,13 +187,14 @@
|
|||||||
<string name="nc_message_read">訊息已讀</string>
|
<string name="nc_message_read">訊息已讀</string>
|
||||||
<string name="nc_message_sent">訊息已傳送</string>
|
<string name="nc_message_sent">訊息已傳送</string>
|
||||||
<string name="nc_microphone_permission_permanently_denied">為了開啟聲音的通訊請在系統設定內同意\"麥克風\"的需求。</string>
|
<string name="nc_microphone_permission_permanently_denied">為了開啟聲音的通訊請在系統設定內同意\"麥克風\"的需求。</string>
|
||||||
|
<string name="nc_missed_call">您錯過了 %s 的來電</string>
|
||||||
<string name="nc_moderator">主持人</string>
|
<string name="nc_moderator">主持人</string>
|
||||||
<string name="nc_never">從未加入</string>
|
<string name="nc_never">從未加入</string>
|
||||||
<string name="nc_new_conversation">新對話</string>
|
<string name="nc_new_conversation">新對話</string>
|
||||||
<string name="nc_new_mention">未讀的提及</string>
|
<string name="nc_new_mention">未讀的提及</string>
|
||||||
<string name="nc_new_messages">未讀郵件</string>
|
<string name="nc_new_messages">未讀郵件</string>
|
||||||
<string name="nc_new_password">新密碼</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_nick_guest">訪客</string>
|
||||||
<string name="nc_no">否</string>
|
<string name="nc_no">否</string>
|
||||||
<string name="nc_no_messages_yet">目前無任何訊息</string>
|
<string name="nc_no_messages_yet">目前無任何訊息</string>
|
||||||
|
@ -175,7 +175,6 @@
|
|||||||
<string name="nc_new_mention">未讀的提及</string>
|
<string name="nc_new_mention">未讀的提及</string>
|
||||||
<string name="nc_new_messages">未讀訊息</string>
|
<string name="nc_new_messages">未讀訊息</string>
|
||||||
<string name="nc_new_password">新密碼</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_nick_guest">訪客</string>
|
||||||
<string name="nc_no">否</string>
|
<string name="nc_no">否</string>
|
||||||
<string name="nc_no_messages_yet">目前無任何訊息</string>
|
<string name="nc_no_messages_yet">目前無任何訊息</string>
|
||||||
|
@ -58,7 +58,7 @@
|
|||||||
<string name="nc_capabilities_failed">Failed to fetch capabilities, aborting</string>
|
<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_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_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_display_name_not_stored">Could not store display name, aborting</string>
|
||||||
|
|
||||||
<string name="nc_never">Never joined</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_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_phone">%1$s with phone</string>
|
||||||
<string name="nc_call_state_with_video">%1$s with video</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 -->
|
<!-- Picture in Picture -->
|
||||||
<string name="nc_pip_microphone_mute">Mute microphone</string>
|
<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_subject">%1$s invitation</string>
|
||||||
<string name="nc_share_text_pass">\nPassword: %1$s</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">Push-to-talk</string>
|
||||||
<string name="nc_push_to_talk_desc">With microphone disabled, click&hold to use Push-to-talk</string>
|
<string name="nc_push_to_talk_desc">With microphone disabled, click&hold to use Push-to-talk</string>
|
||||||
<string name="nc_configure_cert_auth">Select authentication certificate</string>
|
<string name="nc_configure_cert_auth">Select authentication certificate</string>
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
buildscript {
|
buildscript {
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
kotlinVersion = '1.7.20'
|
kotlinVersion = '1.7.21'
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
@ -45,7 +45,7 @@ abstract class DownloadWebRtcTask extends DefaultTask {
|
|||||||
|
|
||||||
private String getDownloadUrl() {
|
private String getDownloadUrl() {
|
||||||
def webRtcVersion = version.get()
|
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() {
|
private String getOutputPath() {
|
||||||
|
@ -26,3 +26,9 @@ include ':app'
|
|||||||
// substitute module('com.github.nextcloud.android-common:ui') using project(':ui')
|
// 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')
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
Loading…
Reference in New Issue
Block a user