First step towards working calls on Q

Signed-off-by: Mario Danic <mario@lovelyhq.com>
This commit is contained in:
Mario Danic 2020-02-10 00:33:41 +01:00
parent 6e7637af43
commit 9e2fbc076c
No known key found for this signature in database
GPG Key ID: CDE0BBD2738C4CC0
10 changed files with 231 additions and 92 deletions

View File

@ -253,6 +253,7 @@ dependencies {
}) })
findbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.9.0' findbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.9.0'
findbugsPlugins 'com.mebigfatguy.fb-contrib:fb-contrib:7.4.6' findbugsPlugins 'com.mebigfatguy.fb-contrib:fb-contrib:7.4.6'
implementation "com.google.firebase:firebase-messaging:20.1.0"
} }
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {

View File

@ -19,5 +19,5 @@
*/ */
dependencies { dependencies {
implementation "com.google.firebase:firebase-messaging:20.0.0" implementation "com.google.firebase:firebase-messaging:20.1.0"
} }

View File

@ -1,73 +0,0 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017 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 autodagger.AutoInjector;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
import com.nextcloud.talk.application.NextcloudTalkApplication;
import com.nextcloud.talk.jobs.NotificationWorker;
import com.nextcloud.talk.jobs.PushRegistrationWorker;
import com.nextcloud.talk.utils.bundle.BundleKeys;
import androidx.work.Data;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import com.nextcloud.talk.utils.preferences.AppPreferences;
import javax.inject.Inject;
@AutoInjector(NextcloudTalkApplication.class)
public class MagicFirebaseMessagingService extends FirebaseMessagingService {
@Inject
AppPreferences appPreferences;
@Override
public void onNewToken(String token) {
super.onNewToken(token);
NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
appPreferences.setPushToken(token);
OneTimeWorkRequest pushRegistrationWork = new OneTimeWorkRequest.Builder(PushRegistrationWorker.class).build();
WorkManager.getInstance().enqueue(pushRegistrationWork);
}
@SuppressLint("LongLogTag")
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
if (remoteMessage == null) {
return;
}
if (remoteMessage.getData() != null) {
Data messageData = new Data.Builder()
.putString(BundleKeys.INSTANCE.getKEY_NOTIFICATION_SUBJECT(), remoteMessage.getData().get("subject"))
.putString(BundleKeys.INSTANCE.getKEY_NOTIFICATION_SIGNATURE(), remoteMessage.getData().get("signature"))
.build();
OneTimeWorkRequest pushNotificationWork = new OneTimeWorkRequest.Builder(NotificationWorker.class)
.setInputData(messageData)
.build();
WorkManager.getInstance().enqueue(pushNotificationWork);
}
}
}

View File

@ -0,0 +1,185 @@
/*
* 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.services.firebase
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Intent
import android.media.AudioAttributes
import android.net.Uri
import android.os.Bundle
import android.text.TextUtils
import android.util.Base64
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
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.MagicCallActivity
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.models.RingtoneSettings
import com.nextcloud.talk.models.SignatureVerification
import com.nextcloud.talk.models.json.push.DecryptedPushMessage
import com.nextcloud.talk.utils.NotificationUtils.NOTIFICATION_CHANNEL_CALLS_V3
import com.nextcloud.talk.utils.NotificationUtils.cancelAllNotificationsForAccount
import com.nextcloud.talk.utils.NotificationUtils.cancelExistingNotificationWithId
import com.nextcloud.talk.utils.NotificationUtils.createNotificationChannel
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 java.io.IOException
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import java.security.PrivateKey
import javax.crypto.Cipher
import javax.crypto.NoSuchPaddingException
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class MagicFirebaseMessagingService : FirebaseMessagingService() {
@JvmField
@Inject
var appPreferences: AppPreferences? = null
private var decryptedPushMessage: DecryptedPushMessage? = null
private var signatureVerification: SignatureVerification? = null
override fun onNewToken(token: String) {
super.onNewToken(token)
sharedApplication!!.componentApplication.inject(this)
appPreferences!!.pushToken = token
val pushRegistrationWork = OneTimeWorkRequest.Builder(PushRegistrationWorker::class.java).build()
WorkManager.getInstance().enqueue(pushRegistrationWork)
}
@SuppressLint("LongLogTag")
override fun onMessageReceived(remoteMessage: RemoteMessage) {
sharedApplication!!.componentApplication.inject(this)
if (!remoteMessage.data["subject"].isNullOrEmpty() && !remoteMessage.data["signature"].isNullOrEmpty()) {
decryptMessage(remoteMessage.data["subject"]!!, remoteMessage.data["signature"]!!)
}
}
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) {
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 {
timestamp = System.currentTimeMillis()
if (delete) {
cancelExistingNotificationWithId(applicationContext, signatureVerification!!.userEntity, notificationId)
} else if (deleteAll) {
cancelAllNotificationsForAccount(applicationContext, signatureVerification!!.userEntity)
} else if (type == "call") {
val fullScreenIntent = Intent(applicationContext, MagicCallActivity::class.java)
val bundle = Bundle()
bundle.putString(BundleKeys.KEY_ROOM_ID, decryptedPushMessage!!.id)
bundle.putParcelable(KEY_USER_ENTITY, signatureVerification!!.userEntity)
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@MagicFirebaseMessagingService, 0, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT)
val audioAttributesBuilder = AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
audioAttributesBuilder.setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST)
val ringtonePreferencesString: String? = appPreferences!!.callRingtoneUri
val soundUri = if (TextUtils.isEmpty(ringtonePreferencesString)) {
Uri.parse("android.resource://" + applicationContext.packageName +
"/raw/librem_by_feandesign_call")
} else {
try {
val ringtoneSettings = LoganSquare.parse(ringtonePreferencesString, RingtoneSettings::class.java)
ringtoneSettings.ringtoneUri
} catch (exception: IOException) {
Uri.parse("android.resource://" + applicationContext.packageName + "/raw/librem_by_feandesign_call")
}
}
createNotificationChannel(applicationContext!!,
NOTIFICATION_CHANNEL_CALLS_V3, applicationContext.resources
.getString(R.string.nc_notification_channel_calls), applicationContext.resources
.getString(R.string.nc_notification_channel_calls_description), true,
NotificationManagerCompat.IMPORTANCE_HIGH, soundUri!!, audioAttributesBuilder.build())
val uri = Uri.parse(signatureVerification!!.userEntity.baseUrl)
val baseUrl = uri.host
val notificationBuilder = NotificationCompat.Builder(this@MagicFirebaseMessagingService, NOTIFICATION_CHANNEL_CALLS_V3)
.setPriority(NotificationCompat.PRIORITY_MAX)
.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)
.setFullScreenIntent(fullScreenPendingIntent, true)
.setSound(soundUri)
startForeground(System.currentTimeMillis().toInt(), notificationBuilder.build())
} 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)
}
}
}
} catch (e1: NoSuchAlgorithmException) {
Log.d(NotificationWorker.TAG, "No proper algorithm to decrypt the message " + e1.localizedMessage)
} catch (e1: NoSuchPaddingException) {
Log.d(NotificationWorker.TAG, "No proper padding to decrypt the message " + e1.localizedMessage)
} catch (e1: InvalidKeyException) {
Log.d(NotificationWorker.TAG, "Invalid private key " + e1.localizedMessage)
}
} catch (exception: Exception) {
Log.d(NotificationWorker.TAG, "Something went very wrong " + exception.localizedMessage)
}
}
}

View File

@ -50,6 +50,7 @@
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/> <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission <uses-permission
android:name="android.permission.USE_CREDENTIALS" android:name="android.permission.USE_CREDENTIALS"

View File

@ -256,7 +256,7 @@ public class CallNotificationController extends BaseController {
@Override @Override
public void onNext(RoomsOverall roomsOverall) { public void onNext(RoomsOverall roomsOverall) {
for (Conversation conversation : roomsOverall.getOcs().getData()) { for (Conversation conversation : roomsOverall.getOcs().getData()) {
if (roomId.equals(conversation.getRoomId())) { if (roomId.equals(conversation.getRoomId()) || roomId.equals(conversation.token)) {
currentConversation = conversation; currentConversation = conversation;
runAllThings(); runAllThings();
break; break;

View File

@ -343,21 +343,43 @@ public class NotificationWorker extends Worker {
groupName);*/ groupName);*/
if (decryptedPushMessage.getType().equals("chat") || decryptedPushMessage.getType().equals("room")) { if (decryptedPushMessage.getType().equals("chat") || decryptedPushMessage.getType().equals("room")) {
AudioAttributes.Builder audioAttributesBuilder = new AudioAttributes.Builder().setContentType
(AudioAttributes.CONTENT_TYPE_SONIFICATION);
audioAttributesBuilder.setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT);
String ringtonePreferencesString;
Uri soundUri;
ringtonePreferencesString = appPreferences.getMessageRingtoneUri();
if (TextUtils.isEmpty(ringtonePreferencesString)) {
soundUri = Uri.parse("android.resource://" + context.getPackageName() +
"/raw/librem_by_feandesign_message");
} else {
try {
RingtoneSettings ringtoneSettings = LoganSquare.parse
(ringtonePreferencesString, RingtoneSettings.class);
soundUri = ringtoneSettings.getRingtoneUri();
} catch (IOException exception) {
soundUri = Uri.parse("android.resource://" + context.getPackageName() +
"/raw/librem_by_feandesign_message");
}
}
NotificationUtils.INSTANCE.createNotificationChannel(context, NotificationUtils.INSTANCE.createNotificationChannel(context,
NotificationUtils.INSTANCE.getNOTIFICATION_CHANNEL_MESSAGES_V3(), context.getResources() NotificationUtils.INSTANCE.getNOTIFICATION_CHANNEL_MESSAGES_V3(), context.getResources()
.getString(R.string.nc_notification_channel_messages), context.getResources() .getString(R.string.nc_notification_channel_messages), context.getResources()
.getString(R.string.nc_notification_channel_messages), true, .getString(R.string.nc_notification_channel_messages), true,
NotificationManager.IMPORTANCE_HIGH); NotificationManager.IMPORTANCE_HIGH, soundUri, audioAttributesBuilder.build());
notificationBuilder.setChannelId(NotificationUtils.INSTANCE.getNOTIFICATION_CHANNEL_MESSAGES_V3()); notificationBuilder.setChannelId(NotificationUtils.INSTANCE.getNOTIFICATION_CHANNEL_MESSAGES_V3());
} else { } else {
NotificationUtils.INSTANCE.createNotificationChannel(context, /*NotificationUtils.INSTANCE.createNotificationChannel(context,
NotificationUtils.INSTANCE.getNOTIFICATION_CHANNEL_CALLS_V3(), context.getResources() NotificationUtils.INSTANCE.getNOTIFICATION_CHANNEL_CALLS_V3(), context.getResources()
.getString(R.string.nc_notification_channel_calls), context.getResources() .getString(R.string.nc_notification_channel_calls), context.getResources()
.getString(R.string.nc_notification_channel_calls_description), true, .getString(R.string.nc_notification_channel_calls_description), true,
NotificationManager.IMPORTANCE_HIGH); NotificationManager.IMPORTANCE_HIGH);
notificationBuilder.setChannelId(NotificationUtils.INSTANCE.getNOTIFICATION_CHANNEL_CALLS_V3()); notificationBuilder.setChannelId(NotificationUtils.INSTANCE.getNOTIFICATION_CHANNEL_CALLS_V3());*/
} }
} else { } else {

View File

@ -34,7 +34,7 @@ import org.parceler.Parcel;
public class RingtoneSettings { public class RingtoneSettings {
@JsonField(name = "ringtoneUri", typeConverter = UriTypeConverter.class) @JsonField(name = "ringtoneUri", typeConverter = UriTypeConverter.class)
@Nullable @Nullable
Uri ringtoneUri; public Uri ringtoneUri;
@JsonField(name = "ringtoneName") @JsonField(name = "ringtoneName")
String ringtoneName; public String ringtoneName;
} }

View File

@ -31,32 +31,32 @@ import org.parceler.Parcel;
@JsonObject @JsonObject
public class DecryptedPushMessage { public class DecryptedPushMessage {
@JsonField(name = "app") @JsonField(name = "app")
String app; public String app;
@JsonField(name = "type") @JsonField(name = "type")
String type; public String type;
@JsonField(name = "subject") @JsonField(name = "subject")
String subject; public String subject;
@JsonField(name = "id") @JsonField(name = "id")
String id; public String id;
@JsonField(name = "nid") @JsonField(name = "nid")
long notificationId; public long notificationId;
@JsonField(name = "delete") @JsonField(name = "delete")
boolean delete; public boolean delete;
@JsonField(name = "delete-all") @JsonField(name = "delete-all")
boolean deleteAll; public boolean deleteAll;
@JsonIgnore @JsonIgnore
NotificationUser notificationUser; public NotificationUser notificationUser;
@JsonIgnore @JsonIgnore
String text; public String text;
@JsonIgnore @JsonIgnore
long timestamp; public long timestamp;
} }

View File

@ -26,6 +26,8 @@ import android.app.NotificationChannel
import android.app.NotificationChannelGroup import android.app.NotificationChannelGroup
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.media.AudioAttributes
import android.net.Uri
import android.os.Build import android.os.Build
import android.service.notification.StatusBarNotification import android.service.notification.StatusBarNotification
import com.nextcloud.talk.R import com.nextcloud.talk.R
@ -44,7 +46,7 @@ object NotificationUtils {
fun createNotificationChannel(context: Context, fun createNotificationChannel(context: Context,
channelId: String, channelName: String, channelId: String, channelName: String,
channelDescription: String, enableLights: Boolean, channelDescription: String, enableLights: Boolean,
importance: Int) { importance: Int, sound: Uri, audioAttributes: AudioAttributes) {
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@ -56,7 +58,8 @@ object NotificationUtils {
channel.description = channelDescription channel.description = channelDescription
channel.enableLights(enableLights) channel.enableLights(enableLights)
channel.lightColor = R.color.colorPrimary channel.lightColor = R.color.colorPrimary
channel.setSound(null, null) channel.setSound(sound, audioAttributes)
channel.shouldVibrate()
notificationManager.createNotificationChannel(channel) notificationManager.createNotificationChannel(channel)
} }