Skip to content
Snippets Groups Projects
Select Git revision
  • 011943c4b8db106a94406d0d441a32fda68d0256
  • master default protected
  • release/202005
  • release/202001
  • release/201912
  • release/201911
  • release/releaseWindowsTestOne
  • release/releaseTest
  • release/releaseWindowsTest
  • release/windowsReleaseTest
  • release/201910
  • release/qt/201910
  • release/windows-test/201910
  • release/201908
  • release/201906
  • release/201905
  • release/201904
  • release/201903
  • release/201902
  • release/201901
  • release/201812
  • 1.0.0
  • 0.3.0
  • 0.2.1
  • 0.2.0
  • 0.1.0
26 results

callbackshandler.cpp

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    NotificationServiceImpl.java 45.02 KiB
    /*
     * Copyright (C) 2004-2019 Savoir-faire Linux Inc.
     *
     *  Author: Aline Bonnet <aline.bonnet@savoirfairelinux.com>
     *  Author: Adrien Béraud <adrien.beraud@savoirfairelinux.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, write to the Free Software
     *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
     */
    package cx.ring.services;
    
    import android.annotation.SuppressLint;
    import android.app.Notification;
    import android.app.NotificationChannel;
    import android.app.NotificationChannelGroup;
    import android.app.NotificationManager;
    import android.app.PendingIntent;
    import android.content.Context;
    import android.content.Intent;
    import android.graphics.Bitmap;
    import android.graphics.drawable.BitmapDrawable;
    import android.media.AudioAttributes;
    import android.media.RingtoneManager;
    import android.os.Build;
    import android.os.Bundle;
    import android.text.TextUtils;
    import android.text.format.Formatter;
    import android.util.Log;
    import android.util.SparseArray;
    
    import androidx.annotation.RequiresApi;
    import androidx.core.app.NotificationCompat;
    import androidx.core.app.NotificationCompat.CarExtender.UnreadConversation;
    import androidx.core.app.NotificationManagerCompat;
    import androidx.core.app.Person;
    import androidx.core.app.RemoteInput;
    import androidx.core.content.res.ResourcesCompat;
    import androidx.core.graphics.drawable.IconCompat;
    
    import com.bumptech.glide.Glide;
    
    import java.io.File;
    import java.util.Collection;
    import java.util.Iterator;
    import java.util.LinkedHashMap;
    import java.util.Random;
    import java.util.Set;
    import java.util.TreeMap;
    import java.util.concurrent.ConcurrentHashMap;
    
    import javax.inject.Inject;
    
    import cx.ring.R;
    import cx.ring.client.ConversationActivity;
    import cx.ring.client.HomeActivity;
    import cx.ring.contactrequests.ContactRequestsFragment;
    import cx.ring.contacts.AvatarFactory;
    import cx.ring.fragments.ConversationFragment;
    import cx.ring.model.Account;
    import cx.ring.model.CallContact;
    import cx.ring.model.Conference;
    import cx.ring.model.Conversation;
    import cx.ring.model.Interaction;
    import cx.ring.model.Interaction.InteractionStatus;
    import cx.ring.model.DataTransfer;
    import cx.ring.model.SipCall;
    import cx.ring.model.TextMessage;
    import cx.ring.model.Uri;
    import cx.ring.service.CallNotificationService;
    import cx.ring.service.DRingService;
    import cx.ring.tv.call.TVCallActivity;
    import cx.ring.utils.ConversationPath;
    import cx.ring.utils.DeviceUtils;
    import cx.ring.utils.FileUtils;
    import cx.ring.utils.ResourceMapper;
    import cx.ring.utils.Tuple;
    
    public class NotificationServiceImpl implements NotificationService {
    
        private static final String TAG = NotificationServiceImpl.class.getSimpleName();
    
        private static final String NOTIF_MSG = "MESSAGE";
        private static final String NOTIF_TRUST_REQUEST = "TRUST REQUEST";
        private static final String NOTIF_FILE_TRANSFER = "FILE_TRANSFER";
        private static final String NOTIF_MISSED_CALL = "MISSED_CALL";
    
        private static final String NOTIF_CHANNEL_CALL_IN_PROGRESS = "current_call";
        private static final String NOTIF_CHANNEL_MISSED_CALL = "missed_calls";
        private static final String NOTIF_CHANNEL_INCOMING_CALL = "incoming_call";
    
        private static final String NOTIF_CHANNEL_MESSAGE = "messages";
        private static final String NOTIF_CHANNEL_REQUEST = "requests";
        private static final String NOTIF_CHANNEL_FILE_TRANSFER = "file_transfer";
        public static final String NOTIF_CHANNEL_SYNC = "sync";
        private static final String NOTIF_CHANNEL_SERVICE = "service";
    
        // old channel codes that were replaced or split
        private static final String NOTIF_CHANNEL_CALL_LEGACY = "call";
        private static final String NOTIF_CHANNEL_MISSED_CALL_LEGACY = "missed_call";
    
        private static final String NOTIF_CALL_GROUP = "calls";
    
        private static final int NOTIF_CALL_ID = 1001;
    
    
        private final SparseArray<NotificationCompat.Builder> mNotificationBuilders = new SparseArray<>();
        @Inject
        protected Context mContext;
        @Inject
        protected AccountService mAccountService;
        @Inject
        protected ContactService mContactService;
        @Inject
        protected PreferencesService mPreferencesService;
        @Inject
        protected HistoryService mHistoryService;
        @Inject
        protected DeviceRuntimeService mDeviceRuntimeService;
        private NotificationManagerCompat notificationManager;
        private final Random random = new Random();
        private int avatarSize;
        private LinkedHashMap<Integer, Conference> currentCalls = new LinkedHashMap<>();
        private ConcurrentHashMap<Integer, Notification> dataTransferNotifications = new ConcurrentHashMap<>();
    
        @SuppressLint("CheckResult")
        public void initHelper() {
            if (notificationManager == null) {
                notificationManager = NotificationManagerCompat.from(mContext);
            }
            avatarSize = (int) (mContext.getResources().getDisplayMetrics().density * AvatarFactory.SIZE_NOTIF);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                registerNotificationChannels();
            }
        }
    
        @RequiresApi(api = Build.VERSION_CODES.O)
        private void registerNotificationChannels() {
            NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
            if (notificationManager == null)
                return;
    
    
            notificationManager.deleteNotificationChannel(NOTIF_CHANNEL_MISSED_CALL_LEGACY);
            notificationManager.deleteNotificationChannel(NOTIF_CHANNEL_CALL_LEGACY);
    
            // Setting up groups
    
            notificationManager.createNotificationChannelGroup(new NotificationChannelGroup(NOTIF_CALL_GROUP, mContext.getString(R.string.notif_group_calls)));
    
            // Missed calls channel
            NotificationChannel missedCallsChannel = new NotificationChannel(NOTIF_CHANNEL_MISSED_CALL, mContext.getString(R.string.notif_channel_missed_calls), NotificationManager.IMPORTANCE_DEFAULT);
            missedCallsChannel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
            missedCallsChannel.setSound(null, null);
            missedCallsChannel.enableVibration(false);
            missedCallsChannel.setGroup(NOTIF_CALL_GROUP);
            notificationManager.createNotificationChannel(missedCallsChannel);
    
            // Incoming call channel
            NotificationChannel incomingCallChannel = new NotificationChannel(NOTIF_CHANNEL_INCOMING_CALL, mContext.getString(R.string.notif_channel_incoming_calls), NotificationManager.IMPORTANCE_HIGH);
            incomingCallChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
            incomingCallChannel.setGroup(NOTIF_CALL_GROUP);
            incomingCallChannel.setSound(null, null);
            incomingCallChannel.enableVibration(false);
            notificationManager.createNotificationChannel(incomingCallChannel);
    
            // Call in progress channel
            NotificationChannel callInProgressChannel = new NotificationChannel(NOTIF_CHANNEL_CALL_IN_PROGRESS, mContext.getString(R.string.notif_channel_call_in_progress), NotificationManager.IMPORTANCE_DEFAULT);
            callInProgressChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
            callInProgressChannel.setSound(null, null);
            callInProgressChannel.enableVibration(false);
            callInProgressChannel.setGroup(NOTIF_CALL_GROUP);
            notificationManager.createNotificationChannel(callInProgressChannel);
    
            // Text messages channel
            AudioAttributes soundAttributes = new AudioAttributes.Builder()
                    .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                    .setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT)
                    .build();
    
            NotificationChannel messageChannel = new NotificationChannel(NOTIF_CHANNEL_MESSAGE, mContext.getString(R.string.notif_channel_messages), NotificationManager.IMPORTANCE_HIGH);
            messageChannel.enableVibration(true);
            messageChannel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
            messageChannel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), soundAttributes);
            notificationManager.createNotificationChannel(messageChannel);
    
            // Contact requests
            NotificationChannel requestsChannel = new NotificationChannel(NOTIF_CHANNEL_REQUEST, mContext.getString(R.string.notif_channel_requests), NotificationManager.IMPORTANCE_DEFAULT);
            requestsChannel.enableVibration(true);
            requestsChannel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
            requestsChannel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), soundAttributes);
            notificationManager.createNotificationChannel(requestsChannel);
    
            // File transfer requests
            NotificationChannel fileTransferChannel = new NotificationChannel(NOTIF_CHANNEL_FILE_TRANSFER, mContext.getString(R.string.notif_channel_file_transfer), NotificationManager.IMPORTANCE_DEFAULT);
            fileTransferChannel.enableVibration(true);
            fileTransferChannel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
            fileTransferChannel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), soundAttributes);
            notificationManager.createNotificationChannel(fileTransferChannel);
    
            // File transfer requests
            NotificationChannel syncChannel = new NotificationChannel(NOTIF_CHANNEL_SYNC, mContext.getString(R.string.notif_channel_sync), NotificationManager.IMPORTANCE_DEFAULT);
            syncChannel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
            syncChannel.enableLights(false);
            syncChannel.enableVibration(false);
            syncChannel.setShowBadge(false);
            syncChannel.setSound(null, null);
            notificationManager.createNotificationChannel(syncChannel);
    
            // Background service channel
            NotificationChannel backgroundChannel = new NotificationChannel(NOTIF_CHANNEL_SERVICE, mContext.getString(R.string.notif_channel_background_service), NotificationManager.IMPORTANCE_LOW);
            backgroundChannel.setDescription(mContext.getString(R.string.notif_channel_background_service_descr));
            backgroundChannel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
            backgroundChannel.enableLights(false);
            backgroundChannel.enableVibration(false);
            backgroundChannel.setShowBadge(false);
            notificationManager.createNotificationChannel(backgroundChannel);
        }
    
        /**
         * Starts the call activity directly for Android TV
         *
         * @param callId the call ID
         */
        private void startCallActivity(String callId) {
            mContext.startActivity(new Intent(Intent.ACTION_VIEW)
                    .putExtra(KEY_CALL_ID, callId)
                    .setClass(mContext.getApplicationContext(), TVCallActivity.class)
                    .setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NEW_TASK));
        }
    
        @Override
        public Object showCallNotification(int callId) {
            Conference mConference = currentCalls.get(callId);
            if (mConference == null || mConference.getParticipants().isEmpty()) {
                return null;
            }
    
            SipCall call = mConference.getParticipants().get(0);
    
            notificationManager.cancel(NOTIF_CALL_ID);
    
            PendingIntent gotoIntent = PendingIntent.getService(mContext,
                    random.nextInt(),
                    new Intent(DRingService.ACTION_CALL_VIEW)
                            .setClass(mContext, DRingService.class)
                            .putExtra(KEY_CALL_ID, call.getDaemonIdString()), 0);
    
            CallContact contact = call.getContact();
            NotificationCompat.Builder messageNotificationBuilder;
            if (mConference.isOnGoing()) {
                messageNotificationBuilder = new NotificationCompat.Builder(mContext, NOTIF_CHANNEL_CALL_IN_PROGRESS);
                messageNotificationBuilder.setContentTitle(mContext.getString(R.string.notif_current_call_title, contact.getDisplayName()))
                        .setContentText(mContext.getText(R.string.notif_current_call))
                        .setPriority(NotificationCompat.PRIORITY_DEFAULT)
                        .setContentIntent(gotoIntent)
                        .setSound(null)
                        .setVibrate(null)
                        .setColorized(true)
                        .setColor(mContext.getResources().getColor(R.color.color_primary_light))
                        .addAction(R.drawable.baseline_call_end_24, mContext.getText(R.string.action_call_hangup),
                                PendingIntent.getService(mContext, random.nextInt(),
                                        new Intent(DRingService.ACTION_CALL_END)
                                                .setClass(mContext, DRingService.class)
                                                .putExtra(KEY_CALL_ID, call.getDaemonIdString()),
                                        PendingIntent.FLAG_ONE_SHOT));
            } else if (mConference.isRinging()) {
                if (mConference.isIncoming()) {
                    messageNotificationBuilder = new NotificationCompat.Builder(mContext, NOTIF_CHANNEL_INCOMING_CALL);
                    messageNotificationBuilder.setContentTitle(mContext.getString(R.string.notif_incoming_call_title, contact.getDisplayName()))
                            .setPriority(NotificationCompat.PRIORITY_MAX)
                            .setContentText(mContext.getText(R.string.notif_incoming_call))
                            .setContentIntent(gotoIntent)
                            .setSound(null)
                            .setVibrate(null)
                            .setFullScreenIntent(gotoIntent, true)
                            .addAction(R.drawable.baseline_call_end_24, mContext.getText(R.string.action_call_decline),
                                    PendingIntent.getService(mContext, random.nextInt(),
                                            new Intent(DRingService.ACTION_CALL_REFUSE)
                                                    .setClass(mContext, DRingService.class)
                                                    .putExtra(KEY_CALL_ID, call.getDaemonIdString()),
                                            PendingIntent.FLAG_ONE_SHOT))
                            .addAction(R.drawable.baseline_call_24, mContext.getText(R.string.action_call_accept),
                                    PendingIntent.getService(mContext, random.nextInt(),
                                            new Intent(DRingService.ACTION_CALL_ACCEPT)
                                                    .setClass(mContext, DRingService.class)
                                                    .putExtra(KEY_CALL_ID, call.getDaemonIdString()),
                                            PendingIntent.FLAG_ONE_SHOT));
                } else {
                    messageNotificationBuilder = new NotificationCompat.Builder(mContext, NOTIF_CHANNEL_CALL_IN_PROGRESS);
                    messageNotificationBuilder.setContentTitle(mContext.getString(R.string.notif_outgoing_call_title, contact.getDisplayName()))
                            .setContentText(mContext.getText(R.string.notif_outgoing_call))
                            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
                            .setContentIntent(gotoIntent)
                            .setSound(null)
                            .setVibrate(null)
                            .setColorized(true)
                            .setColor(mContext.getResources().getColor(R.color.color_primary_light))
                            .addAction(R.drawable.baseline_call_end_24, mContext.getText(R.string.action_call_hangup),
                                    PendingIntent.getService(mContext, random.nextInt(),
                                            new Intent(DRingService.ACTION_CALL_END)
                                                    .setClass(mContext, DRingService.class)
                                                    .putExtra(KEY_CALL_ID, call.getDaemonIdString()),
                                            PendingIntent.FLAG_ONE_SHOT));
                }
            } else {
                handleCallNotification(mConference, true);
                return null;
            }
    
            messageNotificationBuilder.setOngoing(true)
                    .setCategory(NotificationCompat.CATEGORY_CALL)
                    .setSmallIcon(R.drawable.ic_ring_logo_white);
    
            setContactPicture(contact, messageNotificationBuilder);
    
            return messageNotificationBuilder.build();
        }
    
        /**
         * Updates a notification
         *
         * @param notification   a built notification object
         * @param notificationId the notification's id
         */
        @Override
        public void updateNotification(Object notification, int notificationId) {
            if(notification != null)
                notificationManager.notify(notificationId, (Notification) notification);
        }
    
        /**
         * Starts a service (data transfer or call)
         *
         * @param id            the notification id
         */
        private void startForegroundService(int id, Class serviceClass) {
            Intent serviceIntent = new Intent(mContext, serviceClass);
            serviceIntent.putExtra(KEY_NOTIFICATION_ID, id);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
                mContext.startForegroundService(serviceIntent);
            else
                mContext.startService(serviceIntent);
        }
    
        /**
         * Handles the creation and destruction of services associated with calls as well as displaying notifications.
         *
         * @param conference the conference object for the notification
         * @param remove     true if it should be removed from current calls
         */
        @Override
        public void handleCallNotification(Conference conference, boolean remove) {
            if (DeviceUtils.isTv(mContext)) {
                if (!remove)
                    startCallActivity(conference.getId());
                return;
            }
    
            int id = conference.getId().hashCode();
            currentCalls.remove(id);
            if (!remove) {
                currentCalls.put(id, conference);
                startForegroundService(id, CallNotificationService.class);
            } else if (currentCalls.isEmpty())
                mContext.stopService(new Intent(mContext, CallNotificationService.class));
                // this is a temporary solution until we have direct support for concurrent calls and the call state will exclusively update notifications
            else {
                int key = -1;
                Iterator<Integer> iterator = (currentCalls.keySet().iterator());
                while (iterator.hasNext()) {
                    key = iterator.next();
                }
                updateNotification(showCallNotification(key), NOTIF_CALL_ID);
            }
        }
    
    
        @Override
        public void onConnectionUpdate(Boolean b) {
            Log.i(TAG, "onConnectionUpdate " + b);
            if (b) {
                Intent serviceIntent = new Intent(SyncService.ACTION_START).setClass(mContext, SyncService.class);
                try {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
                    mContext.startForegroundService(serviceIntent);
                else
                    mContext.startService(serviceIntent);
                } catch (IllegalStateException e) {
                    Log.e(TAG, "Error starting service", e);
                }
            } else {
                try {
                    mContext.startService(new Intent(SyncService.ACTION_STOP).setClass(mContext, SyncService.class));
                } catch (IllegalStateException ignored) {
                }
            }
        }
    
        /**
         * Handles the creation and destruction of services associated with transfers as well as displaying notifications.
         *
         * @param transfer the data transfer object
         * @param contact  the contact to whom the data transfer is being sent
         * @param remove   true if it should be removed from current calls
         */
        @Override
        public void handleDataTransferNotification(DataTransfer transfer, CallContact contact, boolean remove) {
            Log.d(TAG, "handleDataTransferNotification, a data transfer event is in progress");
            if (DeviceUtils.isTv(mContext)) {
                return;
            }
            if (!remove) {
                showFileTransferNotification(transfer, contact);
            } else {
                removeTransferNotification(transfer.getDaemonId());
            }
        }
    
        /**
         * Cancels a data transfer notification and removes it from the list of notifications
         *
         * @param transferId the transfer id which is required to generate the notification id
         */
        @Override
        public void removeTransferNotification(long transferId) {
            int id = getFileTransferNotificationId(transferId);
            dataTransferNotifications.remove(id);
            cancelFileNotification(id, false);
            if (dataTransferNotifications.isEmpty())
                mContext.stopService(new Intent(mContext, DataTransferService.class));
            else {
                startForegroundService(dataTransferNotifications.keySet().iterator().next(), DataTransferService.class);
            }
        }
    
        /**
         * @param notificationId the notification id
         * @return the notification object for a data transfer notification
         */
        @Override
        public Notification getDataTransferNotification(int notificationId) {
            return dataTransferNotifications.get(notificationId);
        }
    
        @Override
        public void showTextNotification(String accountId, Conversation conversation) {
            TreeMap<Long, TextMessage> texts = conversation.getUnreadTextMessages();
    
            CallContact contact = conversation.getContact();
            if (texts.isEmpty() || conversation.isVisible()) {
                cancelTextNotification(contact.getPrimaryUri());
                return;
            }
            if (texts.lastEntry().getValue().isNotified()) {
                return;
            }
    
            Log.w(TAG, "showTextNotification " + accountId + " " + contact.getPrimaryNumber());
            mContactService.getLoadedContact(accountId, contact)
                    .subscribe(c -> textNotification(accountId, texts, c),
                            e -> Log.w(TAG, "Can't load contact", e));
        }
    
        private void textNotification(String accountId, TreeMap<Long, TextMessage> texts, CallContact contact) {
            Uri contactUri = contact.getPrimaryUri();
            String contactId = contactUri.getUri();
            android.net.Uri path = ConversationPath.toUri(accountId, contactId);
    
            String contactName = contact.getDisplayName();
            if (TextUtils.isEmpty(contactName) || texts.isEmpty())
                return;
            Bitmap contactPicture = getContactPicture(contact);
    
            TextMessage last = texts.lastEntry().getValue();
            Intent intentConversation = new Intent(DRingService.ACTION_CONV_ACCEPT, path, mContext, DRingService.class);
            Intent intentDelete = new Intent(DRingService.ACTION_CONV_DISMISS, path, mContext, DRingService.class);
    
            NotificationCompat.Builder messageNotificationBuilder = new NotificationCompat.Builder(mContext, NOTIF_CHANNEL_MESSAGE)
                    .setCategory(NotificationCompat.CATEGORY_MESSAGE)
                    .setPriority(Notification.PRIORITY_HIGH)
                    .setDefaults(NotificationCompat.DEFAULT_ALL)
                    .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
                    .setSmallIcon(R.drawable.ic_ring_logo_white)
                    .setContentTitle(contactName)
                    .setContentText(last.getBody())
                    .setWhen(last.getTimestamp())
                    .setContentIntent(PendingIntent.getService(mContext, random.nextInt(), intentConversation, 0))
                    .setDeleteIntent(PendingIntent.getService(mContext, random.nextInt(), intentDelete, 0))
                    .setAutoCancel(true)
                    .setColor(ResourcesCompat.getColor(mContext.getResources(), R.color.color_primary_dark, null));
    
            if (contactPicture != null) {
                messageNotificationBuilder.setLargeIcon(contactPicture);
                Intent intentBubble = new Intent(Intent.ACTION_VIEW, path, mContext, ConversationActivity.class);
                messageNotificationBuilder.setBubbleMetadata(new NotificationCompat.BubbleMetadata.Builder()
                        .setDesiredHeight(600)
                        .setIcon(IconCompat.createWithAdaptiveBitmap(contactPicture))
                        .setIntent(PendingIntent.getActivity(mContext, 0, intentBubble, 0))
                        .build());
            }
    
            UnreadConversation.Builder unreadConvBuilder = new UnreadConversation.Builder(contactName)
                    .setLatestTimestamp(last.getTimestamp());
    
            if (texts.size() == 1) {
                last.setNotified(true);
                messageNotificationBuilder.setStyle(null);
                unreadConvBuilder.addMessage(last.getBody());
            } else {
                Account account = mAccountService.getAccount(accountId);
                Tuple<String, Object> profile = account == null ? null : VCardServiceImpl.loadProfile(account).blockingGet();
                Bitmap myPic = account == null ? null : getContactPicture(account);
                Person userPerson = new Person.Builder()
                        .setKey(accountId)
                        .setName(profile == null || TextUtils.isEmpty(profile.first) ? "You" : profile.first)
                        .setIcon(myPic == null ? null : IconCompat.createWithBitmap(myPic))
                        .build();
    
                Person contactPerson = new Person.Builder()
                        .setKey(contactId)
                        .setName(contactName)
                        .setIcon(contactPicture == null ? null : IconCompat.createWithBitmap(contactPicture))
                        .build();
    
                NotificationCompat.MessagingStyle history = new NotificationCompat.MessagingStyle(userPerson);
                for (TextMessage textMessage : texts.values()) {
                    history.addMessage(new NotificationCompat.MessagingStyle.Message(
                            textMessage.getBody(),
                            textMessage.getTimestamp(),
                            textMessage.isIncoming() ? contactPerson : null));
                    unreadConvBuilder.addMessage(textMessage.getBody());
                }
                messageNotificationBuilder.setStyle(history);
            }
    
            int notificationId = getTextNotificationId(contactUri);
            int replyId = notificationId + 1;
            int markAsReadId = notificationId + 2;
    
            CharSequence replyLabel = mContext.getText(R.string.notif_reply);
            RemoteInput remoteInput = new RemoteInput.Builder(DRingService.KEY_TEXT_REPLY)
                    .setLabel(replyLabel)
                    .build();
    
            PendingIntent replyPendingIntent = PendingIntent.getService(mContext, replyId,
                    new Intent(DRingService.ACTION_CONV_REPLY_INLINE, path, mContext, DRingService.class),
                    PendingIntent.FLAG_UPDATE_CURRENT);
    
            PendingIntent readPendingIntent = PendingIntent.getService(mContext, markAsReadId,
                    new Intent(DRingService.ACTION_CONV_READ, path, mContext, DRingService.class), 0);
    
            messageNotificationBuilder
                    .addAction(new NotificationCompat.Action.Builder(R.drawable.baseline_reply_24, replyLabel, replyPendingIntent)
                            .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
                            .addRemoteInput(remoteInput)
                            .build())
                    .addAction(new NotificationCompat.Action.Builder(0,
                            mContext.getString(R.string.notif_mark_as_read),
                            readPendingIntent)
                            .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
                            .setShowsUserInterface(false)
                            .build())
                    .extend(new NotificationCompat.CarExtender()
                            .setUnreadConversation(unreadConvBuilder
                                    .setReadPendingIntent(readPendingIntent)
                                    .setReplyAction(replyPendingIntent, remoteInput)
                                    .build()));
    
            notificationManager.notify(notificationId, messageNotificationBuilder.build());
            mNotificationBuilders.put(notificationId, messageNotificationBuilder);
        }
    
        private NotificationCompat.Builder getRequestNotificationBuilder(String accountId) {
            NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, NOTIF_CHANNEL_REQUEST)
                    .setDefaults(NotificationCompat.DEFAULT_ALL)
                    .setPriority(NotificationCompat.PRIORITY_HIGH)
                    .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
                    .setAutoCancel(true)
                    .setSmallIcon(R.drawable.ic_ring_logo_white)
                    .setCategory(NotificationCompat.CATEGORY_SOCIAL)
                    .setContentTitle(mContext.getString(R.string.contact_request_title));
            Intent intentOpenTrustRequestFragment = new Intent(HomeActivity.ACTION_PRESENT_TRUST_REQUEST_FRAGMENT)
                    .setClass(mContext, HomeActivity.class)
                    .putExtra(ContactRequestsFragment.ACCOUNT_ID, accountId);
            builder.setContentIntent(PendingIntent.getActivity(mContext,
                    random.nextInt(), intentOpenTrustRequestFragment, PendingIntent.FLAG_ONE_SHOT));
            builder.setColor(ResourcesCompat.getColor(mContext.getResources(),
                    R.color.color_primary_dark, null));
            return builder;
        }
    
        @Override
        public void showIncomingTrustRequestNotification(final Account account) {
            int notificationId = getIncomingTrustNotificationId(account.getAccountID());
            Set<String> notifiedRequests = mPreferencesService.loadRequestsPreferences(account.getAccountID());
    
            Collection<Conversation> requests = account.getPending();
            if (requests.isEmpty())
                return;
            if (requests.size() == 1) {
                Conversation request = requests.iterator().next();
                CallContact contact = request.getContact();
                String contactKey = contact.getPrimaryUri().getRawRingId();
                if (notifiedRequests.contains(contactKey)) {
                    return;
                }
                mContactService.getLoadedContact(account.getAccountID(), contact).subscribe(c -> {
                    NotificationCompat.Builder builder = getRequestNotificationBuilder(account.getAccountID());
                    mPreferencesService.saveRequestPreferences(account.getAccountID(), contactKey);
                    Bundle info = new Bundle();
                    info.putString(TRUST_REQUEST_NOTIFICATION_ACCOUNT_ID, account.getAccountID());
                    info.putString(TRUST_REQUEST_NOTIFICATION_FROM, c.getPrimaryNumber());
                    builder.setContentText(c.getRingUsername())
                            .addAction(R.drawable.baseline_person_add_24, mContext.getText(R.string.accept),
                                    PendingIntent.getService(mContext, random.nextInt(),
                                            new Intent(DRingService.ACTION_TRUST_REQUEST_ACCEPT)
                                                    .setClass(mContext, DRingService.class)
                                                    .putExtras(info),
                                            PendingIntent.FLAG_ONE_SHOT))
                            .addAction(R.drawable.baseline_delete_24, mContext.getText(R.string.refuse),
                                    PendingIntent.getService(mContext, random.nextInt(),
                                            new Intent(DRingService.ACTION_TRUST_REQUEST_REFUSE)
                                                    .setClass(mContext, DRingService.class)
                                                    .putExtras(info),
                                            PendingIntent.FLAG_ONE_SHOT))
                            .addAction(R.drawable.baseline_block_24, mContext.getText(R.string.block),
                                    PendingIntent.getService(mContext, random.nextInt(),
                                            new Intent(DRingService.ACTION_TRUST_REQUEST_BLOCK)
                                                    .setClass(mContext, DRingService.class)
                                                    .putExtras(info),
                                            PendingIntent.FLAG_ONE_SHOT));
    
                    setContactPicture(c, builder);
                    notificationManager.notify(notificationId, builder.build());
                }, e -> Log.w(TAG, "error showing notification", e));
            } else {
                NotificationCompat.Builder builder = getRequestNotificationBuilder(account.getAccountID());
                boolean newRequest = false;
                for (Conversation request : requests) {
                    CallContact contact = request.getContact();
                    String contactKey = contact.getPrimaryUri().getRawRingId();
                    if (!notifiedRequests.contains(contactKey)) {
                        newRequest = true;
                        mPreferencesService.saveRequestPreferences(account.getAccountID(), contactKey);
                    }
                }
                if (!newRequest)
                    return;
                builder.setContentText(String.format(mContext.getString(R.string.contact_request_msg), Integer.toString(requests.size())));
                builder.setLargeIcon(null);
                notificationManager.notify(notificationId, builder.build());
            }
        }
    
        @Override
        public void showFileTransferNotification(DataTransfer info, CallContact contact) {
            if (info == null) {
                return;
            }
            InteractionStatus event = info.getStatus();
            if (event == null) {
                return;
            }
            long dataTransferId = info.getDaemonId();
            int notificationId = getFileTransferNotificationId(dataTransferId);
    
            android.net.Uri path = ConversationPath.toUri(info.getAccount(), new Uri(info.getConversation().getParticipant()));
    
            Intent intentConversation = new Intent(DRingService.ACTION_CONV_ACCEPT, path, mContext, DRingService.class);
    
            if (event.isOver()) {
                removeTransferNotification(dataTransferId);
    
                if (info.isOutgoing()) {
                    return;
                }
    
                NotificationCompat.Builder notif = new NotificationCompat.Builder(mContext, NOTIF_CHANNEL_FILE_TRANSFER)
                        .setSmallIcon(R.drawable.ic_ring_logo_white)
                        .setContentIntent(PendingIntent.getService(mContext, random.nextInt(), intentConversation, 0))
                        .setAutoCancel(true);
    
                if (info.showPicture()) {
                    File filePath = mDeviceRuntimeService.getConversationPath(info.getPeerId(), info.getStoragePath());
                    Bitmap img;
                    try {
                        BitmapDrawable d = (BitmapDrawable) Glide.with(mContext)
                                .load(filePath)
                                .submit()
                                .get();
                        img = d.getBitmap();
                        notif.setContentTitle(mContext.getString(R.string.notif_incoming_picture, contact.getDisplayName()));
                        notif.setStyle(new NotificationCompat.BigPictureStyle()
                                .bigPicture(img));
                    } catch (Exception e) {
                        Log.w(TAG, "Can't load image for notification", e);
                        return;
                    }
                } else {
                    notif.setContentTitle(mContext.getString(R.string.notif_incoming_file_transfer_title, contact.getDisplayName()));
                    notif.setStyle(null);
                }
    
                setContactPicture(contact, notif);
                notificationManager.notify(random.nextInt(), notif.build());
                return;
            }
            NotificationCompat.Builder messageNotificationBuilder = mNotificationBuilders.get(notificationId);
            if (messageNotificationBuilder == null) {
                messageNotificationBuilder = new NotificationCompat.Builder(mContext, NOTIF_CHANNEL_FILE_TRANSFER);
            }
    
            boolean ongoing = event == InteractionStatus.TRANSFER_ONGOING || event == InteractionStatus.TRANSFER_ACCEPTED;
            String titleMessage = mContext.getString(info.isOutgoing() ? R.string.notif_outgoing_file_transfer_title : R.string.notif_incoming_file_transfer_title, contact.getDisplayName());
            messageNotificationBuilder.setContentTitle(titleMessage)
                    .setPriority(NotificationCompat.PRIORITY_DEFAULT)
                    .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
                    .setAutoCancel(false)
                    .setOngoing(ongoing)
                    .setSmallIcon(R.drawable.ic_ring_logo_white)
                    .setCategory(NotificationCompat.CATEGORY_PROGRESS)
                    .setOnlyAlertOnce(true)
                    .setContentText(event == Interaction.InteractionStatus.TRANSFER_ONGOING ?
                            Formatter.formatFileSize(mContext, info.getBytesProgress()) + " / " + Formatter.formatFileSize(mContext, info.getTotalSize()) :
                            info.getDisplayName() + ": " + ResourceMapper.getReadableFileTransferStatus(mContext, event))
                    .setContentIntent(PendingIntent.getService(mContext, random.nextInt(), intentConversation, 0))
                    .setColor(ResourcesCompat.getColor(mContext.getResources(), R.color.color_primary_dark, null));
            setContactPicture(contact, messageNotificationBuilder);
            if (event.isOver()) {
                messageNotificationBuilder.setProgress(0, 0, false);
            } else if (ongoing) {
                messageNotificationBuilder.setProgress((int) info.getTotalSize(), (int) info.getBytesProgress(), false);
            } else {
                messageNotificationBuilder.setProgress(0, 0, true);
            }
            if (event == Interaction.InteractionStatus.TRANSFER_CREATED) {
                messageNotificationBuilder.setDefaults(NotificationCompat.DEFAULT_VIBRATE);
                mNotificationBuilders.put(notificationId, messageNotificationBuilder);
                updateNotification(messageNotificationBuilder.build(), notificationId);
                return;
            } else {
                messageNotificationBuilder.setDefaults(NotificationCompat.DEFAULT_LIGHTS);
            }
            messageNotificationBuilder.mActions.clear();
            if (event == Interaction.InteractionStatus.TRANSFER_AWAITING_HOST) {
                messageNotificationBuilder
                        .addAction(R.drawable.baseline_call_received_24, mContext.getText(R.string.accept),
                                PendingIntent.getService(mContext, random.nextInt(),
                                        new Intent(DRingService.ACTION_FILE_ACCEPT)
                                                .setClass(mContext, DRingService.class)
                                                .putExtra(DRingService.KEY_TRANSFER_ID, dataTransferId),
                                        PendingIntent.FLAG_ONE_SHOT))
                        .addAction(R.drawable.baseline_cancel_24, mContext.getText(R.string.refuse),
                                PendingIntent.getService(mContext, random.nextInt(),
                                        new Intent(DRingService.ACTION_FILE_CANCEL)
                                                .setClass(mContext, DRingService.class)
                                                .putExtra(DRingService.KEY_TRANSFER_ID, dataTransferId),
                                        PendingIntent.FLAG_ONE_SHOT));
                mNotificationBuilders.put(notificationId, messageNotificationBuilder);
                updateNotification(messageNotificationBuilder.build(), notificationId);
                return;
            } else if (!event.isOver()) {
                messageNotificationBuilder
                        .addAction(R.drawable.baseline_cancel_24, mContext.getText(android.R.string.cancel),
                                PendingIntent.getService(mContext, random.nextInt(),
                                        new Intent(DRingService.ACTION_FILE_CANCEL)
                                                .setClass(mContext, DRingService.class)
                                                .putExtra(DRingService.KEY_TRANSFER_ID, dataTransferId),
                                        PendingIntent.FLAG_ONE_SHOT));
            }
            mNotificationBuilders.put(notificationId, messageNotificationBuilder);
            dataTransferNotifications.remove(notificationId);
            dataTransferNotifications.put(notificationId, messageNotificationBuilder.build());
            startForegroundService(notificationId, DataTransferService.class);
        }
    
        @Override
        public void showMissedCallNotification(SipCall call) {
            final int notificationId = call.getDaemonIdString().hashCode();
            NotificationCompat.Builder messageNotificationBuilder = mNotificationBuilders.get(notificationId);
            if (messageNotificationBuilder == null) {
                messageNotificationBuilder = new NotificationCompat.Builder(mContext, NOTIF_CHANNEL_MISSED_CALL);
            }
    
            android.net.Uri path = ConversationPath.toUri(call);
    
            Intent intentConversation = new Intent(DRingService.ACTION_CONV_ACCEPT, path, mContext, DRingService.class);
    
            messageNotificationBuilder.setContentTitle(mContext.getText(R.string.notif_missed_incoming_call))
                    .setPriority(NotificationCompat.PRIORITY_DEFAULT)
                    .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
                    .setSmallIcon(R.drawable.baseline_call_missed_24)
                    .setCategory(NotificationCompat.CATEGORY_CALL)
                    .setOnlyAlertOnce(true)
                    .setAutoCancel(true)
                    .setContentText(call.getContact().getDisplayName())
                    .setContentIntent(PendingIntent.getService(mContext, random.nextInt(), intentConversation, 0))
                    .setColor(ResourcesCompat.getColor(mContext.getResources(), R.color.color_primary_dark, null));
    
            setContactPicture(call.getContact(), messageNotificationBuilder);
            notificationManager.notify(notificationId, messageNotificationBuilder.build());
        }
    
        @Override
        public Object getServiceNotification() {
            Intent intentHome = new Intent(Intent.ACTION_VIEW)
                    .setClass(mContext, HomeActivity.class)
                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            PendingIntent pendIntent = PendingIntent.getActivity(mContext, 0, intentHome, PendingIntent.FLAG_UPDATE_CURRENT);
            NotificationCompat.Builder messageNotificationBuilder = new NotificationCompat.Builder(mContext, NotificationServiceImpl.NOTIF_CHANNEL_SERVICE);
            messageNotificationBuilder
                    .setContentTitle(mContext.getText(R.string.app_name))
                    .setContentText(mContext.getText(R.string.notif_background_service))
                    .setSmallIcon(R.drawable.ic_ring_logo_white)
                    .setContentIntent(pendIntent)
                    .setVisibility(NotificationCompat.VISIBILITY_SECRET)
                    .setPriority(NotificationCompat.PRIORITY_MIN)
                    .setOngoing(true)
                    .setCategory(NotificationCompat.CATEGORY_SERVICE);
            return messageNotificationBuilder.build();
        }
    
        @Override
        public void cancelTextNotification(Uri contact) {
            if (contact == null) {
                return;
            }
            int notificationId = getTextNotificationId(contact);
            notificationManager.cancel(notificationId);
            mNotificationBuilders.remove(notificationId);
        }
    
        public void cancelTextNotification(String ringId) {
            int notificationId = (NOTIF_MSG + ringId).hashCode();
            notificationManager.cancel(notificationId);
            mNotificationBuilders.remove(notificationId);
        }
    
        @Override
        public void cancelTrustRequestNotification(String accountID) {
            if (accountID == null) {
                return;
            }
            int notificationId = getIncomingTrustNotificationId(accountID);
            notificationManager.cancel(notificationId);
        }
    
        @Override
        public void cancelCallNotification() {
            notificationManager.cancel(NOTIF_CALL_ID);
            mNotificationBuilders.remove(NOTIF_CALL_ID);
        }
    
        /**\
         * Cancels a notification
         * @param notificationId the notification ID
         * @param isMigratingToService true if the notification is being updated to be a part of the foreground service
         */
        @Override
        public void cancelFileNotification(int notificationId, boolean isMigratingToService) {
            notificationManager.cancel(notificationId);
            if(!isMigratingToService)
                mNotificationBuilders.remove(notificationId);
        }
    
        @Override
        public void cancelAll() {
            notificationManager.cancelAll();
            mNotificationBuilders.clear();
        }
    
        private int getIncomingTrustNotificationId(String accountId) {
            return (NOTIF_TRUST_REQUEST + accountId).hashCode();
        }
    
        private int getTextNotificationId(Uri contact) {
            return (NOTIF_MSG + contact.toString()).hashCode();
        }
    
        private int getFileTransferNotificationId(long dataTransferId) {
            return (NOTIF_FILE_TRANSFER + dataTransferId).hashCode();
        }
    
        private Bitmap getContactPicture(CallContact contact) {
            try {
                return AvatarFactory.getBitmapAvatar(mContext, contact, avatarSize).blockingGet();
            } catch (Exception e) {
                return null;
            }
        }
    
        private Bitmap getContactPicture(Account account) {
            return AvatarFactory.getBitmapAvatar(mContext, account, avatarSize).blockingGet();
        }
    
        private void setContactPicture(CallContact contact, NotificationCompat.Builder messageNotificationBuilder) {
            Bitmap pic = getContactPicture(contact);
            if (pic != null)
                messageNotificationBuilder.setLargeIcon(pic);
        }
    }