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'
|
||||
|
||||
android {
|
||||
compileSdkVersion 31
|
||||
compileSdkVersion 32
|
||||
buildToolsVersion '33.0.0'
|
||||
|
||||
namespace 'com.nextcloud.talk'
|
||||
@ -48,8 +48,8 @@ android {
|
||||
|
||||
// mayor.minor.hotfix.increment (for increment: 01-50=Alpha / 51-89=RC / 90-99=stable)
|
||||
// xx .xxx .xx .xx
|
||||
versionCode 150010007
|
||||
versionName "15.1.0 Alpha 07"
|
||||
versionCode 150010008
|
||||
versionName "15.1.0 Alpha 08"
|
||||
|
||||
flavorDimensions "default"
|
||||
renderscriptTargetApi 19
|
||||
@ -142,7 +142,7 @@ android {
|
||||
ext {
|
||||
androidxCameraVersion = "1.1.0"
|
||||
coilKtVersion = "2.2.2"
|
||||
daggerVersion = "2.44"
|
||||
daggerVersion = "2.44.2"
|
||||
lifecycleVersion = '2.5.1'
|
||||
okhttpVersion = "4.10.0"
|
||||
materialDialogsVersion = "3.3.0"
|
||||
@ -151,10 +151,10 @@ ext {
|
||||
roomVersion = "2.4.3"
|
||||
workVersion = "2.7.1"
|
||||
markwonVersion = "4.6.2"
|
||||
espressoVersion = "3.4.0"
|
||||
espressoVersion = "3.5.0"
|
||||
}
|
||||
|
||||
def webRtcVersion = "96.4664.0"
|
||||
def webRtcVersion = "106.5249.0"
|
||||
tasks.register('downloadWebRtc', DownloadWebRtcTask){
|
||||
version = webRtcVersion
|
||||
}
|
||||
@ -175,7 +175,7 @@ dependencies {
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1"
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.5.1'
|
||||
implementation 'com.google.android.material:material:1.6.1'
|
||||
implementation 'com.google.android.material:material:1.7.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation "com.vanniktech:emoji-google:0.15.0"
|
||||
implementation group: 'androidx.emoji', name: 'emoji-bundled', version: '1.1.0'
|
||||
@ -207,14 +207,14 @@ dependencies {
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
||||
implementation "io.reactivex.rxjava2:rxjava:2.2.21"
|
||||
|
||||
implementation 'com.bluelinelabs:conductor:3.1.7'
|
||||
implementation 'com.bluelinelabs:conductor:3.1.8'
|
||||
|
||||
implementation "com.squareup.okhttp3:okhttp:${okhttpVersion}"
|
||||
implementation "com.squareup.okhttp3:okhttp-urlconnection:${okhttpVersion}"
|
||||
implementation "com.squareup.okhttp3:logging-interceptor:${okhttpVersion}"
|
||||
|
||||
implementation 'com.bluelinelabs:logansquare:1.3.7'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-core:2.13.4'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-core:2.14.0'
|
||||
kapt 'com.bluelinelabs:logansquare-compiler:1.3.7'
|
||||
|
||||
implementation "com.squareup.retrofit2:retrofit:${retrofit2Version}"
|
||||
@ -254,7 +254,7 @@ dependencies {
|
||||
implementation 'com.github.nextcloud-deps.fresco:webpsupport:v111'
|
||||
implementation 'com.github.nextcloud-deps.fresco:animated-gif:v111'
|
||||
implementation 'com.github.nextcloud-deps.fresco:imagepipeline-okhttp3:v111'
|
||||
implementation 'joda-time:joda-time:2.12.0'
|
||||
implementation 'joda-time:joda-time:2.12.1'
|
||||
implementation "io.coil-kt:coil:${coilKtVersion}"
|
||||
implementation "io.coil-kt:coil-gif:${coilKtVersion}"
|
||||
implementation "io.coil-kt:coil-svg:${coilKtVersion}"
|
||||
@ -280,8 +280,7 @@ dependencies {
|
||||
|
||||
implementation "io.noties.markwon:core:$markwonVersion"
|
||||
|
||||
//implementation 'com.github.dhaval2404:imagepicker:1.8'
|
||||
implementation 'com.github.nextcloud-deps:ImagePicker:1.8.0.2'
|
||||
implementation 'com.github.nextcloud-deps:ImagePicker:2.1.0.2'
|
||||
implementation 'com.elyeproj.libraries:loaderviewlibrary:2.0.0'
|
||||
|
||||
implementation 'org.osmdroid:osmdroid-android:6.1.14'
|
||||
@ -293,9 +292,9 @@ dependencies {
|
||||
implementation 'androidx.core:core-ktx:1.8.0'
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.mockito:mockito-core:4.8.1'
|
||||
testImplementation 'org.mockito:mockito-core:4.9.0'
|
||||
|
||||
androidTestImplementation "androidx.test:core:1.4.0"
|
||||
androidTestImplementation "androidx.test:core:1.5.0"
|
||||
|
||||
// Espresso core
|
||||
androidTestImplementation ("androidx.test.espresso:espresso-core:$espressoVersion", {
|
||||
|
@ -39,7 +39,7 @@
|
||||
<meta-data android:name="google_analytics_adid_collection_enabled" android:value="false" />
|
||||
|
||||
<service
|
||||
android:name=".services.firebase.ChatAndCallMessagingService"
|
||||
android:name=".services.firebase.NCFirebaseMessagingService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="phoneCall">
|
||||
<intent-filter>
|
||||
|
@ -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.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
@ -439,7 +440,7 @@ public class CallActivity extends CallBaseActivity {
|
||||
binding.gridview.setOnItemClickListener((parent, view, position, id) -> animateCallControls(true, 0));
|
||||
|
||||
binding.callStates.callStateRelativeLayout.setOnClickListener(l -> {
|
||||
if (currentCallStatus.equals(CallStatus.CALLING_TIMEOUT)) {
|
||||
if (currentCallStatus == CallStatus.CALLING_TIMEOUT) {
|
||||
setCallState(CallStatus.RECONNECTING);
|
||||
hangupNetworkCalls(false);
|
||||
}
|
||||
@ -746,7 +747,7 @@ public class CallActivity extends CallBaseActivity {
|
||||
}
|
||||
|
||||
private boolean isConnectionEstablished() {
|
||||
return (currentCallStatus.equals(CallStatus.JOINED) || currentCallStatus.equals(CallStatus.IN_CONVERSATION));
|
||||
return (currentCallStatus == CallStatus.JOINED || currentCallStatus == CallStatus.IN_CONVERSATION);
|
||||
}
|
||||
|
||||
@AfterPermissionGranted(100)
|
||||
@ -837,9 +838,9 @@ public class CallActivity extends CallBaseActivity {
|
||||
Log.d(TAG, "onAudioManagerDevicesChanged: " + availableDevices + ", "
|
||||
+ "currentDevice: " + currentDevice);
|
||||
|
||||
final boolean shouldDisableProximityLock = (currentDevice.equals(WebRtcAudioManager.AudioDevice.WIRED_HEADSET)
|
||||
|| currentDevice.equals(WebRtcAudioManager.AudioDevice.SPEAKER_PHONE)
|
||||
|| currentDevice.equals(WebRtcAudioManager.AudioDevice.BLUETOOTH));
|
||||
final boolean shouldDisableProximityLock = (currentDevice == WebRtcAudioManager.AudioDevice.WIRED_HEADSET
|
||||
|| currentDevice == WebRtcAudioManager.AudioDevice.SPEAKER_PHONE
|
||||
|| currentDevice == WebRtcAudioManager.AudioDevice.BLUETOOTH);
|
||||
|
||||
if (shouldDisableProximityLock) {
|
||||
powerManagerUtils.updatePhoneState(PowerManagerUtils.PhoneState.WITHOUT_PROXIMITY_SENSOR_LOCK);
|
||||
@ -1229,7 +1230,7 @@ public class CallActivity extends CallBaseActivity {
|
||||
Log.d(TAG, "localStream is null");
|
||||
}
|
||||
|
||||
if (!currentCallStatus.equals(CallStatus.LEAVING)) {
|
||||
if (currentCallStatus != CallStatus.LEAVING) {
|
||||
hangup(true);
|
||||
}
|
||||
powerManagerUtils.updatePhoneState(PowerManagerUtils.PhoneState.IDLE);
|
||||
@ -1456,7 +1457,7 @@ public class CallActivity extends CallBaseActivity {
|
||||
|
||||
@Override
|
||||
public void onNext(@io.reactivex.annotations.NonNull GenericOverall genericOverall) {
|
||||
if (!currentCallStatus.equals(CallStatus.LEAVING)) {
|
||||
if (currentCallStatus != CallStatus.LEAVING) {
|
||||
setCallState(CallStatus.JOINED);
|
||||
|
||||
ApplicationWideCurrentRoomHolder.getInstance().setInCall(true);
|
||||
@ -1472,6 +1473,8 @@ public class CallActivity extends CallBaseActivity {
|
||||
int apiVersion = ApiUtils.getSignalingApiVersion(conversationUser,
|
||||
new int[]{ApiUtils.APIv3, 2, 1});
|
||||
|
||||
AtomicInteger delayOnError = new AtomicInteger(0);
|
||||
|
||||
ncApi.pullSignalingMessages(credentials,
|
||||
ApiUtils.getUrlForSignaling(apiVersion,
|
||||
baseUrl,
|
||||
@ -1480,7 +1483,22 @@ public class CallActivity extends CallBaseActivity {
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.repeatWhen(observable -> observable)
|
||||
.takeWhile(observable -> isConnectionEstablished())
|
||||
.retry(3, observable -> isConnectionEstablished())
|
||||
.doOnNext(value -> delayOnError.set(0))
|
||||
.retryWhen(errors -> errors
|
||||
.flatMap(error -> {
|
||||
if (!isConnectionEstablished()) {
|
||||
return Observable.error(error);
|
||||
}
|
||||
|
||||
if (delayOnError.get() == 0) {
|
||||
delayOnError.set(1);
|
||||
} else if (delayOnError.get() < 16) {
|
||||
delayOnError.set(delayOnError.get() * 2);
|
||||
}
|
||||
|
||||
return Observable.timer(delayOnError.get(), TimeUnit.SECONDS);
|
||||
})
|
||||
)
|
||||
.subscribe(new Observer<SignalingOverall>() {
|
||||
@Override
|
||||
public void onSubscribe(@io.reactivex.annotations.NonNull Disposable d) {
|
||||
@ -1531,7 +1549,7 @@ public class CallActivity extends CallBaseActivity {
|
||||
conversationUser, externalSignalingServer.getExternalSignalingTicket(),
|
||||
TextUtils.isEmpty(credentials));
|
||||
} else {
|
||||
if (webSocketClient.isConnected() && currentCallStatus.equals(CallStatus.PUBLISHER_FAILED)) {
|
||||
if (webSocketClient.isConnected() && currentCallStatus == CallStatus.PUBLISHER_FAILED) {
|
||||
webSocketClient.restartWebSocket();
|
||||
}
|
||||
}
|
||||
@ -1549,7 +1567,7 @@ public class CallActivity extends CallBaseActivity {
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.BACKGROUND)
|
||||
public void onMessageEvent(WebSocketCommunicationEvent webSocketCommunicationEvent) {
|
||||
if (CallStatus.LEAVING.equals(currentCallStatus)) {
|
||||
if (currentCallStatus == CallStatus.LEAVING) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1557,7 +1575,7 @@ public class CallActivity extends CallBaseActivity {
|
||||
case "hello":
|
||||
Log.d(TAG, "onMessageEvent 'hello'");
|
||||
if (!webSocketCommunicationEvent.getHashMap().containsKey("oldResumeId")) {
|
||||
if (currentCallStatus.equals(CallStatus.RECONNECTING)) {
|
||||
if (currentCallStatus == CallStatus.RECONNECTING) {
|
||||
hangup(false);
|
||||
} else {
|
||||
initiateCall();
|
||||
@ -1642,7 +1660,7 @@ public class CallActivity extends CallBaseActivity {
|
||||
private void receivedSignalingMessage(Signaling signaling) throws IOException {
|
||||
String messageType = signaling.getType();
|
||||
|
||||
if (!isConnectionEstablished() && !currentCallStatus.equals(CallStatus.CONNECTING)) {
|
||||
if (!isConnectionEstablished() && currentCallStatus != CallStatus.CONNECTING) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1871,7 +1889,7 @@ public class CallActivity extends CallBaseActivity {
|
||||
userIdsBySessionId.put(participant.get("sessionId").toString(), userId);
|
||||
} else {
|
||||
Log.d(TAG, " inCallFlag of currentSessionId: " + inCallFlag);
|
||||
if (inCallFlag == 0 && !CallStatus.LEAVING.equals(currentCallStatus) && ApplicationWideCurrentRoomHolder.getInstance().isInCall()) {
|
||||
if (inCallFlag == 0 && currentCallStatus != CallStatus.LEAVING && ApplicationWideCurrentRoomHolder.getInstance().isInCall()) {
|
||||
Log.d(TAG, "Most probably a moderator ended the call for all.");
|
||||
hangup(true);
|
||||
}
|
||||
@ -1891,7 +1909,7 @@ public class CallActivity extends CallBaseActivity {
|
||||
// Calculate sessions that join the call
|
||||
newSessions.removeAll(oldSessions);
|
||||
|
||||
if (!isConnectionEstablished() && !currentCallStatus.equals(CallStatus.CONNECTING)) {
|
||||
if (!isConnectionEstablished() && currentCallStatus != CallStatus.CONNECTING) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1920,7 +1938,7 @@ public class CallActivity extends CallBaseActivity {
|
||||
});
|
||||
}
|
||||
|
||||
if (newSessions.size() > 0 && !currentCallStatus.equals(CallStatus.IN_CONVERSATION)) {
|
||||
if (newSessions.size() > 0 && currentCallStatus != CallStatus.IN_CONVERSATION) {
|
||||
setCallState(CallStatus.IN_CONVERSATION);
|
||||
}
|
||||
|
||||
@ -2069,8 +2087,9 @@ public class CallActivity extends CallBaseActivity {
|
||||
if (!(peerConnectionWrappers = getPeerConnectionWrapperListForSessionId(sessionId)).isEmpty()) {
|
||||
for (PeerConnectionWrapper peerConnectionWrapper : peerConnectionWrappers) {
|
||||
if (peerConnectionWrapper.getSessionId().equals(sessionId)) {
|
||||
if (VIDEO_STREAM_TYPE_SCREEN.equals(peerConnectionWrapper.getVideoStreamType()) || !justScreen) {
|
||||
runOnUiThread(() -> removeMediaStream(sessionId));
|
||||
String videoStreamType = peerConnectionWrapper.getVideoStreamType();
|
||||
if (VIDEO_STREAM_TYPE_SCREEN.equals(videoStreamType) || !justScreen) {
|
||||
runOnUiThread(() -> removeMediaStream(sessionId, videoStreamType));
|
||||
deletePeerConnection(peerConnectionWrapper);
|
||||
}
|
||||
}
|
||||
@ -2078,9 +2097,9 @@ public class CallActivity extends CallBaseActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private void removeMediaStream(String sessionId) {
|
||||
private void removeMediaStream(String sessionId, String videoStreamType) {
|
||||
Log.d(TAG, "removeMediaStream");
|
||||
participantDisplayItems.remove(sessionId);
|
||||
participantDisplayItems.remove(sessionId + "-" + videoStreamType);
|
||||
|
||||
if (!isDestroyed()) {
|
||||
initGridAdapter();
|
||||
@ -2145,21 +2164,22 @@ public class CallActivity extends CallBaseActivity {
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
public void onMessageEvent(PeerConnectionEvent peerConnectionEvent) {
|
||||
String sessionId = peerConnectionEvent.getSessionId();
|
||||
String participantDisplayItemId = sessionId + "-" + peerConnectionEvent.getVideoStreamType();
|
||||
|
||||
if (peerConnectionEvent.getPeerConnectionEventType() ==
|
||||
PeerConnectionEvent.PeerConnectionEventType.PEER_CONNECTED) {
|
||||
if (webSocketClient != null && webSocketClient.getSessionId() == sessionId) {
|
||||
if (webSocketClient != null && webSocketClient.getSessionId() != null && webSocketClient.getSessionId().equals(sessionId)) {
|
||||
updateSelfVideoViewConnected(true);
|
||||
} else if (participantDisplayItems.get(sessionId) != null) {
|
||||
participantDisplayItems.get(sessionId).setConnected(true);
|
||||
} else if (participantDisplayItems.get(participantDisplayItemId) != null) {
|
||||
participantDisplayItems.get(participantDisplayItemId).setConnected(true);
|
||||
participantsAdapter.notifyDataSetChanged();
|
||||
}
|
||||
} else if (peerConnectionEvent.getPeerConnectionEventType() ==
|
||||
PeerConnectionEvent.PeerConnectionEventType.PEER_DISCONNECTED) {
|
||||
if (webSocketClient != null && webSocketClient.getSessionId() == sessionId) {
|
||||
if (webSocketClient != null && webSocketClient.getSessionId() != null && webSocketClient.getSessionId().equals(sessionId)) {
|
||||
updateSelfVideoViewConnected(false);
|
||||
} else if (participantDisplayItems.get(sessionId) != null) {
|
||||
participantDisplayItems.get(sessionId).setConnected(false);
|
||||
} else if (participantDisplayItems.get(participantDisplayItemId) != null) {
|
||||
participantDisplayItems.get(participantDisplayItemId).setConnected(false);
|
||||
participantsAdapter.notifyDataSetChanged();
|
||||
}
|
||||
} else if (peerConnectionEvent.getPeerConnectionEventType() ==
|
||||
@ -2174,27 +2194,27 @@ public class CallActivity extends CallBaseActivity {
|
||||
boolean enableVideo = peerConnectionEvent.getPeerConnectionEventType() ==
|
||||
PeerConnectionEvent.PeerConnectionEventType.SENSOR_FAR && videoOn;
|
||||
if (EffortlessPermissions.hasPermissions(this, PERMISSIONS_CAMERA) &&
|
||||
(currentCallStatus.equals(CallStatus.CONNECTING) || isConnectionEstablished()) && videoOn
|
||||
(currentCallStatus == CallStatus.CONNECTING || isConnectionEstablished()) && videoOn
|
||||
&& enableVideo != localVideoTrack.enabled()) {
|
||||
toggleMedia(enableVideo, true);
|
||||
}
|
||||
}
|
||||
} else if (peerConnectionEvent.getPeerConnectionEventType() ==
|
||||
PeerConnectionEvent.PeerConnectionEventType.NICK_CHANGE) {
|
||||
if (participantDisplayItems.get(sessionId) != null) {
|
||||
participantDisplayItems.get(sessionId).setNick(peerConnectionEvent.getNick());
|
||||
if (participantDisplayItems.get(participantDisplayItemId) != null) {
|
||||
participantDisplayItems.get(participantDisplayItemId).setNick(peerConnectionEvent.getNick());
|
||||
participantsAdapter.notifyDataSetChanged();
|
||||
}
|
||||
} else if (peerConnectionEvent.getPeerConnectionEventType() ==
|
||||
PeerConnectionEvent.PeerConnectionEventType.VIDEO_CHANGE && !isVoiceOnlyCall) {
|
||||
if (participantDisplayItems.get(sessionId) != null) {
|
||||
participantDisplayItems.get(sessionId).setStreamEnabled(peerConnectionEvent.getChangeValue());
|
||||
if (participantDisplayItems.get(participantDisplayItemId) != null) {
|
||||
participantDisplayItems.get(participantDisplayItemId).setStreamEnabled(peerConnectionEvent.getChangeValue());
|
||||
participantsAdapter.notifyDataSetChanged();
|
||||
}
|
||||
} else if (peerConnectionEvent.getPeerConnectionEventType() ==
|
||||
PeerConnectionEvent.PeerConnectionEventType.AUDIO_CHANGE) {
|
||||
if (participantDisplayItems.get(sessionId) != null) {
|
||||
participantDisplayItems.get(sessionId).setAudioEnabled(peerConnectionEvent.getChangeValue());
|
||||
if (participantDisplayItems.get(participantDisplayItemId) != null) {
|
||||
participantDisplayItems.get(participantDisplayItemId).setAudioEnabled(peerConnectionEvent.getChangeValue());
|
||||
participantsAdapter.notifyDataSetChanged();
|
||||
}
|
||||
} else if (peerConnectionEvent.getPeerConnectionEventType() ==
|
||||
@ -2382,33 +2402,22 @@ public class CallActivity extends CallBaseActivity {
|
||||
}
|
||||
}
|
||||
|
||||
String urlForAvatar;
|
||||
if (!TextUtils.isEmpty(userId4Usage)) {
|
||||
urlForAvatar = ApiUtils.getUrlForAvatar(baseUrl,
|
||||
ParticipantDisplayItem participantDisplayItem = new ParticipantDisplayItem(baseUrl,
|
||||
userId4Usage,
|
||||
true);
|
||||
} else {
|
||||
urlForAvatar = ApiUtils.getUrlForGuestAvatar(baseUrl,
|
||||
nick,
|
||||
true);
|
||||
}
|
||||
|
||||
ParticipantDisplayItem participantDisplayItem = new ParticipantDisplayItem(userId4Usage,
|
||||
session,
|
||||
connected,
|
||||
nick,
|
||||
urlForAvatar,
|
||||
mediaStream,
|
||||
videoStreamType,
|
||||
videoStreamEnabled,
|
||||
rootEglBase);
|
||||
participantDisplayItems.put(session, participantDisplayItem);
|
||||
participantDisplayItems.put(session + "-" + videoStreamType, participantDisplayItem);
|
||||
|
||||
initGridAdapter();
|
||||
}
|
||||
|
||||
private void setCallState(CallStatus callState) {
|
||||
if (currentCallStatus == null || !currentCallStatus.equals(callState)) {
|
||||
if (currentCallStatus == null || currentCallStatus != callState) {
|
||||
currentCallStatus = callState;
|
||||
if (handler == null) {
|
||||
handler = new Handler(Looper.getMainLooper());
|
||||
|
@ -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 var showFullscreen = false
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_preview, menu)
|
||||
return true
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ class FullScreenMediaActivity : AppCompatActivity(), Player.Listener {
|
||||
private lateinit var path: String
|
||||
private lateinit var player: SimpleExoPlayer
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_preview, menu)
|
||||
return true
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ class FullScreenTextViewerActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var path: String
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_preview, menu)
|
||||
return true
|
||||
}
|
||||
|
@ -1,9 +1,14 @@
|
||||
package com.nextcloud.talk.adapters;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.nextcloud.talk.utils.ApiUtils;
|
||||
|
||||
import org.webrtc.EglBase;
|
||||
import org.webrtc.MediaStream;
|
||||
|
||||
public class ParticipantDisplayItem {
|
||||
private String baseUrl;
|
||||
private String userId;
|
||||
private String session;
|
||||
private boolean connected;
|
||||
@ -15,16 +20,18 @@ public class ParticipantDisplayItem {
|
||||
private EglBase rootEglBase;
|
||||
private boolean isAudioEnabled;
|
||||
|
||||
public ParticipantDisplayItem(String userId, String session, boolean connected, String nick, String urlForAvatar, MediaStream mediaStream, String streamType, boolean streamEnabled, EglBase rootEglBase) {
|
||||
public ParticipantDisplayItem(String baseUrl, String userId, String session, boolean connected, String nick, MediaStream mediaStream, String streamType, boolean streamEnabled, EglBase rootEglBase) {
|
||||
this.baseUrl = baseUrl;
|
||||
this.userId = userId;
|
||||
this.session = session;
|
||||
this.connected = connected;
|
||||
this.nick = nick;
|
||||
this.urlForAvatar = urlForAvatar;
|
||||
this.mediaStream = mediaStream;
|
||||
this.streamType = streamType;
|
||||
this.streamEnabled = streamEnabled;
|
||||
this.rootEglBase = rootEglBase;
|
||||
|
||||
this.updateUrlForAvatar();
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
@ -33,6 +40,8 @@ public class ParticipantDisplayItem {
|
||||
|
||||
public void setUserId(String userId) {
|
||||
this.userId = userId;
|
||||
|
||||
this.updateUrlForAvatar();
|
||||
}
|
||||
|
||||
public String getSession() {
|
||||
@ -57,14 +66,20 @@ public class ParticipantDisplayItem {
|
||||
|
||||
public void setNick(String nick) {
|
||||
this.nick = nick;
|
||||
|
||||
this.updateUrlForAvatar();
|
||||
}
|
||||
|
||||
public String getUrlForAvatar() {
|
||||
return urlForAvatar;
|
||||
}
|
||||
|
||||
public void setUrlForAvatar(String urlForAvatar) {
|
||||
this.urlForAvatar = urlForAvatar;
|
||||
private void updateUrlForAvatar() {
|
||||
if (!TextUtils.isEmpty(userId)) {
|
||||
urlForAvatar = ApiUtils.getUrlForAvatar(baseUrl, userId, true);
|
||||
} else {
|
||||
urlForAvatar = ApiUtils.getUrlForGuestAvatar(baseUrl, nick, true);
|
||||
}
|
||||
}
|
||||
|
||||
public MediaStream getMediaStream() {
|
||||
|
@ -145,8 +145,8 @@ public class ContactItem extends AbstractFlexibleItem<ContactItem.ContactItemVie
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(participant.getDisplayName()) &&
|
||||
(participant.getType().equals(Participant.ParticipantType.GUEST) ||
|
||||
participant.getType().equals(Participant.ParticipantType.USER_FOLLOWING_LINK))) {
|
||||
(participant.getType() == Participant.ParticipantType.GUEST ||
|
||||
participant.getType() == Participant.ParticipantType.USER_FOLLOWING_LINK)) {
|
||||
holder.binding.nameText.setText(NextcloudTalkApplication
|
||||
.Companion
|
||||
.getSharedApplication()
|
||||
@ -167,8 +167,8 @@ public class ContactItem extends AbstractFlexibleItem<ContactItem.ContactItemVie
|
||||
|
||||
} else if (
|
||||
participant.getCalculatedActorType() == Participant.ActorType.GUESTS ||
|
||||
Participant.ParticipantType.GUEST.equals(participant.getType()) ||
|
||||
Participant.ParticipantType.GUEST_MODERATOR.equals(participant.getType())) {
|
||||
participant.getType() == Participant.ParticipantType.GUEST ||
|
||||
participant.getType() == Participant.ParticipantType.GUEST_MODERATOR) {
|
||||
|
||||
String displayName;
|
||||
|
||||
|
@ -274,7 +274,7 @@ public class ConversationItem extends AbstractFlexibleItem<ConversationItem.Conv
|
||||
}
|
||||
}
|
||||
|
||||
if (Conversation.ConversationType.ROOM_SYSTEM.equals(conversation.getType())) {
|
||||
if (conversation.getType() == Conversation.ConversationType.ROOM_SYSTEM) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
|
||||
Drawable[] layers = new Drawable[2];
|
||||
|
@ -140,8 +140,8 @@ public class ParticipantItem extends AbstractFlexibleItem<ParticipantItem.Partic
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(participant.getDisplayName()) &&
|
||||
(participant.getType().equals(Participant.ParticipantType.GUEST) ||
|
||||
participant.getType().equals(Participant.ParticipantType.USER_FOLLOWING_LINK))) {
|
||||
(participant.getType() == Participant.ParticipantType.GUEST ||
|
||||
participant.getType() == Participant.ParticipantType.USER_FOLLOWING_LINK)) {
|
||||
holder.binding.nameText.setText(NextcloudTalkApplication
|
||||
.Companion
|
||||
.getSharedApplication()
|
||||
@ -170,8 +170,8 @@ public class ParticipantItem extends AbstractFlexibleItem<ParticipantItem.Partic
|
||||
holder.binding.avatarDraweeView.setImageResource(R.drawable.ic_circular_mail);
|
||||
}
|
||||
} else if (participant.getCalculatedActorType() == Participant.ActorType.GUESTS ||
|
||||
Participant.ParticipantType.GUEST.equals(participant.getType()) ||
|
||||
Participant.ParticipantType.GUEST_MODERATOR.equals(participant.getType())) {
|
||||
participant.getType() == Participant.ParticipantType.GUEST ||
|
||||
participant.getType() == Participant.ParticipantType.GUEST_MODERATOR) {
|
||||
|
||||
String displayName = NextcloudTalkApplication.Companion.getSharedApplication()
|
||||
.getResources().getString(R.string.nc_guest);
|
||||
|
@ -40,13 +40,14 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import autodagger.AutoInjector
|
||||
import com.github.dhaval2404.imagepicker.ImagePicker
|
||||
import com.github.dhaval2404.imagepicker.ImagePicker.Companion.getError
|
||||
import com.github.dhaval2404.imagepicker.ImagePicker.Companion.getFile
|
||||
import com.github.dhaval2404.imagepicker.ImagePicker.Companion.with
|
||||
import com.github.dhaval2404.imagepicker.constant.ImageProvider
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.activities.TakePhotoActivity
|
||||
import com.nextcloud.talk.api.NcApi
|
||||
@ -486,14 +487,13 @@ class ProfileController : BaseController(R.layout.controller_profile) {
|
||||
}
|
||||
|
||||
private fun sendSelectLocalFileIntent() {
|
||||
val intent = with(activity!!)
|
||||
.galleryOnly()
|
||||
with(activity!!)
|
||||
.provider(ImageProvider.GALLERY)
|
||||
.crop()
|
||||
.cropSquare()
|
||||
.compress(MAX_SIZE)
|
||||
.maxResultSize(MAX_SIZE, MAX_SIZE)
|
||||
.prepareIntent()
|
||||
startActivityForResult(intent, 1)
|
||||
.createIntent { intent -> startActivityForResult(intent, REQUEST_CODE_IMAGE_PICKER) }
|
||||
}
|
||||
|
||||
private fun showBrowserScreen() {
|
||||
@ -584,21 +584,21 @@ class ProfileController : BaseController(R.layout.controller_profile) {
|
||||
}
|
||||
|
||||
private fun openImageWithPicker(file: File) {
|
||||
val intent = with(activity!!)
|
||||
.fileOnly()
|
||||
with(activity!!)
|
||||
.provider(ImageProvider.URI)
|
||||
.crop()
|
||||
.cropSquare()
|
||||
.compress(MAX_SIZE)
|
||||
.maxResultSize(MAX_SIZE, MAX_SIZE)
|
||||
.prepareIntent()
|
||||
intent.putExtra("extra.file", file)
|
||||
startActivityForResult(intent, REQUEST_CODE_IMAGE_PICKER)
|
||||
.setUri(Uri.fromFile(file))
|
||||
.createIntent { intent -> startActivityForResult(intent, REQUEST_CODE_IMAGE_PICKER) }
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
if (requestCode == REQUEST_CODE_IMAGE_PICKER) {
|
||||
uploadAvatar(getFile(data))
|
||||
val uri: Uri = data?.data!!
|
||||
uploadAvatar(uri.toFile())
|
||||
} else if (requestCode == REQUEST_CODE_SELECT_REMOTE_FILES) {
|
||||
val pathList = data?.getStringArrayListExtra(RemoteFileBrowserActivity.EXTRA_SELECTED_PATHS)
|
||||
if (pathList?.size!! >= 1) {
|
||||
|
@ -299,7 +299,7 @@ public class RestModule {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (Proxy.Type.SOCKS.equals(Proxy.Type.valueOf(appPreferences.getProxyType()))) {
|
||||
if (Proxy.Type.valueOf(appPreferences.getProxyType()) == Proxy.Type.SOCKS) {
|
||||
proxy = new Proxy(Proxy.Type.valueOf(appPreferences.getProxyType()),
|
||||
InetSocketAddress.createUnresolved(appPreferences.getProxyHost(), Integer.parseInt(
|
||||
appPreferences.getProxyPort())));
|
||||
|
@ -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()
|
||||
|
||||
uploadSuccess = ChunkedFileUploader(
|
||||
okHttpClient!!,
|
||||
okHttpClient,
|
||||
currentUser,
|
||||
roomToken,
|
||||
metaData,
|
||||
|
@ -210,13 +210,13 @@ class MessageSearchActivity : BaseActivity() {
|
||||
binding.emptyContainer.emptyListView.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_search, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||
val menuItem = menu!!.findItem(R.id.action_search)
|
||||
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
||||
val menuItem = menu.findItem(R.id.action_search)
|
||||
searchView = menuItem.actionView as SearchView
|
||||
setupSearchView()
|
||||
menuItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||
|
@ -56,7 +56,7 @@ data class Notification(
|
||||
@JsonField(name = ["messageRich"])
|
||||
var messageRich: String?,
|
||||
@JsonField(name = ["messageRichParameters"])
|
||||
var messageRichParameters: HashMap<String, HashMap<String, String>>?,
|
||||
var messageRichParameters: HashMap<String?, HashMap<String?, String?>>?,
|
||||
@JsonField(name = ["link"])
|
||||
var link: String?,
|
||||
@JsonField(name = ["actions"])
|
||||
|
@ -24,6 +24,8 @@ package com.nextcloud.talk.models.json.notifications
|
||||
import com.bluelinelabs.logansquare.annotation.JsonField
|
||||
import com.bluelinelabs.logansquare.annotation.JsonObject
|
||||
|
||||
// see https://github.com/nextcloud/notifications/blob/master/docs/ocs-endpoint-v2.md
|
||||
|
||||
@JsonObject
|
||||
data class NotificationOverall(
|
||||
@JsonField(name = ["ocs"])
|
||||
|
@ -70,6 +70,7 @@ data class DecryptedPushMessage(
|
||||
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
|
||||
constructor() : this(null, null, "", null, 0, null, false, false, false, null, null, 0, null)
|
||||
|
||||
@Suppress("Detekt.ComplexMethod")
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
@ -180,7 +180,7 @@ class RemoteFileBrowserActivity : AppCompatActivity(), SelectionInterface, Swipe
|
||||
showList()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
menuInflater.inflate(R.menu.menu_share_files, menu)
|
||||
filesSelectionDoneMenuItem = menu?.findItem(R.id.files_selection_done)
|
||||
|
@ -391,6 +391,7 @@ public class ApiUtils {
|
||||
getApplicationContext().getResources().getString(R.string.nc_push_server_url) + "/devices";
|
||||
}
|
||||
|
||||
// see https://github.com/nextcloud/notifications/blob/master/docs/ocs-endpoint-v2.md
|
||||
public static String getUrlForNotificationWithId(String baseUrl, String notificationId) {
|
||||
return baseUrl + ocsApiVersion + "/apps/notifications/api/v2/notifications/" + notificationId;
|
||||
}
|
||||
|
@ -47,6 +47,7 @@ import com.nextcloud.talk.utils.bundle.BundleKeys
|
||||
import com.nextcloud.talk.utils.preferences.AppPreferences
|
||||
import java.io.IOException
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
object NotificationUtils {
|
||||
|
||||
enum class NotificationChannels {
|
||||
@ -241,7 +242,7 @@ object NotificationUtils {
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelExistingNotificationWithId(context: Context?, conversationUser: User, notificationId: Long?) {
|
||||
fun cancelNotification(context: Context?, conversationUser: User, notificationId: Long?) {
|
||||
scanNotifications(context, conversationUser) { notificationManager, statusBarNotification, notification ->
|
||||
if (notificationId == notification.extras.getLong(BundleKeys.KEY_NOTIFICATION_ID)) {
|
||||
notificationManager.cancel(statusBarNotification.id)
|
||||
|
@ -467,7 +467,7 @@ public class MagicWebSocketInstance extends WebSocketListener {
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.BACKGROUND)
|
||||
public void onMessageEvent(NetworkEvent networkEvent) {
|
||||
if (networkEvent.getNetworkConnectionEvent().equals(NetworkEvent.NetworkConnectionEvent.NETWORK_CONNECTED) && !isConnected()) {
|
||||
if (networkEvent.getNetworkConnectionEvent() == NetworkEvent.NetworkConnectionEvent.NETWORK_CONNECTED && !isConnected()) {
|
||||
restartWebSocket();
|
||||
}
|
||||
}
|
||||
|
@ -276,7 +276,7 @@ public class PeerConnectionWrapper {
|
||||
|
||||
@Override
|
||||
public void onStateChange() {
|
||||
if (dataChannel != null && dataChannel.state().equals(DataChannel.State.OPEN) &&
|
||||
if (dataChannel != null && dataChannel.state() == DataChannel.State.OPEN &&
|
||||
dataChannel.label().equals("status")) {
|
||||
sendInitialMediaStatus();
|
||||
}
|
||||
@ -343,9 +343,9 @@ public class PeerConnectionWrapper {
|
||||
public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
|
||||
|
||||
Log.d("iceConnectionChangeTo: ", iceConnectionState.name() + " over " + peerConnection.hashCode() + " " + sessionId);
|
||||
if (iceConnectionState.equals(PeerConnection.IceConnectionState.CONNECTED)) {
|
||||
if (iceConnectionState == PeerConnection.IceConnectionState.CONNECTED) {
|
||||
EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType.PEER_CONNECTED,
|
||||
sessionId, null, null, null));
|
||||
sessionId, null, null, videoStreamType));
|
||||
|
||||
if (!isMCUPublisher) {
|
||||
EventBus.getDefault().post(new MediaStreamEvent(remoteStream, sessionId, videoStreamType));
|
||||
@ -354,22 +354,22 @@ public class PeerConnectionWrapper {
|
||||
if (hasInitiated) {
|
||||
sendInitialMediaStatus();
|
||||
}
|
||||
} else if (iceConnectionState.equals(PeerConnection.IceConnectionState.COMPLETED)) {
|
||||
} else if (iceConnectionState == PeerConnection.IceConnectionState.COMPLETED) {
|
||||
EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType.PEER_CONNECTED,
|
||||
sessionId, null, null, null));
|
||||
} else if (iceConnectionState.equals(PeerConnection.IceConnectionState.CLOSED)) {
|
||||
sessionId, null, null, videoStreamType));
|
||||
} else if (iceConnectionState == PeerConnection.IceConnectionState.CLOSED) {
|
||||
EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType
|
||||
.PEER_CLOSED, sessionId, null, null, videoStreamType));
|
||||
} else if (iceConnectionState.equals(PeerConnection.IceConnectionState.DISCONNECTED) ||
|
||||
iceConnectionState.equals(PeerConnection.IceConnectionState.NEW) ||
|
||||
iceConnectionState.equals(PeerConnection.IceConnectionState.CHECKING)) {
|
||||
} else if (iceConnectionState == PeerConnection.IceConnectionState.DISCONNECTED ||
|
||||
iceConnectionState == PeerConnection.IceConnectionState.NEW ||
|
||||
iceConnectionState == PeerConnection.IceConnectionState.CHECKING) {
|
||||
EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType.PEER_DISCONNECTED,
|
||||
sessionId, null, null, null));
|
||||
} else if (iceConnectionState.equals(PeerConnection.IceConnectionState.FAILED)) {
|
||||
sessionId, null, null, videoStreamType));
|
||||
} else if (iceConnectionState == PeerConnection.IceConnectionState.FAILED) {
|
||||
EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType.PEER_DISCONNECTED,
|
||||
sessionId, null, null, null));
|
||||
sessionId, null, null, videoStreamType));
|
||||
if (isMCUPublisher) {
|
||||
EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType.PUBLISHER_FAILED, sessionId, null, null, null));
|
||||
EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType.PUBLISHER_FAILED, sessionId, null, null, videoStreamType));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -469,7 +469,7 @@ public class PeerConnectionWrapper {
|
||||
|
||||
if (shouldNotReceiveVideo()) {
|
||||
for (RtpTransceiver t : peerConnection.getTransceivers()) {
|
||||
if (t.getMediaType().equals(MediaStreamTrack.MediaType.MEDIA_TYPE_VIDEO)) {
|
||||
if (t.getMediaType() == MediaStreamTrack.MediaType.MEDIA_TYPE_VIDEO) {
|
||||
t.stop();
|
||||
}
|
||||
}
|
||||
|
@ -129,7 +129,7 @@ public class WebRtcAudioManager {
|
||||
return;
|
||||
}
|
||||
|
||||
if (userSelectedAudioDevice.equals(AudioDevice.SPEAKER_PHONE)
|
||||
if (userSelectedAudioDevice == AudioDevice.SPEAKER_PHONE
|
||||
&& audioDevices.contains(AudioDevice.EARPIECE)
|
||||
&& audioDevices.contains(AudioDevice.SPEAKER_PHONE)) {
|
||||
|
||||
|
@ -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_messages">رسائل غير مقروءة</string>
|
||||
<string name="nc_new_password">كلمة سرية جديدة</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">تطبيق %1$s غير مثبت على الخادم، جارِ الإلغاء</string>
|
||||
<string name="nc_nick_guest">ضيف</string>
|
||||
<string name="nc_no">لا</string>
|
||||
<string name="nc_no_messages_yet">ليس لديك رسائل بعد</string>
|
||||
|
@ -118,7 +118,6 @@
|
||||
<string name="nc_never">Never joined</string>
|
||||
<string name="nc_new_conversation">New conversation</string>
|
||||
<string name="nc_new_password">New password</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">%1$s app not installed on the server, aborting</string>
|
||||
<string name="nc_nick_guest">Guest</string>
|
||||
<string name="nc_no">No</string>
|
||||
<string name="nc_no_messages_yet">No messages yet</string>
|
||||
|
@ -187,13 +187,13 @@
|
||||
<string name="nc_message_read">Съобщението е прочетено</string>
|
||||
<string name="nc_message_sent">Съобщението е изпратено</string>
|
||||
<string name="nc_microphone_permission_permanently_denied">За активиране на гласова комуникация, моля, дайте право на „Микрофон“ в системните настройки.</string>
|
||||
<string name="nc_missed_call">Пропуснахте обаждане от %s</string>
|
||||
<string name="nc_moderator">Модератор</string>
|
||||
<string name="nc_never">Никога не е присъединяван</string>
|
||||
<string name="nc_new_conversation">Нов разговор</string>
|
||||
<string name="nc_new_mention">Непрочетени споменавания</string>
|
||||
<string name="nc_new_messages">Непрочетени съобщения.</string>
|
||||
<string name="nc_new_password">Нова парола</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">Приложението %1$s не е инсталирано на сървъра, прекратяване</string>
|
||||
<string name="nc_nick_guest">Гост</string>
|
||||
<string name="nc_no">Не</string>
|
||||
<string name="nc_no_messages_yet">Няма съобщения.</string>
|
||||
|
@ -4,6 +4,8 @@
|
||||
<string name="appbar_search_in">Cerca a %s</string>
|
||||
<string name="audio_output_dialog_headline">Sortida d\'àudio</string>
|
||||
<string name="audio_output_phone">Telèfon</string>
|
||||
<string name="audio_output_speaker">Altaveu</string>
|
||||
<string name="audio_output_wired_headset">Auriculars per cable</string>
|
||||
<string name="avatar">Avatar</string>
|
||||
<string name="away">Absent</string>
|
||||
<string name="clear_status_message">Esborrar el missatge d\'estat</string>
|
||||
@ -28,12 +30,15 @@
|
||||
<string name="menu_item_sort_by_size_biggest_first">Més gran primer</string>
|
||||
<string name="menu_item_sort_by_size_smallest_first">Més petit primer</string>
|
||||
<string name="message_search_begin_empty">No s\'han trobat resultats</string>
|
||||
<string name="message_search_begin_typing">Comença a escriure per cercar ...</string>
|
||||
<string name="message_search_hint">Cerca ...</string>
|
||||
<string name="messages">Missatges</string>
|
||||
<string name="nc_Server_account_imported">El compte que heu seleccionat s\'ha importat i ja és disponible</string>
|
||||
<string name="nc_about">Quant a</string>
|
||||
<string name="nc_account_chooser_active_user">Usuari actiu</string>
|
||||
<string name="nc_account_chooser_add_account">Afegeix un compte</string>
|
||||
<string name="nc_account_scheduled_for_deletion">El compte està planificat per ser suprimit i no es pot canviar</string>
|
||||
<string name="nc_action_open_main_menu">Obre el menú principal</string>
|
||||
<string name="nc_add_attachment">Afegeix un adjunt</string>
|
||||
<string name="nc_add_emojis">Afegeix emoji</string>
|
||||
<string name="nc_add_participants">Afegeix participants</string>
|
||||
@ -62,6 +67,7 @@
|
||||
<string name="nc_common_set">Estableix</string>
|
||||
<string name="nc_common_skip">Omet</string>
|
||||
<string name="nc_configure_cert_auth">Selecciona el certificat d’autenticació</string>
|
||||
<string name="nc_connecting_call">S\'està connectant …</string>
|
||||
<string name="nc_contacts_done">Fet</string>
|
||||
<string name="nc_conversation_link">Enllaç de conversa</string>
|
||||
<string name="nc_conversation_menu_conversation_info">Informació de la conversa</string>
|
||||
@ -85,6 +91,8 @@
|
||||
<string name="nc_display_name_not_fetched">No 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_email">Correu</string>
|
||||
<string name="nc_expire_message_eight_hours">8 hores</string>
|
||||
<string name="nc_expire_message_four_weeks">4 setmanes</string>
|
||||
<string name="nc_expire_message_off">Desactivada</string>
|
||||
<string name="nc_expire_message_one_day">1 dia</string>
|
||||
<string name="nc_expire_message_one_hour">1 hora</string>
|
||||
@ -105,6 +113,7 @@
|
||||
<string name="nc_guest_access_password_dialog_hint">Introduïu una contrasenya</string>
|
||||
<string name="nc_guest_access_password_title">Protecció amb contrasenya</string>
|
||||
<string name="nc_guest_access_password_weak_alert_title">Contrasenya feble</string>
|
||||
<string name="nc_hint_enter_a_message">Introduïu un missatge …</string>
|
||||
<string name="nc_important_conversation">Conversa important</string>
|
||||
<string name="nc_important_conversation_desc">Les notificacions d\'aquesta conversa anul·laran els paràmetres de no destorbar.</string>
|
||||
<string name="nc_join_via_link">Uniu-vos amb un enllaç</string>
|
||||
@ -118,6 +127,7 @@
|
||||
<string name="nc_limit_hit">S\'ha arribat al límit de %s caràcters </string>
|
||||
<string name="nc_lobby">Vestíbul</string>
|
||||
<string name="nc_lobby_waiting">Esteu esperant al vestíbul.</string>
|
||||
<string name="nc_location_current_position_description">La vostra ubicació actual</string>
|
||||
<string name="nc_locked_tap_to_unlock">Toca per desblocar</string>
|
||||
<string name="nc_make_call_private">Fes que la conversa sigui privada</string>
|
||||
<string name="nc_make_call_public">Fes que la conversa sigui pública</string>
|
||||
@ -132,7 +142,6 @@
|
||||
<string name="nc_new_conversation">Nova conversa</string>
|
||||
<string name="nc_new_messages">Missatges sense llegir</string>
|
||||
<string name="nc_new_password">Nova contrasenya</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">L\'aplicació %1$s no està instal·lada al servidor, s\'està avortant</string>
|
||||
<string name="nc_nick_guest">Convidat</string>
|
||||
<string name="nc_no">No</string>
|
||||
<string name="nc_no_messages_yet">No hi ha cap missatge encara</string>
|
||||
|
@ -187,13 +187,14 @@
|
||||
<string name="nc_message_read">Zpráva přečtena</string>
|
||||
<string name="nc_message_sent">Zpráva odeslána</string>
|
||||
<string name="nc_microphone_permission_permanently_denied">Pro zapnutí hlasové komunikace udělte v nastavení systému oprávnění „Mikrofon“ .</string>
|
||||
<string name="nc_missed_call">Zmeškali jste hovor od %s</string>
|
||||
<string name="nc_moderator">Moderátor</string>
|
||||
<string name="nc_never">Nikdy nepřipojeno</string>
|
||||
<string name="nc_new_conversation">Nová konverzace</string>
|
||||
<string name="nc_new_mention">Nepřečtená zmínění</string>
|
||||
<string name="nc_new_messages">Nepřečtené zprávy</string>
|
||||
<string name="nc_new_password">Nové heslo</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">Aplikace %1$s není na serveru nainstalována, přerušuje se</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">%1$s není k dispozici (nenainstalováno nebo administrativně omezen přístup k)</string>
|
||||
<string name="nc_nick_guest">Host</string>
|
||||
<string name="nc_no">Ne</string>
|
||||
<string name="nc_no_messages_yet">Zatím žádné zprávy</string>
|
||||
|
@ -129,7 +129,6 @@
|
||||
<string name="nc_new_conversation">Ny samtale</string>
|
||||
<string name="nc_new_messages">Ulæste beskedder</string>
|
||||
<string name="nc_new_password">Ny adgangskode</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">%1$s appen er ikke installeret på serveren, afbryder</string>
|
||||
<string name="nc_nick_guest">Gæst</string>
|
||||
<string name="nc_no">Nej</string>
|
||||
<string name="nc_no_messages_yet">Ingen beskeder endnu</string>
|
||||
|
@ -187,13 +187,14 @@
|
||||
<string name="nc_message_read">Nachricht gelesen</string>
|
||||
<string name="nc_message_sent">Nachricht gesendet</string>
|
||||
<string name="nc_microphone_permission_permanently_denied">Um Audioanrufe zu ermöglichen, gewähren Sie die Berechtigung für das \"Mikrofon\" in den Systemeinstellungen</string>
|
||||
<string name="nc_missed_call">Sie haben einen Anruf von %s verpasst</string>
|
||||
<string name="nc_moderator">Moderator</string>
|
||||
<string name="nc_never">Nie beigetreten</string>
|
||||
<string name="nc_new_conversation">Neue Unterhaltung</string>
|
||||
<string name="nc_new_mention">Ungelesene Erwähnungen</string>
|
||||
<string name="nc_new_messages">Ungelesene Nachrichten</string>
|
||||
<string name="nc_new_password">Neues Passwort</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">%1$s App nicht auf dem Server installiert, Abbruch</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">%1$s nicht verfügbar (nicht installiert oder von der Administration eingeschränkt)</string>
|
||||
<string name="nc_nick_guest">Gast</string>
|
||||
<string name="nc_no">Nein</string>
|
||||
<string name="nc_no_messages_yet">Noch keine Nachrichten</string>
|
||||
|
@ -165,7 +165,6 @@
|
||||
<string name="nc_new_conversation">Νέα συνομιλία</string>
|
||||
<string name="nc_new_messages">Μη αναγνωσμένα μηνύματα</string>
|
||||
<string name="nc_new_password">Νέο συνθηματικό</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">Η εφαρμογή %1$s δεν εγκαταστάθηκε στον διακομιστή, ματαίωση</string>
|
||||
<string name="nc_nick_guest">Επισκέπτης</string>
|
||||
<string name="nc_no">Όχι</string>
|
||||
<string name="nc_no_messages_yet">Κανένα μήνυμα ακόμα</string>
|
||||
|
@ -187,13 +187,14 @@
|
||||
<string name="nc_message_read">Mensajes leídos</string>
|
||||
<string name="nc_message_sent">Mensaje enviado</string>
|
||||
<string name="nc_microphone_permission_permanently_denied">Para permitir la comunicación de voz, concede el permiso de \"Micrófono\" en la configuración del sistema.</string>
|
||||
<string name="nc_missed_call">Perdiste una llamada de %s</string>
|
||||
<string name="nc_moderator">Moderador</string>
|
||||
<string name="nc_never">Nunca unido</string>
|
||||
<string name="nc_new_conversation">Nueva conversación</string>
|
||||
<string name="nc_new_mention">Menciones sin leer</string>
|
||||
<string name="nc_new_messages">Mensajes no leídos</string>
|
||||
<string name="nc_new_password">Nueva contraseña</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">La app %1$s no está instalada en el servidor. Abortando</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">%1$s no está disponible (no se encuentra instalado o está restringido por el administrador)</string>
|
||||
<string name="nc_nick_guest">Invitado</string>
|
||||
<string name="nc_no">No</string>
|
||||
<string name="nc_no_messages_yet">Aún no hay mensajes</string>
|
||||
@ -354,7 +355,7 @@
|
||||
<string name="nc_upload_confirm_send_multiple">¿Enviar estos archivos a %1$s?</string>
|
||||
<string name="nc_upload_confirm_send_single">¿Enviar este archivo a %1$s?</string>
|
||||
<string name="nc_upload_failed">Lo siento, error en la subida</string>
|
||||
<string name="nc_upload_failed_notification_text">Imposible subir %1$s</string>
|
||||
<string name="nc_upload_failed_notification_text">Fallo al subir %1$s</string>
|
||||
<string name="nc_upload_failed_notification_title">Falla</string>
|
||||
<string name="nc_upload_from_cloud">Compartido por %1$s</string>
|
||||
<string name="nc_upload_from_device">Subir desde dispositivo</string>
|
||||
|
@ -193,7 +193,6 @@
|
||||
<string name="nc_new_mention">Irakurri gabeko aipamenak</string>
|
||||
<string name="nc_new_messages">Irakurri gabeko mezuak</string>
|
||||
<string name="nc_new_password">Pasahitz berria</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">%1$s app-a ez dago instalatuta zerbitzarian, bertan behera uzten</string>
|
||||
<string name="nc_nick_guest">Gonbidatua</string>
|
||||
<string name="nc_no">Ez</string>
|
||||
<string name="nc_no_messages_yet">Ez dago mezurik oraindik</string>
|
||||
|
@ -20,6 +20,7 @@
|
||||
<string name="file_list_loading">بارگذاری …</string>
|
||||
<string name="fourHours">۴ ساعت</string>
|
||||
<string name="invisible">نامرئی</string>
|
||||
<string name="load_more_results">بار کردن نتیحههای بیشتر</string>
|
||||
<string name="lock_symbol">نماد قفل</string>
|
||||
<string name="menu_item_sort_by_date_newest_first">تازهترینها اول </string>
|
||||
<string name="menu_item_sort_by_date_oldest_first">قدیمیترینها اول </string>
|
||||
@ -132,7 +133,6 @@
|
||||
<string name="nc_new_conversation">مکالمه جدید</string>
|
||||
<string name="nc_new_messages">پیامهای خوانده نشده</string>
|
||||
<string name="nc_new_password">گذرواژه جدید</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">برنامه %1$s روی سرور نصب نمیباشد. درحال لغو</string>
|
||||
<string name="nc_nick_guest">مهمان</string>
|
||||
<string name="nc_no">نه</string>
|
||||
<string name="nc_no_messages_yet">هنوز پیامی ارسال نشده است</string>
|
||||
|
@ -164,7 +164,6 @@
|
||||
<string name="nc_new_mention">Lukemattomat maininnat</string>
|
||||
<string name="nc_new_messages">Lukemattomat viestit</string>
|
||||
<string name="nc_new_password">Uusi salasana</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">Sovellusta %1$s ei ole asennettu, keskeytetään</string>
|
||||
<string name="nc_nick_guest">Vieras</string>
|
||||
<string name="nc_no">Ei</string>
|
||||
<string name="nc_no_messages_yet">Ei viestejä vielä</string>
|
||||
|
@ -187,13 +187,13 @@
|
||||
<string name="nc_message_read">Message lu</string>
|
||||
<string name="nc_message_sent">Message envoyé</string>
|
||||
<string name="nc_microphone_permission_permanently_denied">Pour établir une communication audio, veuillez autoriser 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_never">Jamais contacté</string>
|
||||
<string name="nc_new_conversation">Nouvelle conversation</string>
|
||||
<string name="nc_new_mention">Mentions non lues</string>
|
||||
<string name="nc_new_messages">Messages non lus</string>
|
||||
<string name="nc_new_password">Nouveau mot de passe</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">Application %1$s non installée sur le serveur, abandon</string>
|
||||
<string name="nc_nick_guest">Invité</string>
|
||||
<string name="nc_no">Non</string>
|
||||
<string name="nc_no_messages_yet">Pas de messages</string>
|
||||
|
@ -140,7 +140,6 @@
|
||||
<string name="nc_new_conversation">Nova conversa</string>
|
||||
<string name="nc_new_messages">Mensaxes sen ler</string>
|
||||
<string name="nc_new_password">Novo contrasinal</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">A aplicación %1$s non está instalado no servidor, interrompendo</string>
|
||||
<string name="nc_nick_guest">Convidado</string>
|
||||
<string name="nc_no">Non</string>
|
||||
<string name="nc_no_messages_yet">Aínda non hai mensaxes</string>
|
||||
|
@ -172,7 +172,6 @@
|
||||
<string name="nc_new_mention">Nepročitana spominjanja</string>
|
||||
<string name="nc_new_messages">Nove poruke</string>
|
||||
<string name="nc_new_password">Nova zaporka</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">Aplikacija %1$s nije instalirana na poslužitelju, prekid</string>
|
||||
<string name="nc_nick_guest">Gost</string>
|
||||
<string name="nc_no">Ne</string>
|
||||
<string name="nc_no_messages_yet">Još nema poruka</string>
|
||||
|
@ -10,6 +10,7 @@
|
||||
<string name="avatar">Profilkép</string>
|
||||
<string name="away">Távol</string>
|
||||
<string name="call_without_notification">Hívás értesítés nélkül</string>
|
||||
<string name="camera_permission_granted">Kamera engedély megadva. Válassza újra a kamerát.</string>
|
||||
<string name="choose_avatar_from_cloud">Profilkép választása a felhőből</string>
|
||||
<string name="clear_status_message">Állapotüzenet törlése</string>
|
||||
<string name="clear_status_message_after">Állapotüzenet törlése ennyi idő után:</string>
|
||||
@ -23,6 +24,7 @@
|
||||
<string name="emoji_category_recent">Legutóbbiak</string>
|
||||
<string name="emoji_search">Emodzsi keresése</string>
|
||||
<string name="encrypted">Titkosított</string>
|
||||
<string name="error_loading_chats">Hiba történt a csevegések betöltése során</string>
|
||||
<string name="failed_to_save">Sikertelen mentés: %1$s</string>
|
||||
<string name="file_list_folder">mappa</string>
|
||||
<string name="file_list_loading">Betöltés…</string>
|
||||
@ -185,23 +187,26 @@
|
||||
<string name="nc_message_read">Üzenet elolvasva</string>
|
||||
<string name="nc_message_sent">Üzenet elküldve</string>
|
||||
<string name="nc_microphone_permission_permanently_denied">A hanghívás engedélyezéséhez a rendszerbeállításokban meg kell adnia a „Mikrofon” engedélyt.</string>
|
||||
<string name="nc_missed_call">Nem fogadott hívás a következőtől: %s</string>
|
||||
<string name="nc_moderator">Moderátor</string>
|
||||
<string name="nc_never">Soha nem csatlakozott</string>
|
||||
<string name="nc_new_conversation">Új beszélgetés</string>
|
||||
<string name="nc_new_mention">Olvasatlan említések</string>
|
||||
<string name="nc_new_messages">Olvasatlan üzenetek</string>
|
||||
<string name="nc_new_password">Új jelszó</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">A(z) %1$s alkalmazás nincs telepítve a kiszolgálón, megszakítás</string>
|
||||
<string name="nc_nick_guest">Vendég</string>
|
||||
<string name="nc_no">Nem</string>
|
||||
<string name="nc_no_messages_yet">Nincs még üzenet</string>
|
||||
<string name="nc_no_proxy">Nincs proxy</string>
|
||||
<string name="nc_not_allowed_to_activate_audio">Nem kapcsolhatja be a hangot.</string>
|
||||
<string name="nc_not_allowed_to_activate_video">Nem kapcsolhatja be a videót.</string>
|
||||
<string name="nc_notification_channel">%1$s a(z) %2$s értesítési csatornán</string>
|
||||
<string name="nc_notification_channel_calls">Hívások</string>
|
||||
<string name="nc_notification_channel_calls_description">Értesítés a bejövő hívásokról</string>
|
||||
<string name="nc_notification_channel_messages">Üzenetek</string>
|
||||
<string name="nc_notification_channel_messages_description">Értesítés a bejövő üzenetekről</string>
|
||||
<string name="nc_notification_channel_uploads">Feltöltések</string>
|
||||
<string name="nc_notification_channel_uploads_description">Értesítés a feltöltési folyamatról</string>
|
||||
<string name="nc_notification_settings">Értesítési beállítások</string>
|
||||
<string name="nc_notify_me_always">Mindig értesítsen</string>
|
||||
<string name="nc_notify_me_mention">Értesítsen, ha megemlítik Önt</string>
|
||||
@ -348,11 +353,16 @@
|
||||
<string name="nc_upload_confirm_send_multiple">Fájlok küldése ide: %1$s</string>
|
||||
<string name="nc_upload_confirm_send_single">Ezen fájl küldése ide: %1$s</string>
|
||||
<string name="nc_upload_failed">A feltöltés sikertelen</string>
|
||||
<string name="nc_upload_failed_notification_text">A(z) %1$s feltöltése sikertelen</string>
|
||||
<string name="nc_upload_failed_notification_title">Sikertelen</string>
|
||||
<string name="nc_upload_from_cloud">Megosztás innen: %1$s</string>
|
||||
<string name="nc_upload_from_device">Feltöltés az eszközről</string>
|
||||
<string name="nc_upload_in_progess">Feltöltés</string>
|
||||
<string name="nc_upload_notification_text">%1$s → %2$s – %3$s\%%</string>
|
||||
<string name="nc_upload_picture_from_cam">Fénykép készítése</string>
|
||||
<string name="nc_upload_video_from_cam">Videó készítése</string>
|
||||
<string name="nc_user">Felhasználó</string>
|
||||
<string name="nc_video_filename">Videórögzítés innen: %1$s</string>
|
||||
<string name="nc_voice_message_filename">Beszédrögzítés innen: %1$s (%2$s)</string>
|
||||
<string name="nc_voice_message_hold_to_record_info">Tartsa a rögzítéshez, engedje el a küldéshez.</string>
|
||||
<string name="nc_voice_message_missing_audio_permission">Hangrögzítési engedély szükséges</string>
|
||||
|
@ -127,7 +127,6 @@
|
||||
<string name="nc_new_conversation">Nýtt samtal</string>
|
||||
<string name="nc_new_messages">Ólesin skilaboð</string>
|
||||
<string name="nc_new_password">Nýtt lykilorð</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">%1$s forritið ekki uppsett á þjóninu, hætti við</string>
|
||||
<string name="nc_nick_guest">Gestur</string>
|
||||
<string name="nc_no">Nei</string>
|
||||
<string name="nc_no_messages_yet">Engin skilaboð ennþá</string>
|
||||
|
@ -178,7 +178,6 @@
|
||||
<string name="nc_new_mention">Menzioni non lette</string>
|
||||
<string name="nc_new_messages">Messaggi non letti</string>
|
||||
<string name="nc_new_password">Nuova password</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">Applicazione %1$s non installata, interruzione in corso</string>
|
||||
<string name="nc_nick_guest">Ospite</string>
|
||||
<string name="nc_no">No</string>
|
||||
<string name="nc_no_messages_yet">Ancora nessun messaggio</string>
|
||||
|
@ -127,7 +127,6 @@
|
||||
<string name="nc_new_conversation">דיון חדש</string>
|
||||
<string name="nc_new_messages">הודעות שלא נקראו</string>
|
||||
<string name="nc_new_password">ססמה חדשה</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">היישומון %1$s אינו מותקן על השרת, הפעולה מבוטלת</string>
|
||||
<string name="nc_nick_guest">אורח/ת</string>
|
||||
<string name="nc_no">לא</string>
|
||||
<string name="nc_no_messages_yet">אין הודעות עדיין</string>
|
||||
|
@ -179,7 +179,6 @@
|
||||
<string name="nc_new_mention">未読の返信</string>
|
||||
<string name="nc_new_messages">未読のメッセージ</string>
|
||||
<string name="nc_new_password">新しいパスワード</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">%1$s アプリがサーバーにインストールされていません。中断します。</string>
|
||||
<string name="nc_nick_guest">ゲスト</string>
|
||||
<string name="nc_no">いいえ</string>
|
||||
<string name="nc_no_messages_yet">メッセージはまだありません</string>
|
||||
|
@ -147,7 +147,7 @@
|
||||
<string name="nc_new_mention">읽지 않은 언급</string>
|
||||
<string name="nc_new_messages">읽지 않은 메세지</string>
|
||||
<string name="nc_new_password">새 암호</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">서버에 %1$s 앱이 설치되어 있지 않음, 중단함</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">%1$s을(를) 사용할 수 없음 (설치되지 않았거나 관리자에 의해 제한됨)</string>
|
||||
<string name="nc_nick_guest">손님</string>
|
||||
<string name="nc_no">아니요</string>
|
||||
<string name="nc_no_messages_yet">아직 메시지 없음</string>
|
||||
|
@ -125,7 +125,6 @@
|
||||
<string name="nc_new_conversation">Naujas pokalbis</string>
|
||||
<string name="nc_new_messages">Neskaitytos žinutės</string>
|
||||
<string name="nc_new_password">Naujas slaptažodis</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">Serveryje nėra įdiegta programėlė %1$s, nutraukiama</string>
|
||||
<string name="nc_nick_guest">Svečias</string>
|
||||
<string name="nc_no">Ne</string>
|
||||
<string name="nc_no_messages_yet">Kol kas žinučių nėra</string>
|
||||
|
@ -140,7 +140,6 @@
|
||||
<string name="nc_new_mention">Uleste nevner</string>
|
||||
<string name="nc_new_messages">Uleste meldinger</string>
|
||||
<string name="nc_new_password">Nytt passord</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">%1$s-appen er ikke installert på serveren, avbryter</string>
|
||||
<string name="nc_nick_guest">Gjest</string>
|
||||
<string name="nc_no">Nei</string>
|
||||
<string name="nc_no_messages_yet">Ingen meldiner enda</string>
|
||||
@ -279,6 +278,7 @@
|
||||
<string name="scope_federated_description">Synkroniser kun til betrodde servere</string>
|
||||
<string name="scope_federated_title">Sammenknyttet</string>
|
||||
<string name="scope_local_title">Lokal</string>
|
||||
<string name="scope_private_description">Kun synlig for personer som matches via telefonnummerintegrasjon via Talk på mobil</string>
|
||||
<string name="scope_private_title">Privat</string>
|
||||
<string name="scope_published_description">Synkroniser til betrodde servere og den globale og offentlige adresseboken</string>
|
||||
<string name="scope_published_title">Publisert</string>
|
||||
|
@ -176,7 +176,6 @@ Kies er eentje van een provider.</string>
|
||||
<string name="nc_new_mention">Ongelezen vermeldingen</string>
|
||||
<string name="nc_new_messages">Ongelezen berichten</string>
|
||||
<string name="nc_new_password">Nieuw wachtwoord</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">%1$s app is niet geïnstalleerd op de server, afbreken</string>
|
||||
<string name="nc_nick_guest">Gast</string>
|
||||
<string name="nc_no">Nee</string>
|
||||
<string name="nc_no_messages_yet">Nog geen berichten</string>
|
||||
|
@ -137,7 +137,6 @@
|
||||
<string name="nc_new_conversation">Ny samtale</string>
|
||||
<string name="nc_new_messages">Meldingar er ikkje lest</string>
|
||||
<string name="nc_new_password">Nytt passord</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">%1$s applikasjon er ikkje installert på denne server, avbryt</string>
|
||||
<string name="nc_nick_guest">Gjest</string>
|
||||
<string name="nc_no">Nei</string>
|
||||
<string name="nc_no_messages_yet">Ingen meldingar til no</string>
|
||||
|
@ -193,7 +193,6 @@
|
||||
<string name="nc_new_mention">Nieprzeczytane wzmianki</string>
|
||||
<string name="nc_new_messages">Nieprzeczytane wiadomości</string>
|
||||
<string name="nc_new_password">Nowe hasło</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">Aplikacja %1$s nie została zainstalowana na serwerze, przerwano żądanie</string>
|
||||
<string name="nc_nick_guest">Gość</string>
|
||||
<string name="nc_no">Nie</string>
|
||||
<string name="nc_no_messages_yet">Nie ma nowych wiadomości</string>
|
||||
|
@ -193,7 +193,6 @@
|
||||
<string name="nc_new_mention">Menções não lidas</string>
|
||||
<string name="nc_new_messages">Mensagens não lidas</string>
|
||||
<string name="nc_new_password">Nova senha</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">Aplicativo %1$s não instalado no servidor, cancelando</string>
|
||||
<string name="nc_nick_guest">Convidado</string>
|
||||
<string name="nc_no">Não</string>
|
||||
<string name="nc_no_messages_yet">Sem mensagens ainda</string>
|
||||
@ -248,7 +247,7 @@
|
||||
<string name="nc_screen_lock_timeout_sixty">60</string>
|
||||
<string name="nc_screen_lock_timeout_thirty">30</string>
|
||||
<string name="nc_screen_lock_timeout_three_hundred">300</string>
|
||||
<string name="nc_search">Persquisar</string>
|
||||
<string name="nc_search">Pesquisar</string>
|
||||
<string name="nc_select_an_account">Selecionar uma conta</string>
|
||||
<string name="nc_select_participants">Selecionar participantes</string>
|
||||
<string name="nc_sent_a_gif" formatted="true">%1$s enviou um GIF.</string>
|
||||
|
@ -193,7 +193,7 @@
|
||||
<string name="nc_new_mention">Непрочитанные упоминания</string>
|
||||
<string name="nc_new_messages">Непрочитанные сообщения</string>
|
||||
<string name="nc_new_password">Новый пароль</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">Приложение %1$s не установлено на сервере. Действие отменено.</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">Приложение %1$s недоступно (не установлено, либо использование приложения ограничено администратором)</string>
|
||||
<string name="nc_nick_guest">Гость</string>
|
||||
<string name="nc_no">Нет</string>
|
||||
<string name="nc_no_messages_yet">Сообщений еще нет</string>
|
||||
|
@ -161,7 +161,6 @@
|
||||
<string name="nc_new_conversation">Resonada noa</string>
|
||||
<string name="nc_new_messages">Messàgios non lèghidos</string>
|
||||
<string name="nc_new_password">Crae noa</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">%1$s s\'aplicatzione no est installada in su serbidore, annullende</string>
|
||||
<string name="nc_nick_guest">Persone invitada</string>
|
||||
<string name="nc_no">No</string>
|
||||
<string name="nc_no_messages_yet">Ancora perunu messàgiu</string>
|
||||
|
@ -193,7 +193,6 @@
|
||||
<string name="nc_new_mention">Neprečítané upozornenia</string>
|
||||
<string name="nc_new_messages">Neprečítané správy</string>
|
||||
<string name="nc_new_password">Nové heslo</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">Aplikácia %1$s nie je na serveri nainštalovaná, ruší sa</string>
|
||||
<string name="nc_nick_guest">Hosť</string>
|
||||
<string name="nc_no">Nie</string>
|
||||
<string name="nc_no_messages_yet">Ešte žiadne správy</string>
|
||||
|
@ -9,6 +9,7 @@
|
||||
<string name="audio_output_wired_headset">Ožičene slušalke</string>
|
||||
<string name="avatar">Podoba</string>
|
||||
<string name="away">Ne spremljam</string>
|
||||
<string name="call_without_notification">Klic brez obvestila</string>
|
||||
<string name="choose_avatar_from_cloud">Izbor pogodbe iz oblaka</string>
|
||||
<string name="clear_status_message">Počisti sporočilo stanja</string>
|
||||
<string name="clear_status_message_after">Počisti sporočilo stanja po</string>
|
||||
@ -181,7 +182,6 @@
|
||||
<string name="nc_new_mention">Neprebrane omembe</string>
|
||||
<string name="nc_new_messages">Neprebrana sporočila</string>
|
||||
<string name="nc_new_password">Novo geslo</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">Program %1$s na strežniku ni nameščen, zahteva bo preklicana.</string>
|
||||
<string name="nc_nick_guest">Gost</string>
|
||||
<string name="nc_no">Ne</string>
|
||||
<string name="nc_no_messages_yet">Ni še sporočil</string>
|
||||
|
@ -121,7 +121,6 @@
|
||||
<string name="nc_new_conversation">Нови разговор</string>
|
||||
<string name="nc_new_messages">Непрочитане поруке</string>
|
||||
<string name="nc_new_password">Нова лозинка</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">%1$s апликација није инсталирана на серверу, прекидам</string>
|
||||
<string name="nc_nick_guest">Гост</string>
|
||||
<string name="nc_no">Не</string>
|
||||
<string name="nc_no_messages_yet">Још нема порука</string>
|
||||
|
@ -141,7 +141,6 @@
|
||||
<string name="nc_new_conversation">Ny konversation</string>
|
||||
<string name="nc_new_messages">Olästa meddelanden</string>
|
||||
<string name="nc_new_password">Nytt lösenord</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">%1$s app inte installerad på servern, avbryter</string>
|
||||
<string name="nc_nick_guest">Gäst</string>
|
||||
<string name="nc_no">Nej</string>
|
||||
<string name="nc_no_messages_yet">Inga meddelanden än</string>
|
||||
@ -284,6 +283,7 @@
|
||||
<string name="online_status">Online-status</string>
|
||||
<string name="polls_add_option">Lägg till alternativ</string>
|
||||
<string name="polls_options">Alternativ</string>
|
||||
<string name="polls_private_poll">Privat omröstning</string>
|
||||
<string name="polls_results_subtitle">Resultat</string>
|
||||
<string name="polls_settings">Inställningar</string>
|
||||
<string name="polls_submit_vote">Rösta</string>
|
||||
|
@ -187,13 +187,14 @@
|
||||
<string name="nc_message_read">İleti okundu</string>
|
||||
<string name="nc_message_sent">İleti gönderildi</string>
|
||||
<string name="nc_microphone_permission_permanently_denied">Sesli iletişim kurabilmek için sistem ayarlarından \"Mikrofon\" erişme iznini verin.</string>
|
||||
<string name="nc_missed_call">%s sizi aramış</string>
|
||||
<string name="nc_moderator">Sorumlu</string>
|
||||
<string name="nc_never">Hiç katılmadı</string>
|
||||
<string name="nc_new_conversation">Yeni görüşme</string>
|
||||
<string name="nc_new_mention">Okunmamış anmalar</string>
|
||||
<string name="nc_new_messages">Okunmamış iletiler</string>
|
||||
<string name="nc_new_password">Yeni parola</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">%1$s uygulaması sunucu üzerinde kurulu değil, vazgeçiliyor</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">%1$s kullanılamıyor (kurulmamış ya da yönetici tarafından engellenmiş)</string>
|
||||
<string name="nc_nick_guest">Konuk</string>
|
||||
<string name="nc_no">Hayır</string>
|
||||
<string name="nc_no_messages_yet">Henüz bir ileti yok</string>
|
||||
|
@ -166,7 +166,6 @@
|
||||
<string name="nc_new_mention">Непрочитані згадки</string>
|
||||
<string name="nc_new_messages">Непрочитані повідомлення</string>
|
||||
<string name="nc_new_password">Новий пароль</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">Застосунок %1$s не встановлено на сервері, скасування</string>
|
||||
<string name="nc_nick_guest">Гість</string>
|
||||
<string name="nc_no">Ні</string>
|
||||
<string name="nc_no_messages_yet">Повідомлень немає</string>
|
||||
@ -289,7 +288,7 @@
|
||||
<string name="nc_share_to_choose_account">Оберіть обліковий запис</string>
|
||||
<string name="nc_shared_items_location">Місце</string>
|
||||
<string name="nc_shared_location">Місце у спільному доступі</string>
|
||||
<string name="nc_sort_by">Сортувати по</string>
|
||||
<string name="nc_sort_by">Впорядкувати за</string>
|
||||
<string name="nc_upload_choose_local_files">Виберіть файли</string>
|
||||
<string name="nc_upload_failed">Вибачте, помилка завантаження</string>
|
||||
<string name="nc_upload_from_device">Завантажити з пристрою</string>
|
||||
@ -319,7 +318,7 @@
|
||||
<string name="selected_list_item">Selected</string>
|
||||
<string name="set_status">Встановити статус</string>
|
||||
<string name="set_status_message">Встановити повідомлення про стан</string>
|
||||
<string name="share">Поділитися</string>
|
||||
<string name="share">Спільний доступ</string>
|
||||
<string name="shared_items_audio">Аудіо</string>
|
||||
<string name="shared_items_file">Файл</string>
|
||||
<string name="shared_items_media">Зображення та відео</string>
|
||||
|
@ -126,7 +126,6 @@
|
||||
<string name="nc_new_conversation">Tạo đàm thoại mới</string>
|
||||
<string name="nc_new_messages">Tin nhắn chưa đọc</string>
|
||||
<string name="nc_new_password">Mật khẩu mới</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">%1$s ứng dụng chưa cài đặt trên hệ thống, hủy bỏ</string>
|
||||
<string name="nc_nick_guest">Khách</string>
|
||||
<string name="nc_no">Không</string>
|
||||
<string name="nc_no_messages_yet">Chưa có thông điệp nào</string>
|
||||
|
@ -51,6 +51,7 @@
|
||||
<string name="nc_call_name">会话名称</string>
|
||||
<string name="nc_call_name_is_same">您输入的名称已经存在</string>
|
||||
<string name="nc_call_notifications">呼叫通知</string>
|
||||
<string name="nc_call_reconnecting">正在重新连接 ...</string>
|
||||
<string name="nc_call_ringing">响铃</string>
|
||||
<string name="nc_call_state_in_call">%1$s 在通话中</string>
|
||||
<string name="nc_call_state_with_phone">%1$s 通过手机</string>
|
||||
@ -112,6 +113,7 @@
|
||||
<string name="nc_expire_message_one_day">1 天</string>
|
||||
<string name="nc_expire_message_one_hour">1 小时</string>
|
||||
<string name="nc_expire_message_one_week">1 周</string>
|
||||
<string name="nc_expire_messages_explanation">可指定聊天消息在一定时间后过期。注意:聊天中分享的文件不会被从所有者一方删除,但是会被从会话中取消分享。</string>
|
||||
<string name="nc_external_server_failed">获取网络设置失败</string>
|
||||
<string name="nc_failed_signaling_settings">目标服务器不支持通过移动电话加入公共对话。你可以尝试通过浏览器加入对话。</string>
|
||||
<string name="nc_failed_to_perform_operation">抱歉,有地方出错了!</string>
|
||||
@ -132,6 +134,7 @@
|
||||
<string name="nc_guest_access_password_weak_alert_title">弱密码</string>
|
||||
<string name="nc_guest_access_resend_invitations">重发邀请</string>
|
||||
<string name="nc_guest_access_share_link">共享会话链接</string>
|
||||
<string name="nc_hint_enter_a_message">输入消息…</string>
|
||||
<string name="nc_important_conversation">重要会话</string>
|
||||
<string name="nc_important_conversation_desc">此会话中的通知将覆盖免打扰设置</string>
|
||||
<string name="nc_join_via_link">使用链接加入</string>
|
||||
@ -140,6 +143,7 @@
|
||||
<string name="nc_last_moderator_title">无法离开会话</string>
|
||||
<string name="nc_last_modified">%1$s 我上一次更改: %2$s</string>
|
||||
<string name="nc_leave">离开会话</string>
|
||||
<string name="nc_leaving_call">正在离开通话 ...</string>
|
||||
<string name="nc_license_summary">GNU 通用公共许可证,第3版</string>
|
||||
<string name="nc_license_title">许可证</string>
|
||||
<string name="nc_limit_hit">已达到%s字符限制</string>
|
||||
@ -167,7 +171,6 @@
|
||||
<string name="nc_new_mention">未读的提及</string>
|
||||
<string name="nc_new_messages">未读消息</string>
|
||||
<string name="nc_new_password">新密码</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">服务器未安装 %1$s 应用,中止</string>
|
||||
<string name="nc_nick_guest">来宾</string>
|
||||
<string name="nc_no">不</string>
|
||||
<string name="nc_no_messages_yet">暂无消息</string>
|
||||
@ -309,7 +312,9 @@
|
||||
<string name="nc_share_text_pass">\n密码: %1$s</string>
|
||||
<string name="nc_share_this_location">分享这个位置</string>
|
||||
<string name="nc_share_to_choose_account">选择一个账户</string>
|
||||
<string name="nc_shared_items">已分享项目</string>
|
||||
<string name="nc_shared_items_deck_card">Deck 卡片</string>
|
||||
<string name="nc_shared_items_empty">沒有已分享的项目</string>
|
||||
<string name="nc_shared_items_location">位置</string>
|
||||
<string name="nc_shared_location">共享的位置</string>
|
||||
<string name="nc_sort_by">排序依据</string>
|
||||
|
@ -187,13 +187,14 @@
|
||||
<string name="nc_message_read">訊息已讀</string>
|
||||
<string name="nc_message_sent">訊息已傳送</string>
|
||||
<string name="nc_microphone_permission_permanently_denied">為了開啟聲音的通訊請在系統設定內同意\"麥克風\"的需求。</string>
|
||||
<string name="nc_missed_call">您錯過了 %s 的來電</string>
|
||||
<string name="nc_moderator">主持人</string>
|
||||
<string name="nc_never">從未加入</string>
|
||||
<string name="nc_new_conversation">新對話</string>
|
||||
<string name="nc_new_mention">未讀的提及</string>
|
||||
<string name="nc_new_messages">未讀郵件</string>
|
||||
<string name="nc_new_password">新密碼</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">此伺服器並未安裝%1$s應用程式,操作中斷。</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">%1$s 不可用(管理員未安裝或限制)</string>
|
||||
<string name="nc_nick_guest">訪客</string>
|
||||
<string name="nc_no">否</string>
|
||||
<string name="nc_no_messages_yet">目前無任何訊息</string>
|
||||
|
@ -175,7 +175,6 @@
|
||||
<string name="nc_new_mention">未讀的提及</string>
|
||||
<string name="nc_new_messages">未讀訊息</string>
|
||||
<string name="nc_new_password">新密碼</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">此伺服器並未安裝%1$s應用,操作中斷。</string>
|
||||
<string name="nc_nick_guest">訪客</string>
|
||||
<string name="nc_no">否</string>
|
||||
<string name="nc_no_messages_yet">目前無任何訊息</string>
|
||||
|
@ -58,7 +58,7 @@
|
||||
<string name="nc_capabilities_failed">Failed to fetch capabilities, aborting</string>
|
||||
<string name="nc_external_server_failed">Failed to fetch signaling settings</string>
|
||||
<string name="nc_display_name_not_fetched">Display name couldn\'t be fetched, aborting</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">%1$s app not installed on the server, aborting</string>
|
||||
<string name="nc_nextcloud_talk_app_not_installed">%1$s not available (not installed or restricted by admin)</string>
|
||||
<string name="nc_display_name_not_stored">Could not store display name, aborting</string>
|
||||
|
||||
<string name="nc_never">Never joined</string>
|
||||
@ -210,6 +210,7 @@
|
||||
<string name="nc_call_state_in_call">%1$s in call</string>
|
||||
<string name="nc_call_state_with_phone">%1$s with phone</string>
|
||||
<string name="nc_call_state_with_video">%1$s with video</string>
|
||||
<string name="nc_missed_call">You missed a call from %s</string>
|
||||
|
||||
<!-- Picture in Picture -->
|
||||
<string name="nc_pip_microphone_mute">Mute microphone</string>
|
||||
@ -246,7 +247,6 @@
|
||||
<string name="nc_share_subject">%1$s invitation</string>
|
||||
<string name="nc_share_text_pass">\nPassword: %1$s</string>
|
||||
|
||||
<!-- Magical stuff -->
|
||||
<string name="nc_push_to_talk">Push-to-talk</string>
|
||||
<string name="nc_push_to_talk_desc">With microphone disabled, click&hold to use Push-to-talk</string>
|
||||
<string name="nc_configure_cert_auth">Select authentication certificate</string>
|
||||
|
@ -24,7 +24,7 @@
|
||||
buildscript {
|
||||
|
||||
ext {
|
||||
kotlinVersion = '1.7.20'
|
||||
kotlinVersion = '1.7.21'
|
||||
}
|
||||
|
||||
repositories {
|
||||
|
@ -45,7 +45,7 @@ abstract class DownloadWebRtcTask extends DefaultTask {
|
||||
|
||||
private String getDownloadUrl() {
|
||||
def webRtcVersion = version.get()
|
||||
return "https://github.com/nextcloud-releases/talk-clients-webrtc/releases/download/${webRtcVersion}-RC1/${getFileName()}"
|
||||
return "https://github.com/nextcloud-releases/talk-clients-webrtc/releases/download/${webRtcVersion}/${getFileName()}"
|
||||
}
|
||||
|
||||
private String getOutputPath() {
|
||||
|
@ -26,3 +26,9 @@ include ':app'
|
||||
// substitute module('com.github.nextcloud.android-common:ui') using project(':ui')
|
||||
// }
|
||||
//}
|
||||
|
||||
//includeBuild('../../../deps/ImagePicker') {
|
||||
// dependencySubstitution {
|
||||
// substitute module('com.github.nextcloud-deps:ImagePicker') using project(':imagepicker')
|
||||
// }
|
||||
//}
|
||||
|
Loading…
Reference in New Issue
Block a user