/* * Nextcloud Talk application * * @author Mario Danic * Copyright (C) 2017-2018 Mario Danic * * 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 . */ package com.nextcloud.talk.jobs; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.media.AudioManager; import android.media.MediaPlayer; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.VibrationEffect; import android.os.Vibrator; import android.support.annotation.NonNull; import android.text.TextUtils; import android.util.Base64; import android.util.Log; import com.bluelinelabs.logansquare.LoganSquare; import com.evernote.android.job.Job; import com.evernote.android.job.util.support.PersistableBundleCompat; import com.nextcloud.talk.R; import com.nextcloud.talk.activities.MagicCallActivity; import com.nextcloud.talk.activities.MainActivity; import com.nextcloud.talk.api.NcApi; import com.nextcloud.talk.application.NextcloudTalkApplication; import com.nextcloud.talk.models.RingtoneSettings; import com.nextcloud.talk.models.SignatureVerification; import com.nextcloud.talk.models.database.UserEntity; import com.nextcloud.talk.models.json.chat.ChatUtils; import com.nextcloud.talk.models.json.notifications.NotificationOverall; import com.nextcloud.talk.models.json.push.DecryptedPushMessage; import com.nextcloud.talk.models.json.rooms.Conversation; import com.nextcloud.talk.models.json.rooms.RoomOverall; 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.bundle.BundleKeys; import com.nextcloud.talk.utils.database.user.UserUtils; import com.nextcloud.talk.utils.preferences.AppPreferences; import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder; import com.nextcloud.talk.utils.singletons.ApplicationWideStateHolder; import org.parceler.Parcels; import java.io.IOException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.util.Calendar; import java.util.HashMap; import java.util.zip.CRC32; import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; import javax.inject.Inject; import autodagger.AutoInjector; import io.reactivex.Observer; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; @AutoInjector(NextcloudTalkApplication.class) public class NotificationJob extends Job { public static final String TAG = "NotificationJob"; @Inject UserUtils userUtils; @Inject AppPreferences appPreferences; @Inject NcApi ncApi; private DecryptedPushMessage decryptedPushMessage; private Context context; private SignatureVerification signatureVerification; private String conversationType = ""; @NonNull @Override protected Result onRunJob(Params params) { NextcloudTalkApplication.getSharedApplication().getComponentApplication().inject(this); context = getContext(); PersistableBundleCompat persistableBundleCompat = getParams().getExtras(); String subject = persistableBundleCompat.getString(BundleKeys.KEY_NOTIFICATION_SUBJECT, ""); String signature = persistableBundleCompat.getString(BundleKeys.KEY_NOTIFICATION_SIGNATURE, ""); if (!TextUtils.isEmpty(subject) && !TextUtils.isEmpty(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.isSignatureValid()) { 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); boolean hasChatSupport = signatureVerification.getUserEntity().hasSpreedCapabilityWithName ("chat-v2"); boolean isInTheSameRoomAsNotification = (ApplicationWideCurrentRoomHolder.getInstance(). getCurrentRoomId().equals(decryptedPushMessage.getId()) || ApplicationWideCurrentRoomHolder.getInstance() .getCurrentRoomToken().equals(decryptedPushMessage.getId())) && signatureVerification.getUserEntity().equals(ApplicationWideCurrentRoomHolder .getInstance().getUserInRoom()); boolean shouldShowNotification = decryptedPushMessage.getApp().equals("spreed") && !decryptedPushMessage.getType().equals("room") && (!isInTheSameRoomAsNotification || !ApplicationWideStateHolder.getInstance().isInForeground() || decryptedPushMessage.getType().equals("call")); if (shouldShowNotification) { Intent intent; Bundle bundle = new Bundle(); boolean startACall = decryptedPushMessage.getType().equals("call") || !hasChatSupport; if (startACall) { intent = new Intent(context, MagicCallActivity.class); } else { intent = new Intent(context, MainActivity.class); } intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); if (!signatureVerification.getUserEntity().hasSpreedCapabilityWithName ("no-ping")) { bundle.putString(BundleKeys.KEY_ROOM_ID, decryptedPushMessage.getId()); } else { bundle.putString(BundleKeys.KEY_ROOM_TOKEN, decryptedPushMessage.getId()); } bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, Parcels.wrap(signatureVerification .getUserEntity())); bundle.putBoolean(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL, startACall); intent.putExtras(bundle); switch (decryptedPushMessage.getType()) { case "call": if (!bundle.containsKey(BundleKeys.KEY_ROOM_TOKEN)) { context.startActivity(intent); } else { showNotificationForCallWithNoPing(intent); } break; case "room": // do absolutely nothing, we won't even come to this point break; case "chat": if (decryptedPushMessage.getNotificationId() != Long.MIN_VALUE) { showMessageNotificationWithObjectData(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; } private void showNotificationForCallWithNoPing(Intent intent) { UserEntity userEntity = signatureVerification.getUserEntity(); ncApi.getRoom(ApiUtils.getCredentials(userEntity.getUserId(), userEntity.getToken()), ApiUtils.getRoom(userEntity.getBaseUrl(), intent.getExtras().getString(BundleKeys.KEY_ROOM_TOKEN))) .subscribeOn(Schedulers.newThread()) .subscribe(new Observer() { @Override public void onSubscribe(Disposable d) { } @Override public void onNext(RoomOverall roomOverall) { Conversation conversation = roomOverall.getOcs().getData(); intent.putExtra(BundleKeys.KEY_ROOM, Parcels.wrap(conversation)); if (conversation.getType().equals(Conversation.RoomType.ROOM_TYPE_ONE_TO_ONE_CALL)) { context.startActivity(intent); } else { if (conversation.getType().equals(Conversation.RoomType.ROOM_GROUP_CALL)) { conversationType = "group"; } else { conversationType = "public"; } showNotification(intent); } } @Override public void onError(Throwable e) { } @Override public void onComplete() { } }); } private void showMessageNotificationWithObjectData(Intent intent) { UserEntity userEntity = signatureVerification.getUserEntity(); ncApi.getNotification(ApiUtils.getCredentials(userEntity.getUserId(), userEntity.getToken()), ApiUtils.getUrlForNotificationWithId(userEntity.getBaseUrl(), Long.toString(decryptedPushMessage.getNotificationId()))) .subscribeOn(Schedulers.newThread()) .subscribe(new Observer() { @Override public void onSubscribe(Disposable d) { } @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.getParsedMessage(notification.getMessageRich(), notification.getMessageRichParameters())); } else { decryptedPushMessage.setText(notification.getMessage()); } HashMap> subjectRichParametersRichParameters = notification .getSubjectRichParameters(); if (subjectRichParametersRichParameters != null && subjectRichParametersRichParameters .size() > 0 && subjectRichParametersRichParameters.containsKey("call") && subjectRichParametersRichParameters.containsKey("user")) { HashMap callHashMap = subjectRichParametersRichParameters.get("call"); HashMap userHashMap = subjectRichParametersRichParameters.get("user"); if (callHashMap.containsKey("call-type")) { conversationType = callHashMap.get("call-type"); if ("one2one".equals(conversationType)) { decryptedPushMessage.setSubject(userHashMap.get("name")); } } } showNotification(intent); } @Override public void onError(Throwable e) { showNotification(intent); } @Override public void onComplete() { } }); } private void showNotification(Intent intent) { int smallIcon; Bitmap largeIcon = null; String category; int priority = Notification.PRIORITY_HIGH; Uri soundUri; smallIcon = R.drawable.ic_logo; if (decryptedPushMessage.getType().equals("chat")) { category = Notification.CATEGORY_MESSAGE; } else { category = Notification.CATEGORY_CALL; } switch (conversationType) { case "one2one": if (decryptedPushMessage.getType().equals("chat")) { largeIcon = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_chat_black_24dp); } else { largeIcon = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_call_black_24dp); } break; 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: if (decryptedPushMessage.getType().equals("chat")) { largeIcon = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_chat_black_24dp); } else { largeIcon = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_call_black_24dp); } } PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT); NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); CRC32 crc32 = new CRC32(); Notification.Builder notificationBuilder = new Notification.Builder(context) .setLargeIcon(largeIcon) .setSmallIcon(smallIcon) .setCategory(category) .setPriority(priority) .setWhen(Calendar.getInstance().getTimeInMillis()) .setShowWhen(true) .setSubText(signatureVerification.getUserEntity().getDisplayName()) .setContentTitle(decryptedPushMessage.getSubject()) .setContentIntent(pendingIntent) .setAutoCancel(true); if (!TextUtils.isEmpty(decryptedPushMessage.getText())) { notificationBuilder.setContentText(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)); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { String groupName = String.format(context.getResources().getString(R.string .nc_notification_channel), signatureVerification.getUserEntity() .getUserId(), signatureVerification.getUserEntity().getBaseUrl()); crc32.update(groupName.getBytes()); NotificationUtils.createNotificationChannelGroup(notificationManager, Long.toString(crc32.getValue()), groupName); if (decryptedPushMessage.getType().equals("chat")) { NotificationUtils.createNotificationChannel(notificationManager, NotificationUtils.NOTIFICATION_CHANNEL_MESSAGES_V3, context.getResources() .getString(R.string.nc_notification_channel_messages), context.getResources() .getString(R.string.nc_notification_channel_messages), true, NotificationManager.IMPORTANCE_HIGH); notificationBuilder.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_MESSAGES_V3); } else { NotificationUtils.createNotificationChannel(notificationManager, NotificationUtils.NOTIFICATION_CHANNEL_CALLS_V2, context.getResources() .getString(R.string.nc_notification_channel_calls), context.getResources() .getString(R.string.nc_notification_channel_calls_description), true, NotificationManager.IMPORTANCE_HIGH); notificationBuilder.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_CALLS_V2); } notificationBuilder.setGroup(Long.toString(crc32.getValue())); } notificationBuilder.setContentIntent(pendingIntent); String stringForCrc = decryptedPushMessage.getSubject() + " " + signatureVerification .getUserEntity().getDisplayName() + " " + signatureVerification.getUserEntity ().getBaseUrl() + System.currentTimeMillis(); crc32 = new CRC32(); crc32.update(stringForCrc.getBytes()); String ringtonePreferencesString; 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"); } } if (notificationManager != null) { notificationManager.notify((int) crc32.getValue(), notificationBuilder.build()); if (soundUri != null & !ApplicationWideCurrentRoomHolder.getInstance().isInCall() && DoNotDisturbUtils.shouldPlaySound()) { MediaPlayer mediaPlayer = MediaPlayer.create(context, soundUri); mediaPlayer.setAudioStreamType(AudioManager.STREAM_NOTIFICATION); mediaPlayer.start(); mediaPlayer.setOnCompletionListener(MediaPlayer::release); } if (DoNotDisturbUtils.shouldVibrate(appPreferences.getShouldVibrateSetting())) { Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); if (vibrator != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { vibrator.vibrate(VibrationEffect.createOneShot(500, VibrationEffect.DEFAULT_AMPLITUDE)); } else { vibrator.vibrate(500); } } } } } }