Merge branch 'refactor/1080_move-fcm-intent-service-code-to-worker-and-add-tests' into 'develop'
Replace FcmIntentService with ProcessPushNotificationDataWorker See merge request android/mail/proton-mail-android!259
This commit is contained in:
commit
dd62652748
|
@ -60,13 +60,14 @@ class UserCrypto(
|
|||
override fun decrypt(message: CipherText): TextDecryptionResult =
|
||||
decrypt(message, getVerificationKeys(), openPgp.time)
|
||||
|
||||
fun decryptMessage(message: CipherText): TextDecryptionResult {
|
||||
fun decryptMessage(message: String): TextDecryptionResult {
|
||||
val errorMessage = "Error decrypting message, invalid passphrase"
|
||||
checkNotNull(mailboxPassword) { errorMessage }
|
||||
|
||||
return withCurrentKeys("Error decrypting message") { key ->
|
||||
val cipherText = CipherText(message)
|
||||
val unarmored = Armor.unarmor(key.privateKey.string)
|
||||
val decrypted = openPgp.decryptMessageBinKey(message.armored, unarmored, mailboxPassword)
|
||||
val decrypted = openPgp.decryptMessageBinKey(cipherText.armored, unarmored, mailboxPassword)
|
||||
TextDecryptionResult(decrypted, false, false)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
package ch.protonmail.android.di
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.net.ConnectivityManager
|
||||
|
@ -33,6 +34,7 @@ import ch.protonmail.android.api.models.doh.Proxies
|
|||
import ch.protonmail.android.api.models.factories.IConverterFactory
|
||||
import ch.protonmail.android.api.models.messages.receive.ServerLabel
|
||||
import ch.protonmail.android.api.models.room.contacts.ContactLabel
|
||||
import ch.protonmail.android.api.segments.event.AlarmReceiver
|
||||
import ch.protonmail.android.core.Constants
|
||||
import ch.protonmail.android.core.PREF_USERNAME
|
||||
import ch.protonmail.android.core.ProtonMailApplication
|
||||
|
@ -73,6 +75,9 @@ object ApplicationModule {
|
|||
fun protonMailApplication(context: Context): ProtonMailApplication =
|
||||
context.app
|
||||
|
||||
@Provides
|
||||
fun alarmReceiver() = AlarmReceiver()
|
||||
|
||||
@Provides
|
||||
@AlternativeApiPins
|
||||
fun alternativeApiPins() = listOf(
|
||||
|
@ -145,6 +150,11 @@ object ApplicationModule {
|
|||
userManager: UserManager
|
||||
) = userManager.mailSettings
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun notificationManager(context: Context): NotificationManager =
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun protonRetrofitBuilder(
|
||||
|
|
|
@ -1,327 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Proton Technologies AG
|
||||
*
|
||||
* This file is part of ProtonMail.
|
||||
*
|
||||
* ProtonMail 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.
|
||||
*
|
||||
* ProtonMail 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 ProtonMail. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
package ch.protonmail.android.fcm;
|
||||
|
||||
import android.app.IntentService;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import ch.protonmail.android.BuildConfig;
|
||||
import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository;
|
||||
import ch.protonmail.android.api.ProtonMailApiManager;
|
||||
import ch.protonmail.android.api.local.SnoozeSettings;
|
||||
import ch.protonmail.android.api.models.DatabaseProvider;
|
||||
import ch.protonmail.android.api.models.User;
|
||||
import ch.protonmail.android.api.models.messages.receive.MessageResponse;
|
||||
import ch.protonmail.android.api.models.messages.receive.MessagesResponse;
|
||||
import ch.protonmail.android.api.models.room.messages.Message;
|
||||
import ch.protonmail.android.api.models.room.notifications.Notification;
|
||||
import ch.protonmail.android.api.models.room.notifications.NotificationsDatabase;
|
||||
import ch.protonmail.android.api.segments.event.AlarmReceiver;
|
||||
import ch.protonmail.android.core.QueueNetworkUtil;
|
||||
import ch.protonmail.android.core.UserManager;
|
||||
import ch.protonmail.android.crypto.CipherText;
|
||||
import ch.protonmail.android.crypto.Crypto;
|
||||
import ch.protonmail.android.crypto.UserCrypto;
|
||||
import ch.protonmail.android.fcm.models.NotificationData;
|
||||
import ch.protonmail.android.fcm.models.NotificationEncryptedData;
|
||||
import ch.protonmail.android.fcm.models.NotificationSender;
|
||||
import ch.protonmail.android.servers.notification.INotificationServer;
|
||||
import ch.protonmail.android.servers.notification.NotificationServer;
|
||||
import ch.protonmail.android.utils.AppUtil;
|
||||
import ch.protonmail.android.utils.Logger;
|
||||
import ch.protonmail.android.utils.crypto.TextDecryptionResult;
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
import io.sentry.event.EventBuilder;
|
||||
import timber.log.Timber;
|
||||
|
||||
@AndroidEntryPoint
|
||||
public class FcmIntentService extends IntentService {
|
||||
|
||||
private static final String TAG_FCM_INTENT_SERVICE = "FcmIntentService";
|
||||
|
||||
public static final String EXTRA_READ = "CMD_READ";
|
||||
private static final String EXTRA_ENCRYPTED_DATA = "encryptedMessage";
|
||||
private static final String EXTRA_UID = "UID";
|
||||
|
||||
@Inject
|
||||
ProtonMailApiManager mApi;
|
||||
@Inject
|
||||
UserManager mUserManager;
|
||||
@Inject
|
||||
QueueNetworkUtil mNetworkUtils;
|
||||
@Inject
|
||||
MessageDetailsRepository messageDetailsRepository;
|
||||
@Inject
|
||||
DatabaseProvider databaseProvider;
|
||||
|
||||
private NotificationsDatabase notificationsDatabase;
|
||||
private INotificationServer notificationServer;
|
||||
|
||||
public FcmIntentService() {
|
||||
super("FCM");
|
||||
setIntentRedelivery(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationServer = new NotificationServer(this, notificationManager);
|
||||
}
|
||||
|
||||
private void startMeForeground() {
|
||||
final int messageId = (int) System.currentTimeMillis();
|
||||
final android.app.Notification notification = notificationServer.createCheckingMailboxNotification();
|
||||
startForeground(messageId, notification);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(Intent intent) {
|
||||
startMeForeground();
|
||||
|
||||
final Bundle extras = intent.getExtras();
|
||||
if (extras != null && !extras.isEmpty()) {
|
||||
if (!extras.containsKey("CMD")) {
|
||||
// we are always registering for push in MailboxActivity
|
||||
|
||||
boolean isAppInBackground = AppUtil.isAppInBackground();
|
||||
if (!isAppInBackground) {
|
||||
AlarmReceiver alarmReceiver = new AlarmReceiver();
|
||||
alarmReceiver.setAlarm(this, true);
|
||||
}
|
||||
|
||||
mNetworkUtils.setCurrentlyHasConnectivity(true);
|
||||
NotificationData notificationData = null;
|
||||
NotificationEncryptedData messageData = null;
|
||||
String sessionId = "";
|
||||
|
||||
if (extras.containsKey(EXTRA_UID)) {
|
||||
sessionId = extras.getString(EXTRA_UID, "");
|
||||
}
|
||||
|
||||
String notificationUsername = mUserManager.getUsernameBySessionId(sessionId);
|
||||
if (TextUtils.isEmpty(notificationUsername)) {
|
||||
// we do not show notifications for unknown/inactive users
|
||||
return;
|
||||
}
|
||||
|
||||
User user = mUserManager.getUser(notificationUsername);
|
||||
notificationsDatabase = databaseProvider.provideNotificationsDao(notificationUsername);
|
||||
if (!user.isBackgroundSync()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (extras.containsKey(EXTRA_ENCRYPTED_DATA)) {
|
||||
String encryptedStr = extras.getString(EXTRA_ENCRYPTED_DATA);
|
||||
UserCrypto crypto = Crypto.forUser(mUserManager, notificationUsername);
|
||||
TextDecryptionResult textDecryptionResult = crypto.decryptMessage(new CipherText(encryptedStr));
|
||||
String decryptedStr = textDecryptionResult.getDecryptedData();
|
||||
notificationData = tryParseNotificationModel(decryptedStr);
|
||||
messageData = notificationData.getData();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// can not deliver notification
|
||||
if (!BuildConfig.DEBUG) {
|
||||
EventBuilder eventBuilder = new EventBuilder().withTag("FCM_MU", TextUtils.isEmpty(notificationUsername) ? "EMPTY" : "NOT_EMPTY");
|
||||
Timber.e(e, eventBuilder.toString());
|
||||
}
|
||||
}
|
||||
|
||||
if (notificationData == null || messageData == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final String messageId = messageData.getMessageId();
|
||||
final String notificationBody = messageData.getBody();
|
||||
NotificationSender notificationSender = messageData.getSender();
|
||||
String sender = notificationSender.getSenderName();
|
||||
if (TextUtils.isEmpty(sender)) {
|
||||
sender = notificationSender.getSenderEmail();
|
||||
}
|
||||
boolean primaryUser = mUserManager.getUsername().equals(notificationUsername);
|
||||
if (extras.containsKey(EXTRA_READ) && extras.getBoolean(EXTRA_READ)) {
|
||||
removeNotification(user, messageId, primaryUser);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isQuickSnoozeEnabled = mUserManager.isSnoozeQuickEnabled();
|
||||
boolean isScheduledSnoozeEnabled = mUserManager.isSnoozeScheduledEnabled();
|
||||
if (!isQuickSnoozeEnabled && (!isScheduledSnoozeEnabled || shouldShowNotificationWhileScheduledSnooze(user))) {
|
||||
sendNotification(user, messageId, notificationBody, sender, primaryUser);
|
||||
}
|
||||
}
|
||||
}
|
||||
stopForeground(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the Notification with the given id and eventually recreate a notification with other
|
||||
* unread Notifications from database
|
||||
*
|
||||
* @param user current logged {@link User}
|
||||
* @param messageId String id of {@link Message} for delete relative {@link Notification}
|
||||
*/
|
||||
private void removeNotification(final User user,
|
||||
final String messageId,
|
||||
final boolean primaryUser) {
|
||||
NotificationManager notificationManager =
|
||||
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
// Cancel all the Status Bar Notifications
|
||||
notificationManager.cancelAll();
|
||||
|
||||
// Remove the Notification from Database
|
||||
notificationsDatabase.deleteByMessageId(messageId);
|
||||
List<Notification> notifications = notificationsDatabase.findAllNotifications();
|
||||
|
||||
// Return if there are no more unreadNotifications
|
||||
if (notifications.isEmpty()) return;
|
||||
|
||||
Message message = fetchMessage(user, messageId);
|
||||
if (notifications.size() > 1) {
|
||||
notificationServer.notifyMultipleUnreadEmail(mUserManager, user, notifications);
|
||||
} else {
|
||||
Notification notification = notifications.get(0);
|
||||
notificationServer.notifySingleNewEmail(
|
||||
mUserManager, user, message, messageId,
|
||||
notification.getNotificationBody(),
|
||||
notification.getNotificationTitle(), primaryUser
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a Notification for a new email received.
|
||||
*
|
||||
* @param user current logged {@link User}
|
||||
* @param messageId String id for retrieve the {@link Message} details
|
||||
* @param notificationBody String body of the Notification
|
||||
* @param sender String name of the sender of the email
|
||||
*/
|
||||
private void sendNotification(
|
||||
final User user,
|
||||
final String messageId,
|
||||
@Nullable final String notificationBody,
|
||||
final String sender,
|
||||
final boolean primaryUser
|
||||
) {
|
||||
|
||||
// Insert current Notification in Database
|
||||
Notification notification = new Notification(messageId, sender, notificationBody != null ? notificationBody : "");
|
||||
notificationsDatabase.insertNotification(notification);
|
||||
|
||||
List<Notification> notifications = notificationsDatabase.findAllNotifications();
|
||||
|
||||
Message message = fetchMessage(user, messageId);
|
||||
if (notifications.size() > 1) {
|
||||
notificationServer.notifyMultipleUnreadEmail(mUserManager, user, notifications);
|
||||
} else {
|
||||
notificationServer.notifySingleNewEmail(
|
||||
mUserManager, user, message, messageId, notificationBody, sender, primaryUser
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private Message fetchMessage(final User user, final String messageId) {
|
||||
// Fetch message details if required by the current config
|
||||
boolean fetchMessageDetails = user.isGcmDownloadMessageDetails();
|
||||
Message message;
|
||||
if (fetchMessageDetails) {
|
||||
message = fetchMessageDetails(messageId);
|
||||
} else {
|
||||
message = fetchMessageMetadata(messageId);
|
||||
}
|
||||
if (message == null) {
|
||||
// try to find the message in the local storage, maybe it was received from the event
|
||||
message = messageDetailsRepository.findMessageById(messageId);
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private Message fetchMessageMetadata(final String messageId) {
|
||||
Message message = null;
|
||||
try {
|
||||
MessagesResponse messageResponse = mApi.fetchSingleMessageMetadata(messageId);
|
||||
if (messageResponse != null) {
|
||||
List<Message> messages = messageResponse.getMessages();
|
||||
if (messages.size() > 0) {
|
||||
message = messages.get(0);
|
||||
}
|
||||
if (message != null) {
|
||||
Message savedMessage = messageDetailsRepository.findMessageById(message.getMessageId());
|
||||
if (savedMessage != null) {
|
||||
message.setInline(savedMessage.isInline());
|
||||
}
|
||||
message.setDownloaded(false);
|
||||
messageDetailsRepository.saveMessageInDB(message);
|
||||
} else {
|
||||
// check if the message is already in local store
|
||||
message = messageDetailsRepository.findMessageById(messageId);
|
||||
}
|
||||
}
|
||||
} catch (Exception error) {
|
||||
Logger.doLogException(TAG_FCM_INTENT_SERVICE, "error while fetching message detail", error);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
private Message fetchMessageDetails(final String messageId) {
|
||||
Message message = null;
|
||||
try {
|
||||
MessageResponse messageResponse = mApi.messageDetail(messageId);
|
||||
message = messageResponse.getMessage();
|
||||
Message savedMessage = messageDetailsRepository.findMessageById(messageId);
|
||||
if (savedMessage != null) {
|
||||
message.setInline(savedMessage.isInline());
|
||||
}
|
||||
message.setDownloaded(true);
|
||||
messageDetailsRepository.saveMessageInDB(message);
|
||||
} catch (Exception error) {
|
||||
Logger.doLogException(TAG_FCM_INTENT_SERVICE, "error while fetching message detail", error);
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private boolean shouldShowNotificationWhileScheduledSnooze(User user) {
|
||||
Calendar rightNow = Calendar.getInstance();
|
||||
SnoozeSettings snoozeSettings = mUserManager.getSnoozeSettings();
|
||||
return !snoozeSettings.shouldSuppressNotification(rightNow);
|
||||
}
|
||||
|
||||
private NotificationData tryParseNotificationModel(String decryptedStr) {
|
||||
Gson gson = new Gson();
|
||||
return gson.fromJson(decryptedStr, NotificationData.class);
|
||||
}
|
||||
}
|
|
@ -19,10 +19,8 @@
|
|||
|
||||
package ch.protonmail.android.fcm
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import ch.protonmail.android.R
|
||||
import com.google.firebase.messaging.FirebaseMessagingService
|
||||
|
@ -39,6 +37,8 @@ public class PMFirebaseMessagingService : FirebaseMessagingService() {
|
|||
|
||||
@Inject
|
||||
lateinit var pmRegistrationWorkerEnqueuer: PMRegistrationWorker.Enqueuer
|
||||
@Inject
|
||||
lateinit var processPushNotificationData: ProcessPushNotificationDataWorker.Enqueuer
|
||||
|
||||
override fun onNewToken(token: String) {
|
||||
super.onNewToken(token)
|
||||
|
@ -60,9 +60,7 @@ public class PMFirebaseMessagingService : FirebaseMessagingService() {
|
|||
broadcastIntent.putExtras(bundle)
|
||||
broadcastIntent.action = baseContext.getString(R.string.action_notification)
|
||||
if (!LocalBroadcastManager.getInstance(baseContext).sendBroadcast(broadcastIntent)) {
|
||||
val serviceIntent = Intent(broadcastIntent)
|
||||
val comp = ComponentName(baseContext.packageName, FcmIntentService::class.java.name)
|
||||
ContextCompat.startForegroundService(baseContext, serviceIntent.setComponent(comp))
|
||||
processPushNotificationData(remoteMessage.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,245 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Proton Technologies AG
|
||||
*
|
||||
* This file is part of ProtonMail.
|
||||
*
|
||||
* ProtonMail 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.
|
||||
*
|
||||
* ProtonMail 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 ProtonMail. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package ch.protonmail.android.fcm
|
||||
|
||||
import android.content.Context
|
||||
import androidx.hilt.Assisted
|
||||
import androidx.hilt.work.WorkerInject
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.Data
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.workDataOf
|
||||
import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository
|
||||
import ch.protonmail.android.api.ProtonMailApiManager
|
||||
import ch.protonmail.android.api.models.DatabaseProvider
|
||||
import ch.protonmail.android.api.models.User
|
||||
import ch.protonmail.android.api.models.messages.receive.MessageResponse
|
||||
import ch.protonmail.android.api.models.room.messages.Message
|
||||
import ch.protonmail.android.api.models.room.notifications.Notification
|
||||
import ch.protonmail.android.api.segments.event.AlarmReceiver
|
||||
import ch.protonmail.android.core.QueueNetworkUtil
|
||||
import ch.protonmail.android.core.UserManager
|
||||
import ch.protonmail.android.crypto.UserCrypto
|
||||
import ch.protonmail.android.domain.entity.Name
|
||||
import ch.protonmail.android.fcm.models.PushNotification
|
||||
import ch.protonmail.android.fcm.models.PushNotificationData
|
||||
import ch.protonmail.android.servers.notification.NotificationServer
|
||||
import ch.protonmail.android.utils.AppUtil
|
||||
import me.proton.core.util.kotlin.EMPTY_STRING
|
||||
import me.proton.core.util.kotlin.deserialize
|
||||
import timber.log.Timber
|
||||
import java.util.Calendar
|
||||
import javax.inject.Inject
|
||||
|
||||
const val KEY_PUSH_NOTIFICATION_UID = "UID"
|
||||
const val KEY_PUSH_NOTIFICATION_ENCRYPTED_MESSAGE = "encryptedMessage"
|
||||
const val KEY_PROCESS_PUSH_NOTIFICATION_DATA_ERROR = "ProcessPushNotificationDataError"
|
||||
|
||||
/**
|
||||
* A worker that is responsible for processing the data payload of the received FCM push notifications.
|
||||
*/
|
||||
|
||||
class ProcessPushNotificationDataWorker @WorkerInject constructor(
|
||||
@Assisted context: Context,
|
||||
@Assisted workerParameters: WorkerParameters,
|
||||
private val notificationServer: NotificationServer,
|
||||
private val alarmReceiver: AlarmReceiver,
|
||||
private val queueNetworkUtil: QueueNetworkUtil,
|
||||
private val userManager: UserManager,
|
||||
private val databaseProvider: DatabaseProvider,
|
||||
private val messageDetailsRepository: MessageDetailsRepository,
|
||||
private val protonMailApiManager: ProtonMailApiManager
|
||||
) : CoroutineWorker(context, workerParameters) {
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val sessionId = inputData.getString(KEY_PUSH_NOTIFICATION_UID)
|
||||
val encryptedMessage = inputData.getString(KEY_PUSH_NOTIFICATION_ENCRYPTED_MESSAGE)
|
||||
|
||||
if (sessionId.isNullOrEmpty() || encryptedMessage.isNullOrEmpty()) {
|
||||
return Result.failure(
|
||||
workDataOf(KEY_PROCESS_PUSH_NOTIFICATION_DATA_ERROR to "Input data is missing")
|
||||
)
|
||||
}
|
||||
|
||||
if (!AppUtil.isAppInBackground()) {
|
||||
alarmReceiver.setAlarm(applicationContext, true)
|
||||
}
|
||||
|
||||
queueNetworkUtil.setCurrentlyHasConnectivity(true)
|
||||
|
||||
val notificationUsername = userManager.getUsernameBySessionId(sessionId)
|
||||
if (notificationUsername.isNullOrEmpty()) {
|
||||
// we do not show notifications for unknown/inactive users
|
||||
return Result.failure(workDataOf(KEY_PROCESS_PUSH_NOTIFICATION_DATA_ERROR to "User is unknown or inactive"))
|
||||
}
|
||||
|
||||
val user = userManager.getUser(notificationUsername)
|
||||
if (!user.isBackgroundSync) {
|
||||
// we do not show notifications for users who have disabled background sync
|
||||
return Result.failure(workDataOf(KEY_PROCESS_PUSH_NOTIFICATION_DATA_ERROR to "Background sync is disabled"))
|
||||
}
|
||||
|
||||
var pushNotification: PushNotification? = null
|
||||
var pushNotificationData: PushNotificationData? = null
|
||||
try {
|
||||
val userCrypto = UserCrypto(userManager, userManager.openPgp, Name(notificationUsername))
|
||||
val textDecryptionResult = userCrypto.decryptMessage(encryptedMessage)
|
||||
val decryptedData = textDecryptionResult.decryptedData
|
||||
pushNotification = decryptedData.deserialize()
|
||||
pushNotificationData = pushNotification.data
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error with decryption or deserialization of the notification data")
|
||||
}
|
||||
|
||||
if (pushNotification == null || pushNotificationData == null) {
|
||||
return Result.failure(
|
||||
workDataOf(KEY_PROCESS_PUSH_NOTIFICATION_DATA_ERROR to "Decryption or deserialization error")
|
||||
)
|
||||
}
|
||||
|
||||
val messageId = pushNotificationData.messageId
|
||||
val notificationBody = pushNotificationData.body
|
||||
val notificationSender = pushNotificationData.sender
|
||||
val sender = notificationSender?.let {
|
||||
it.senderName.ifEmpty { it.senderAddress }
|
||||
} ?: EMPTY_STRING
|
||||
|
||||
val primaryUser = userManager.username == notificationUsername
|
||||
val isQuickSnoozeEnabled = userManager.isSnoozeQuickEnabled()
|
||||
val isScheduledSnoozeEnabled = userManager.isSnoozeScheduledEnabled()
|
||||
|
||||
if (!isQuickSnoozeEnabled && (!isScheduledSnoozeEnabled || !shouldSuppressNotification())) {
|
||||
sendNotification(user, messageId, notificationBody, sender, primaryUser)
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
private fun sendNotification(
|
||||
user: User,
|
||||
messageId: String,
|
||||
notificationBody: String,
|
||||
sender: String,
|
||||
primaryUser: Boolean
|
||||
) {
|
||||
|
||||
// Insert current Notification in Database
|
||||
val notificationsDatabase = databaseProvider.provideNotificationsDao(user.username)
|
||||
val notification = Notification(messageId, sender, notificationBody)
|
||||
notificationsDatabase.insertNotification(notification)
|
||||
|
||||
val notifications = notificationsDatabase.findAllNotifications()
|
||||
val message = fetchMessage(user, messageId)
|
||||
|
||||
if (notifications.size > 1) {
|
||||
notificationServer.notifyMultipleUnreadEmail(userManager, user, notifications)
|
||||
} else {
|
||||
notificationServer.notifySingleNewEmail(
|
||||
userManager, user, message, messageId, notificationBody, sender, primaryUser
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchMessage(user: User, messageId: String): Message? {
|
||||
// Fetch message details if required by the current config
|
||||
val fetchMessageDetails = user.isGcmDownloadMessageDetails
|
||||
return if (fetchMessageDetails) {
|
||||
fetchMessageDetails(messageId)
|
||||
} else {
|
||||
fetchMessageMetadata(messageId)
|
||||
}
|
||||
?: messageDetailsRepository.findMessageById(messageId)
|
||||
}
|
||||
|
||||
private fun fetchMessageMetadata(messageId: String): Message? {
|
||||
var message: Message? = null
|
||||
try {
|
||||
val messageResponse = protonMailApiManager.fetchSingleMessageMetadata(messageId)
|
||||
if (messageResponse != null) {
|
||||
val messages = messageResponse.messages
|
||||
if (messages.isNotEmpty()) {
|
||||
message = messages[0]
|
||||
}
|
||||
if (message != null) {
|
||||
val savedMessage = messageDetailsRepository.findMessageById(message.messageId!!)
|
||||
if (savedMessage != null) {
|
||||
message.isInline = savedMessage.isInline
|
||||
}
|
||||
message.isDownloaded = false
|
||||
messageDetailsRepository.saveMessageInDB(message)
|
||||
} else {
|
||||
// check if the message is already in local store
|
||||
message = messageDetailsRepository.findMessageById(messageId)
|
||||
}
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
Timber.e(error, "error while fetching message metadata in ProcessPushNotificationDataWorker")
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
private fun fetchMessageDetails(messageId: String): Message? {
|
||||
var message: Message? = null
|
||||
try {
|
||||
val messageResponse: MessageResponse = protonMailApiManager.messageDetail(messageId)
|
||||
message = messageResponse.message
|
||||
val savedMessage = messageDetailsRepository.findMessageById(messageId)
|
||||
if (savedMessage != null) {
|
||||
message.isInline = savedMessage.isInline
|
||||
}
|
||||
message.isDownloaded = true
|
||||
messageDetailsRepository.saveMessageInDB(message)
|
||||
} catch (error: Exception) {
|
||||
Timber.e(error, "error while fetching message detail in ProcessPushNotificationDataWorker")
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
private fun shouldSuppressNotification(): Boolean {
|
||||
val rightNow = Calendar.getInstance()
|
||||
return userManager.snoozeSettings?.shouldSuppressNotification(rightNow) ?: false
|
||||
}
|
||||
|
||||
class Enqueuer @Inject constructor(
|
||||
private val workManager: WorkManager
|
||||
) {
|
||||
|
||||
operator fun invoke(pushNotificationData: Map<String, String>) {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
|
||||
val inputData = Data.Builder()
|
||||
.putAll(pushNotificationData)
|
||||
.build()
|
||||
|
||||
val workRequest = OneTimeWorkRequestBuilder<ProcessPushNotificationDataWorker>()
|
||||
.setConstraints(constraints)
|
||||
.setInputData(inputData)
|
||||
.build()
|
||||
|
||||
workManager.enqueue(workRequest)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Proton Technologies AG
|
||||
*
|
||||
* This file is part of ProtonMail.
|
||||
*
|
||||
* ProtonMail 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.
|
||||
*
|
||||
* ProtonMail 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 ProtonMail. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
package ch.protonmail.android.fcm.models
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class NotificationEncryptedData(
|
||||
@SerializedName("title") val title: String? = null,
|
||||
@SerializedName("subtitle") val subtitle: String? = null,
|
||||
@SerializedName("body") val body: String? = null,
|
||||
@SerializedName("vibrate") val vibrate: Int = 0,
|
||||
@SerializedName("sound") val sound: Int = 0,
|
||||
@SerializedName("largeIcon") val largeIcon: String? = null,
|
||||
@SerializedName("smallIcon") val smallIcon: String? = null,
|
||||
@SerializedName("badge") val badge: Int = 0,
|
||||
@SerializedName("messageId") val messageId: String? = null,
|
||||
@SerializedName("customId") val customId: String? = null,
|
||||
@SerializedName("sender") val sender: NotificationSender? = null
|
||||
)
|
|
@ -18,9 +18,12 @@
|
|||
*/
|
||||
package ch.protonmail.android.fcm.models
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
data class NotificationSender(
|
||||
@SerializedName("Address") val senderEmail: String? = null,
|
||||
@SerializedName("Name") val senderName: String? = null
|
||||
)
|
||||
@Serializable
|
||||
data class PushNotification(
|
||||
@SerialName("type") val type: String,
|
||||
@SerialName("version") val version: Int,
|
||||
@SerialName("data") val data: PushNotificationData?
|
||||
)
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Proton Technologies AG
|
||||
*
|
||||
* This file is part of ProtonMail.
|
||||
*
|
||||
* ProtonMail 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.
|
||||
*
|
||||
* ProtonMail 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 ProtonMail. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
package ch.protonmail.android.fcm.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class PushNotificationData(
|
||||
@SerialName("title") val title: String,
|
||||
@SerialName("subtitle") val subtitle: String,
|
||||
@SerialName("body") val body: String,
|
||||
@SerialName("vibrate") val vibrate: Int,
|
||||
@SerialName("sound") val sound: Int,
|
||||
@SerialName("largeIcon") val largeIcon: String,
|
||||
@SerialName("smallIcon") val smallIcon: String,
|
||||
@SerialName("badge") val badge: Int,
|
||||
@SerialName("messageId") val messageId: String,
|
||||
@SerialName("customId") val customId: String,
|
||||
@SerialName("sender") val sender: PushNotificationSender?
|
||||
)
|
|
@ -18,10 +18,12 @@
|
|||
*/
|
||||
package ch.protonmail.android.fcm.models
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
data class NotificationData(
|
||||
@SerializedName("type") val type: String? = null,
|
||||
@SerializedName("version") val version: Int = 0,
|
||||
@SerializedName("data") val data: NotificationEncryptedData? = null
|
||||
)
|
||||
@Serializable
|
||||
data class PushNotificationSender(
|
||||
@SerialName("Address") val senderAddress: String,
|
||||
@SerialName("Name") val senderName: String,
|
||||
@SerialName("Group") val senderGroup: String
|
||||
)
|
|
@ -42,7 +42,6 @@ import ch.protonmail.android.R
|
|||
import ch.protonmail.android.activities.EXTRA_SWITCHED_TO_USER
|
||||
import ch.protonmail.android.activities.EXTRA_SWITCHED_USER
|
||||
import ch.protonmail.android.activities.composeMessage.ComposeMessageActivity
|
||||
import ch.protonmail.android.activities.guest.LoginActivity
|
||||
import ch.protonmail.android.activities.mailbox.MailboxActivity
|
||||
import ch.protonmail.android.activities.messageDetails.MessageDetailsActivity
|
||||
import ch.protonmail.android.api.models.User
|
||||
|
@ -57,6 +56,7 @@ import ch.protonmail.android.utils.buildReplyIntent
|
|||
import ch.protonmail.android.utils.buildTrashIntent
|
||||
import ch.protonmail.android.utils.extensions.getColorCompat
|
||||
import ch.protonmail.android.utils.extensions.showToast
|
||||
import javax.inject.Inject
|
||||
import ch.protonmail.android.api.models.room.notifications.Notification as RoomNotification
|
||||
|
||||
// region constants
|
||||
|
@ -73,10 +73,10 @@ const val EXTRA_USERNAME = "username"
|
|||
// endregion
|
||||
|
||||
/**
|
||||
* Created by Kamil Rajtar on 13.07.18.
|
||||
* A class that is responsible for creating notification channels, and creating and showing notifications.
|
||||
*/
|
||||
|
||||
class NotificationServer(
|
||||
class NotificationServer @Inject constructor(
|
||||
private val context: Context,
|
||||
private val notificationManager: NotificationManager
|
||||
) : INotificationServer {
|
||||
|
@ -162,11 +162,13 @@ class NotificationServer(
|
|||
return CHANNEL_ID_ONGOING_OPS
|
||||
}
|
||||
|
||||
override fun notifyVerificationNeeded(user: User?,
|
||||
messageTitle: String,
|
||||
messageId: String,
|
||||
messageInline: Boolean,
|
||||
messageAddressId: String) {
|
||||
override fun notifyVerificationNeeded(
|
||||
user: User?,
|
||||
messageTitle: String,
|
||||
messageId: String,
|
||||
messageInline: Boolean,
|
||||
messageAddressId: String
|
||||
) {
|
||||
val inboxStyle = NotificationCompat.BigTextStyle()
|
||||
inboxStyle.setBigContentTitle(context.getString(R.string.verification_needed))
|
||||
inboxStyle.bigText(String.format(
|
||||
|
@ -228,8 +230,8 @@ class NotificationServer(
|
|||
|
||||
val channelId = createAccountChannel()
|
||||
|
||||
val mBuilder = NotificationCompat.Builder(context,
|
||||
channelId).setSmallIcon(R.drawable.notification_icon)
|
||||
val mBuilder = NotificationCompat.Builder(context, channelId)
|
||||
.setSmallIcon(R.drawable.notification_icon)
|
||||
.setColor(ContextCompat.getColor(context, R.color.ocean_blue))
|
||||
.setStyle(inboxStyle)
|
||||
.setLights(ContextCompat.getColor(context, R.color.light_indicator),
|
||||
|
@ -241,8 +243,12 @@ class NotificationServer(
|
|||
notificationManager.notify(3, notification)
|
||||
}
|
||||
|
||||
override fun notifyAboutAttachment(filename: String, uri: Uri,
|
||||
mimeType: String?, showNotification: Boolean) {
|
||||
override fun notifyAboutAttachment(
|
||||
filename: String,
|
||||
uri: Uri,
|
||||
mimeType: String?,
|
||||
showNotification: Boolean
|
||||
) {
|
||||
val channelId = createAttachmentsChannel()
|
||||
val mBuilder = NotificationCompat.Builder(context, channelId)
|
||||
|
||||
|
@ -560,4 +566,3 @@ class NotificationServer(
|
|||
notificationManager.notify(user.username.hashCode() + NOTIFICATION_ID_SENDING_FAILED, notification)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,463 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Proton Technologies AG
|
||||
*
|
||||
* This file is part of ProtonMail.
|
||||
*
|
||||
* ProtonMail 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.
|
||||
*
|
||||
* ProtonMail 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 ProtonMail. If not, see https://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
package ch.protonmail.android.fcm
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.ListenableWorker
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.workDataOf
|
||||
import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository
|
||||
import ch.protonmail.android.api.ProtonMailApiManager
|
||||
import ch.protonmail.android.api.models.DatabaseProvider
|
||||
import ch.protonmail.android.api.models.User
|
||||
import ch.protonmail.android.api.models.room.messages.Message
|
||||
import ch.protonmail.android.api.models.room.notifications.Notification
|
||||
import ch.protonmail.android.api.segments.event.AlarmReceiver
|
||||
import ch.protonmail.android.core.QueueNetworkUtil
|
||||
import ch.protonmail.android.core.UserManager
|
||||
import ch.protonmail.android.crypto.UserCrypto
|
||||
import ch.protonmail.android.fcm.models.PushNotification
|
||||
import ch.protonmail.android.fcm.models.PushNotificationData
|
||||
import ch.protonmail.android.fcm.models.PushNotificationSender
|
||||
import ch.protonmail.android.servers.notification.NotificationServer
|
||||
import ch.protonmail.android.utils.AppUtil
|
||||
import me.proton.core.util.kotlin.deserialize
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.impl.annotations.RelaxedMockK
|
||||
import io.mockk.just
|
||||
import io.mockk.justRun
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkConstructor
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.runs
|
||||
import io.mockk.slot
|
||||
import io.mockk.spyk
|
||||
import io.mockk.unmockkConstructor
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import io.mockk.verifyOrder
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* Tests the functionality of [ProcessPushNotificationDataWorker].
|
||||
*/
|
||||
|
||||
class ProcessPushNotificationDataWorkerTest {
|
||||
|
||||
@RelaxedMockK
|
||||
private lateinit var context: Context
|
||||
@RelaxedMockK
|
||||
private lateinit var userManager: UserManager
|
||||
@RelaxedMockK
|
||||
private lateinit var workerParameters: WorkerParameters
|
||||
@RelaxedMockK
|
||||
private lateinit var workManager: WorkManager
|
||||
|
||||
@MockK
|
||||
private lateinit var alarmReceiver: AlarmReceiver
|
||||
@MockK
|
||||
private lateinit var databaseProvider: DatabaseProvider
|
||||
@MockK
|
||||
private lateinit var messageDetailsRepository: MessageDetailsRepository
|
||||
@MockK
|
||||
private lateinit var notificationServer: NotificationServer
|
||||
@MockK
|
||||
private lateinit var protonMailApiManager: ProtonMailApiManager
|
||||
@MockK
|
||||
private lateinit var queueNetworkUtil: QueueNetworkUtil
|
||||
|
||||
private lateinit var processPushNotificationDataWorker: ProcessPushNotificationDataWorker
|
||||
private lateinit var processPushNotificationDataWorkerEnqueuer: ProcessPushNotificationDataWorker.Enqueuer
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockKAnnotations.init(this, relaxUnitFun = true)
|
||||
|
||||
mockkConstructor(UserCrypto::class)
|
||||
mockkStatic(AppUtil::class)
|
||||
mockkStatic("me.proton.core.util.kotlin.SerializationUtilsKt")
|
||||
|
||||
processPushNotificationDataWorker = spyk(
|
||||
ProcessPushNotificationDataWorker(
|
||||
context,
|
||||
workerParameters,
|
||||
notificationServer,
|
||||
alarmReceiver,
|
||||
queueNetworkUtil,
|
||||
userManager,
|
||||
databaseProvider,
|
||||
messageDetailsRepository,
|
||||
protonMailApiManager
|
||||
),
|
||||
recordPrivateCalls = true
|
||||
)
|
||||
processPushNotificationDataWorkerEnqueuer = ProcessPushNotificationDataWorker.Enqueuer(
|
||||
workManager
|
||||
)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unmockkConstructor(UserCrypto::class)
|
||||
unmockkStatic(AppUtil::class)
|
||||
unmockkStatic("me.proton.core.util.kotlin.SerializationUtilsKt")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun verifyWorkIsEnqueuedWhenEnqueuerIsInvoked() {
|
||||
// given
|
||||
val mockPushNotificationData = mockk<Map<String, String>>(relaxed = true)
|
||||
|
||||
// when
|
||||
processPushNotificationDataWorkerEnqueuer(mockPushNotificationData)
|
||||
|
||||
// then
|
||||
verify { workManager.enqueue(any<OneTimeWorkRequest>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun returnFailureIfInputDataIsMissing() {
|
||||
runBlocking {
|
||||
// given
|
||||
every { workerParameters.inputData } returns mockk {
|
||||
every { getString(KEY_PUSH_NOTIFICATION_UID) } returns ""
|
||||
every { getString(KEY_PUSH_NOTIFICATION_ENCRYPTED_MESSAGE) } returns null
|
||||
}
|
||||
val expectedResult = ListenableWorker.Result.failure(
|
||||
workDataOf(KEY_PROCESS_PUSH_NOTIFICATION_DATA_ERROR to "Input data is missing")
|
||||
)
|
||||
|
||||
// when
|
||||
val workResult = processPushNotificationDataWorker.doWork()
|
||||
|
||||
// then
|
||||
assertEquals(expectedResult, workResult)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun verifyAlarmIsSetIfAppIsNotInBackground() {
|
||||
runBlocking {
|
||||
// given
|
||||
every { workerParameters.inputData } returns mockk {
|
||||
every { getString(KEY_PUSH_NOTIFICATION_UID) } returns "uid"
|
||||
every { getString(KEY_PUSH_NOTIFICATION_ENCRYPTED_MESSAGE) } returns "encryptedMessage"
|
||||
}
|
||||
every { AppUtil.isAppInBackground() } returns false
|
||||
|
||||
// when
|
||||
processPushNotificationDataWorker.doWork()
|
||||
|
||||
// then
|
||||
verify { alarmReceiver.setAlarm(any(), true) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun verifySettingHasConnectivityToTrue() {
|
||||
runBlocking {
|
||||
// given
|
||||
every { workerParameters.inputData } returns mockk {
|
||||
every { getString(KEY_PUSH_NOTIFICATION_UID) } returns "uid"
|
||||
every { getString(KEY_PUSH_NOTIFICATION_ENCRYPTED_MESSAGE) } returns "encryptedMessage"
|
||||
}
|
||||
every { AppUtil.isAppInBackground() } returns false
|
||||
|
||||
// when
|
||||
processPushNotificationDataWorker.doWork()
|
||||
|
||||
// then
|
||||
verify { queueNetworkUtil.setCurrentlyHasConnectivity(true) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun returnFailureIfUserIsUnknownOrInactive() {
|
||||
runBlocking {
|
||||
// given
|
||||
every { workerParameters.inputData } returns mockk {
|
||||
every { getString(KEY_PUSH_NOTIFICATION_UID) } returns "uid"
|
||||
every { getString(KEY_PUSH_NOTIFICATION_ENCRYPTED_MESSAGE) } returns "encryptedMessage"
|
||||
}
|
||||
every { AppUtil.isAppInBackground() } returns false
|
||||
every { userManager.getUsernameBySessionId("uid") } returns null
|
||||
|
||||
val expectedResult = ListenableWorker.Result.failure(
|
||||
workDataOf(KEY_PROCESS_PUSH_NOTIFICATION_DATA_ERROR to "User is unknown or inactive")
|
||||
)
|
||||
|
||||
// when
|
||||
val workerResult = processPushNotificationDataWorker.doWork()
|
||||
|
||||
// then
|
||||
assertEquals(expectedResult, workerResult)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun returnFailureIfBackgroundSyncIsDisabled() {
|
||||
runBlocking {
|
||||
// given
|
||||
every { workerParameters.inputData } returns mockk {
|
||||
every { getString(KEY_PUSH_NOTIFICATION_UID) } returns "uid"
|
||||
every { getString(KEY_PUSH_NOTIFICATION_ENCRYPTED_MESSAGE) } returns "encryptedMessage"
|
||||
}
|
||||
every { AppUtil.isAppInBackground() } returns false
|
||||
every { userManager.getUsernameBySessionId("uid") } returns "username"
|
||||
every { userManager.getUser("username") } returns mockk {
|
||||
every { isBackgroundSync } returns false
|
||||
}
|
||||
|
||||
val expectedResult = ListenableWorker.Result.failure(
|
||||
workDataOf(KEY_PROCESS_PUSH_NOTIFICATION_DATA_ERROR to "Background sync is disabled")
|
||||
)
|
||||
|
||||
// when
|
||||
val workerResult = processPushNotificationDataWorker.doWork()
|
||||
|
||||
// then
|
||||
assertEquals(expectedResult, workerResult)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun returnFailureIfDecryptionFails() {
|
||||
runBlocking {
|
||||
// given
|
||||
every { workerParameters.inputData } returns mockk {
|
||||
every { getString(KEY_PUSH_NOTIFICATION_UID) } returns "uid"
|
||||
every { getString(KEY_PUSH_NOTIFICATION_ENCRYPTED_MESSAGE) } returns "encryptedMessage"
|
||||
}
|
||||
every { AppUtil.isAppInBackground() } returns false
|
||||
every { userManager.getUsernameBySessionId("uid") } returns "username"
|
||||
every { userManager.getUser("username") } returns mockk {
|
||||
every { isBackgroundSync } returns true
|
||||
}
|
||||
every { userManager.openPgp } returns mockk(relaxed = true)
|
||||
every { anyConstructed<UserCrypto>().decryptMessage(any()) } throws IllegalStateException()
|
||||
|
||||
val expectedResult = ListenableWorker.Result.failure(
|
||||
workDataOf(KEY_PROCESS_PUSH_NOTIFICATION_DATA_ERROR to "Decryption or deserialization error")
|
||||
)
|
||||
|
||||
// when
|
||||
val workerResult = processPushNotificationDataWorker.doWork()
|
||||
|
||||
// then
|
||||
assertEquals(expectedResult, workerResult)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun returnFailureIfDeserializationFails() {
|
||||
runBlocking {
|
||||
// given
|
||||
every { workerParameters.inputData } returns mockk {
|
||||
every { getString(KEY_PUSH_NOTIFICATION_UID) } returns "uid"
|
||||
every { getString(KEY_PUSH_NOTIFICATION_ENCRYPTED_MESSAGE) } returns "encryptedMessage"
|
||||
}
|
||||
every { AppUtil.isAppInBackground() } returns false
|
||||
every { userManager.getUsernameBySessionId("uid") } returns "username"
|
||||
every { userManager.getUser("username") } returns mockk {
|
||||
every { isBackgroundSync } returns true
|
||||
}
|
||||
every { userManager.openPgp } returns mockk(relaxed = true)
|
||||
every { anyConstructed<UserCrypto>().decryptMessage(any()) } returns mockk {
|
||||
every { decryptedData } returns "decryptedData"
|
||||
}
|
||||
every { "decryptedData".deserialize<PushNotification>() } returns mockk {
|
||||
every { data } returns null
|
||||
}
|
||||
|
||||
val expectedResult = ListenableWorker.Result.failure(
|
||||
workDataOf(KEY_PROCESS_PUSH_NOTIFICATION_DATA_ERROR to "Decryption or deserialization error")
|
||||
)
|
||||
|
||||
// when
|
||||
val workerResult = processPushNotificationDataWorker.doWork()
|
||||
|
||||
// then
|
||||
assertEquals(expectedResult, workerResult)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mockForCallingSendNotificationSuccessfully(): Pair<PushNotificationSender, PushNotificationData> {
|
||||
every { workerParameters.inputData } returns mockk {
|
||||
every { getString(KEY_PUSH_NOTIFICATION_UID) } returns "uid"
|
||||
every { getString(KEY_PUSH_NOTIFICATION_ENCRYPTED_MESSAGE) } returns "encryptedMessage"
|
||||
}
|
||||
every { AppUtil.isAppInBackground() } returns false
|
||||
every { userManager.getUsernameBySessionId("uid") } returns "username"
|
||||
every { userManager.getUser("username") } returns mockk {
|
||||
every { isBackgroundSync } returns true
|
||||
every { username } returns "username"
|
||||
}
|
||||
every { userManager.openPgp } returns mockk(relaxed = true)
|
||||
every { anyConstructed<UserCrypto>().decryptMessage(any()) } returns mockk {
|
||||
every { decryptedData } returns "decryptedData"
|
||||
}
|
||||
val mockNotificationSender = mockk<PushNotificationSender> {
|
||||
every { senderName } returns ""
|
||||
every { senderAddress } returns "senderAddress"
|
||||
}
|
||||
val mockNotificationEncryptedData = mockk<PushNotificationData> {
|
||||
every { messageId } returns "messageId"
|
||||
every { body } returns "body"
|
||||
every { sender } returns mockNotificationSender
|
||||
}
|
||||
every { "decryptedData".deserialize<PushNotification>() } returns mockk {
|
||||
every { data } returns mockNotificationEncryptedData
|
||||
}
|
||||
every { userManager.username } returns "username"
|
||||
every { userManager.isSnoozeQuickEnabled() } returns false
|
||||
every { userManager.isSnoozeScheduledEnabled() } returns true
|
||||
every { processPushNotificationDataWorker invokeNoArgs "shouldSuppressNotification" } returns false
|
||||
|
||||
return Pair(mockNotificationSender, mockNotificationEncryptedData)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun verifyCorrectMethodInvocationAfterDecryptionAndDeserializationSucceedsWhenSnoozingNotificationsIsNotActive() {
|
||||
runBlocking {
|
||||
// given
|
||||
val (mockNotificationSender, mockNotificationEncryptedData) = mockForCallingSendNotificationSuccessfully()
|
||||
|
||||
justRun { processPushNotificationDataWorker invoke "sendNotification" withArguments listOf(any<User>(), any<String>(), any<String>(), any<String>(), any<Boolean>()) }
|
||||
|
||||
// when
|
||||
processPushNotificationDataWorker.doWork()
|
||||
|
||||
// then
|
||||
verifyOrder {
|
||||
mockNotificationEncryptedData.messageId
|
||||
mockNotificationEncryptedData.body
|
||||
mockNotificationEncryptedData.sender
|
||||
mockNotificationSender.senderName
|
||||
mockNotificationSender.senderAddress
|
||||
userManager.username
|
||||
userManager.isSnoozeQuickEnabled()
|
||||
userManager.isSnoozeScheduledEnabled()
|
||||
processPushNotificationDataWorker invokeNoArgs "shouldSuppressNotification"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun verifyNotifySingleNewEmailIsCalledWithCorrectParametersWhenThereIsOneNotificationInTheDB() {
|
||||
runBlocking {
|
||||
// given
|
||||
mockForCallingSendNotificationSuccessfully()
|
||||
|
||||
val mockNotification = mockk<Notification>()
|
||||
every { databaseProvider.provideNotificationsDao("username") } returns mockk(relaxed = true) {
|
||||
every { findAllNotifications() } returns listOf(mockNotification)
|
||||
}
|
||||
val mockMessage = mockk<Message>()
|
||||
every { processPushNotificationDataWorker invoke "fetchMessage" withArguments listOf(any<User>(), any<String>()) } returns mockMessage
|
||||
every { notificationServer.notifySingleNewEmail(any(), any(), any(), any(), any(), any(), any()) } just runs
|
||||
|
||||
// when
|
||||
processPushNotificationDataWorker.doWork()
|
||||
|
||||
// then
|
||||
val userManagerSlot = slot<UserManager>()
|
||||
val userSlot = slot<User>()
|
||||
val messageSlot = slot<Message>()
|
||||
val messageIdSlot = slot<String>()
|
||||
val notificationBodySlot = slot<String>()
|
||||
val senderSlot = slot<String>()
|
||||
val primaryUserSlot = slot<Boolean>()
|
||||
verify {
|
||||
notificationServer.notifySingleNewEmail(capture(userManagerSlot), capture(userSlot), capture(messageSlot), capture(messageIdSlot), capture(notificationBodySlot), capture(senderSlot), capture(primaryUserSlot))
|
||||
}
|
||||
assertEquals(userManager, userManagerSlot.captured)
|
||||
assertEquals(userManager.getUser("username"), userSlot.captured)
|
||||
assertEquals(mockMessage, messageSlot.captured)
|
||||
assertEquals("messageId", messageIdSlot.captured)
|
||||
assertEquals("body", notificationBodySlot.captured)
|
||||
assertEquals("senderAddress", senderSlot.captured)
|
||||
assertEquals(true, primaryUserSlot.captured)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun verifyNotifyMultipleUnreadEmailIsCalledWithCorrectParametersWhenThereAreMoreThanOneNotificationsInTheDB() {
|
||||
runBlocking {
|
||||
// given
|
||||
mockForCallingSendNotificationSuccessfully()
|
||||
|
||||
val mockNotification1 = mockk<Notification>()
|
||||
val mockNotification2 = mockk<Notification>()
|
||||
val unreadNotifications = listOf(mockNotification1, mockNotification2)
|
||||
every { databaseProvider.provideNotificationsDao("username") } returns mockk(relaxed = true) {
|
||||
every { findAllNotifications() } returns unreadNotifications
|
||||
}
|
||||
val mockMessage = mockk<Message>()
|
||||
every { processPushNotificationDataWorker invoke "fetchMessage" withArguments listOf(any<User>(), any<String>()) } returns mockMessage
|
||||
every { notificationServer.notifyMultipleUnreadEmail(any(), any(), any()) } just runs
|
||||
|
||||
// when
|
||||
processPushNotificationDataWorker.doWork()
|
||||
|
||||
// then
|
||||
val userManagerSlot = slot<UserManager>()
|
||||
val userSlot = slot<User>()
|
||||
val unreadNotificationsSlot = slot<List<Notification>>()
|
||||
verify {
|
||||
notificationServer.notifyMultipleUnreadEmail(capture(userManagerSlot), capture(userSlot), capture(unreadNotificationsSlot))
|
||||
}
|
||||
assertEquals(userManager, userManagerSlot.captured)
|
||||
assertEquals(userManager.getUser("username"), userSlot.captured)
|
||||
assertEquals(unreadNotifications, unreadNotificationsSlot.captured)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun returnSuccessWhenNotificationWasSent() {
|
||||
runBlocking {
|
||||
// given
|
||||
mockForCallingSendNotificationSuccessfully()
|
||||
|
||||
val mockNotification = mockk<Notification>()
|
||||
every { databaseProvider.provideNotificationsDao("username") } returns mockk(relaxed = true) {
|
||||
every { findAllNotifications() } returns listOf(mockNotification)
|
||||
}
|
||||
val mockMessage = mockk<Message>()
|
||||
every { processPushNotificationDataWorker invoke "fetchMessage" withArguments listOf(any<User>(), any<String>()) } returns mockMessage
|
||||
every { notificationServer.notifySingleNewEmail(any(), any(), any(), any(), any(), any(), any()) } just runs
|
||||
|
||||
val expectedResult = ListenableWorker.Result.success()
|
||||
|
||||
// when
|
||||
val workerResult = processPushNotificationDataWorker.doWork()
|
||||
|
||||
// then
|
||||
assertEquals(expectedResult, workerResult)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue