diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 68ad4d316cb1b17136a067c878fa7341312e9516..215402f41a43979aaf9abd54d4e90785893cbcf0 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -227,11 +227,11 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } } - public void findUnreadMessages(OnMessageFound onMessageFound) { + public void findUnreadMessagesAndCalls(OnMessageFound onMessageFound) { final ArrayList results = new ArrayList<>(); synchronized (this.messages) { for (final Message message : this.messages) { - if (message.isRead() || message.getType() == Message.TYPE_RTP_SESSION) { + if (message.isRead()) { continue; } results.add(message); diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index 2e64815f5deb96604645ffde3996592ea2aff5fb..0cea08e6e37e4c724d873d1f5c3ddac1e231477b 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -75,16 +75,19 @@ public class NotificationService { private static final int CALL_DAT = 120; private static final long[] CALL_PATTERN = {0, 3 * CALL_DAT, CALL_DAT, CALL_DAT, 3 * CALL_DAT, CALL_DAT, CALL_DAT}; - private static final String CONVERSATIONS_GROUP = "eu.siacs.conversations"; + private static final String MESSAGES_GROUP = "eu.siacs.conversations.messages"; + private static final String MISSED_CALLS_GROUP = "eu.siacs.conversations.missed_calls"; private static final int NOTIFICATION_ID_MULTIPLIER = 1024 * 1024; static final int FOREGROUND_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 4; private static final int NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 2; private static final int ERROR_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 6; private static final int INCOMING_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 8; public static final int ONGOING_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 10; + public static final int MISSED_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 12; private final XmppConnectionService mXmppConnectionService; private final LinkedHashMap> notifications = new LinkedHashMap<>(); private final HashMap mBacklogMessageCounter = new HashMap<>(); + private final LinkedHashMap mMissedCalls = new LinkedHashMap<>(); private Conversation mOpenConversation; private boolean mIsInForeground; private long mLastNotification; @@ -182,6 +185,15 @@ public class NotificationService { ongoingCallsChannel.setGroup("calls"); notificationManager.createNotificationChannel(ongoingCallsChannel); + final NotificationChannel missedCallsChannel = new NotificationChannel("missed_calls", + c.getString(R.string.missed_calls_channel_name), + NotificationManager.IMPORTANCE_HIGH); + missedCallsChannel.setShowBadge(true); + missedCallsChannel.setSound(null, null); + missedCallsChannel.setLightColor(LED_COLOR); + missedCallsChannel.enableLights(true); + missedCallsChannel.setGroup("calls"); + notificationManager.createNotificationChannel(missedCallsChannel); final NotificationChannel messagesChannel = new NotificationChannel("messages", c.getString(R.string.messages_channel_name), @@ -222,12 +234,18 @@ public class NotificationService { notificationManager.createNotificationChannel(quietHoursChannel); } - public boolean notify(final Message message) { + private boolean notifyMessage(final Message message) { final Conversation conversation = (Conversation) message.getConversation(); return message.getStatus() == Message.STATUS_RECEIVED && !conversation.isMuted() && (conversation.alwaysNotify() || wasHighlightedOrPrivate(message)) - && (!conversation.isWithStranger() || notificationsFromStrangers()); + && (!conversation.isWithStranger() || notificationsFromStrangers()) + && message.getType() != Message.TYPE_RTP_SESSION; + } + + private boolean notifyMissedCall(final Message message) { + return message.getType() == Message.TYPE_RTP_SESSION + && message.getStatus() == Message.STATUS_RECEIVED; } public boolean notificationsFromStrangers() { @@ -251,11 +269,15 @@ public class NotificationService { } public void pushFromBacklog(final Message message) { - if (notify(message)) { + if (notifyMessage(message)) { synchronized (notifications) { getBacklogMessageCounter((Conversation) message.getConversation()).incrementAndGet(); pushToStack(message); } + } else if (notifyMissedCall(message)) { + synchronized (mMissedCalls) { + pushMissedCall(message); + } } } @@ -290,6 +312,9 @@ public class NotificationService { updateNotification(count > 0, conversations); } } + synchronized (mMissedCalls) { + updateMissedCallNotifications(mMissedCalls.keySet()); + } } private List getBacklogConversations(Account account) { @@ -437,7 +462,7 @@ public class NotificationService { private void pushNow(final Message message) { mXmppConnectionService.updateUnreadCountBadge(); - if (!notify(message)) { + if (!notifyMessage(message)) { Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": suppressing notification because turned off"); return; } @@ -457,7 +482,29 @@ public class NotificationService { } } - public void clear() { + private void pushMissedCall(final Message message) { + final Conversational conversation = message.getConversation(); + final MissedCallsInfo info = mMissedCalls.get(conversation); + if (info == null) { + mMissedCalls.put(conversation, new MissedCallsInfo(message.getTimeSent())); + } else { + info.newMissedCall(message.getTimeSent()); + } + } + + public void pushMissedCallNow(final Message message) { + synchronized (mMissedCalls) { + pushMissedCall(message); + updateMissedCallNotifications(Collections.singleton(message.getConversation())); + } + } + + public void clear(final Conversation conversation) { + clearMessages(conversation); + clearMissedCalls(conversation); + } + + public void clearMessages() { synchronized (notifications) { for (ArrayList messages : notifications.values()) { markAsReadIfHasDirectReply(messages); @@ -467,7 +514,7 @@ public class NotificationService { } } - public void clear(final Conversation conversation) { + public void clearMessages(final Conversation conversation) { synchronized (this.mBacklogMessageCounter) { this.mBacklogMessageCounter.remove(conversation); } @@ -480,6 +527,25 @@ public class NotificationService { } } + public void clearMissedCalls() { + synchronized (mMissedCalls) { + for (final Conversational conversation : mMissedCalls.keySet()) { + cancel(conversation.getUuid(), MISSED_CALL_NOTIFICATION_ID); + } + mMissedCalls.clear(); + updateMissedCallNotifications(null); + } + } + + public void clearMissedCalls(final Conversation conversation) { + synchronized (mMissedCalls) { + if (mMissedCalls.remove(conversation) != null) { + cancel(conversation.getUuid(), MISSED_CALL_NOTIFICATION_ID); + updateMissedCallNotifications(null); + } + } + } + private void markAsReadIfHasDirectReply(final Conversation conversation) { markAsReadIfHasDirectReply(notifications.get(conversation.getUuid())); } @@ -547,7 +613,7 @@ public class NotificationService { singleBuilder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY); } modifyForSoundVibrationAndLight(singleBuilder, notifyThis, quiteHours, preferences); - singleBuilder.setGroup(CONVERSATIONS_GROUP); + singleBuilder.setGroup(MESSAGES_GROUP); setNotificationColor(singleBuilder); notify(entry.getKey(), NOTIFICATION_ID, singleBuilder.build()); } @@ -557,6 +623,31 @@ public class NotificationService { } } + private void updateMissedCallNotifications(final Set update) { + if (mMissedCalls.isEmpty()) { + cancel(MISSED_CALL_NOTIFICATION_ID); + return; + } + if (mMissedCalls.size() == 1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + final Conversational conversation = mMissedCalls.keySet().iterator().next(); + final MissedCallsInfo info = mMissedCalls.values().iterator().next(); + final Notification notification = missedCall(conversation, info); + notify(MISSED_CALL_NOTIFICATION_ID, notification); + } else { + final Notification summary = missedCallsSummary(); + notify(MISSED_CALL_NOTIFICATION_ID, summary); + if (update != null) { + for (final Conversational conversation : update) { + final MissedCallsInfo info = mMissedCalls.get(conversation); + if (info != null) { + final Notification notification = missedCall(conversation, info); + notify(conversation.getUuid(), MISSED_CALL_NOTIFICATION_ID, notification); + } + } + } + } + } + private void modifyForSoundVibrationAndLight(Builder mBuilder, boolean notify, boolean quietHours, SharedPreferences preferences) { final Resources resources = mXmppConnectionService.getResources(); final String ringtone = preferences.getString("notification_ringtone", resources.getString(R.string.notification_ringtone)); @@ -615,6 +706,101 @@ public class NotificationService { } } + private Notification missedCallsSummary() { + final Builder publicBuilder = buildMissedCallsSummary(true); + final Builder builder = buildMissedCallsSummary(false); + builder.setPublicVersion(publicBuilder.build()); + return builder.build(); + } + + private Builder buildMissedCallsSummary(boolean publicVersion) { + final Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "missed_calls"); + int totalCalls = 0; + final StringBuilder names = new StringBuilder(); + long lastTime = 0; + for (Map.Entry entry : mMissedCalls.entrySet()) { + final Conversational conversation = entry.getKey(); + final MissedCallsInfo missedCallsInfo = entry.getValue(); + names.append(conversation.getContact().getDisplayName()); + names.append(", "); + totalCalls += missedCallsInfo.getNumberOfCalls(); + lastTime = Math.max(lastTime, missedCallsInfo.getLastTime()); + } + if (names.length() >= 2) { + names.delete(names.length() - 2, names.length()); + } + final String title = (totalCalls == 1) ? mXmppConnectionService.getString(R.string.missed_call) : + (mMissedCalls.size() == 1) ? mXmppConnectionService.getString(R.string.n_missed_calls, totalCalls) : + mXmppConnectionService.getString(R.string.n_missed_calls_from_m_contacts, totalCalls, mMissedCalls.size()); + builder.setContentTitle(title); + builder.setTicker(title); + if (!publicVersion) { + builder.setContentText(names.toString()); + } + builder.setSmallIcon(R.drawable.ic_notification); + builder.setGroupSummary(true); + builder.setGroup(MISSED_CALLS_GROUP); + builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN); + builder.setCategory(NotificationCompat.CATEGORY_CALL); + builder.setWhen(lastTime); + if (!mMissedCalls.isEmpty()) { + final Conversational firstConversation = mMissedCalls.keySet().iterator().next(); + builder.setContentIntent(createContentIntent(firstConversation)); + } + builder.setDeleteIntent(createMissedCallsDeleteIntent(null)); + modifyMissedCall(builder); + return builder; + } + + private Notification missedCall(final Conversational conversation, final MissedCallsInfo info) { + final Builder publicBuilder = buildMissedCall(conversation, info, true); + final Builder builder = buildMissedCall(conversation, info, false); + builder.setPublicVersion(publicBuilder.build()); + return builder.build(); + } + + private Builder buildMissedCall(final Conversational conversation, final MissedCallsInfo info, boolean publicVersion) { + final Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "missed_calls"); + final String title = (info.getNumberOfCalls() == 1) ? mXmppConnectionService.getString(R.string.missed_call) : + mXmppConnectionService.getString(R.string.n_missed_calls, info.getNumberOfCalls()); + builder.setContentTitle(title); + final String name = conversation.getContact().getDisplayName(); + if (publicVersion) { + builder.setTicker(title); + } else { + if (info.getNumberOfCalls() == 1) { + builder.setTicker(mXmppConnectionService.getString(R.string.missed_call_from_x, name)); + } else { + builder.setTicker(mXmppConnectionService.getString(R.string.n_missed_calls_from_x, info.getNumberOfCalls(), name)); + } + builder.setContentText(name); + } + builder.setSmallIcon(R.drawable.ic_notification); + builder.setGroup(MISSED_CALLS_GROUP); + builder.setCategory(NotificationCompat.CATEGORY_CALL); + builder.setWhen(info.getLastTime()); + builder.setContentIntent(createContentIntent(conversation)); + builder.setDeleteIntent(createMissedCallsDeleteIntent(conversation)); + if (!publicVersion && conversation instanceof Conversation) { + builder.setLargeIcon(mXmppConnectionService.getAvatarService() + .get((Conversation) conversation, AvatarService.getSystemUiAvatarSize(mXmppConnectionService))); + } + modifyMissedCall(builder); + return builder; + } + + private void modifyMissedCall(final Builder builder) { + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService); + final Resources resources = mXmppConnectionService.getResources(); + final boolean led = preferences.getBoolean("led", resources.getBoolean(R.bool.led)); + if (led) { + builder.setLights(LED_COLOR, 2000, 3000); + } + builder.setPriority(NotificationCompat.PRIORITY_HIGH); + builder.setSound(null); + setNotificationColor(builder); + } + private Builder buildMultipleConversation(final boolean notify, final boolean quietHours) { final Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService, quietHours ? "quiet_hours" : (notify ? "messages" : "silent_messages")); final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle(); @@ -651,7 +837,7 @@ public class NotificationService { mBuilder.setContentIntent(createContentIntent(conversation)); } mBuilder.setGroupSummary(true); - mBuilder.setGroup(CONVERSATIONS_GROUP); + mBuilder.setGroup(MESSAGES_GROUP); mBuilder.setDeleteIntent(createDeleteIntent(null)); mBuilder.setSmallIcon(R.drawable.ic_notification); return mBuilder; @@ -952,7 +1138,7 @@ public class NotificationService { private PendingIntent createDeleteIntent(Conversation conversation) { final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); - intent.setAction(XmppConnectionService.ACTION_CLEAR_NOTIFICATION); + intent.setAction(XmppConnectionService.ACTION_CLEAR_MESSAGE_NOTIFICATION); if (conversation != null) { intent.putExtra("uuid", conversation.getUuid()); return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 20), intent, 0); @@ -960,6 +1146,16 @@ public class NotificationService { return PendingIntent.getService(mXmppConnectionService, 0, intent, 0); } + private PendingIntent createMissedCallsDeleteIntent(final Conversational conversation) { + final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); + intent.setAction(XmppConnectionService.ACTION_CLEAR_MISSED_CALL_NOTIFICATION); + if (conversation != null) { + intent.putExtra("uuid", conversation.getUuid()); + return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 21), intent, 0); + } + return PendingIntent.getService(mXmppConnectionService, 1, intent, 0); + } + private PendingIntent createReplyIntent(Conversation conversation, boolean dismissAfterReply) { final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); intent.setAction(XmppConnectionService.ACTION_REPLY_TO_CONVERSATION); @@ -1189,4 +1385,27 @@ public class NotificationService { Log.d(Config.LOGTAG, "unable to cancel notification", e); } } + + private static class MissedCallsInfo { + private int numberOfCalls; + private long lastTime; + + MissedCallsInfo(final long time) { + numberOfCalls = 1; + lastTime = time; + } + + public void newMissedCall(final long time) { + ++numberOfCalls; + lastTime = time; + } + + public int getNumberOfCalls() { + return numberOfCalls; + } + + public long getLastTime() { + return lastTime; + } + } } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 8bc43c99fc127843a6225e46dd74675cb872799d..015474fd62b7f9803504fb22520530aff49d03a1 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -162,7 +162,8 @@ public class XmppConnectionService extends Service { public static final String ACTION_REPLY_TO_CONVERSATION = "reply_to_conversations"; public static final String ACTION_MARK_AS_READ = "mark_as_read"; public static final String ACTION_SNOOZE = "snooze"; - public static final String ACTION_CLEAR_NOTIFICATION = "clear_notification"; + public static final String ACTION_CLEAR_MESSAGE_NOTIFICATION = "clear_message_notification"; + public static final String ACTION_CLEAR_MISSED_CALL_NOTIFICATION = "clear_missed_call_notification"; public static final String ACTION_DISMISS_ERROR_NOTIFICATIONS = "dismiss_error"; public static final String ACTION_TRY_AGAIN = "try_again"; public static final String ACTION_IDLE_PING = "idle_ping"; @@ -632,19 +633,35 @@ public class XmppConnectionService extends Service { case Intent.ACTION_SHUTDOWN: logoutAndSave(true); return START_NOT_STICKY; - case ACTION_CLEAR_NOTIFICATION: + case ACTION_CLEAR_MESSAGE_NOTIFICATION: mNotificationExecutor.execute(() -> { try { final Conversation c = findConversationByUuid(uuid); if (c != null) { - mNotificationService.clear(c); + mNotificationService.clearMessages(c); } else { - mNotificationService.clear(); + mNotificationService.clearMessages(); } restoredFromDatabaseLatch.await(); } catch (InterruptedException e) { - Log.d(Config.LOGTAG, "unable to process clear notification"); + Log.d(Config.LOGTAG, "unable to process clear message notification"); + } + }); + break; + case ACTION_CLEAR_MISSED_CALL_NOTIFICATION: + mNotificationExecutor.execute(() -> { + try { + final Conversation c = findConversationByUuid(uuid); + if (c != null) { + mNotificationService.clearMissedCalls(c); + } else { + mNotificationService.clearMissedCalls(); + } + restoredFromDatabaseLatch.await(); + + } catch (InterruptedException e) { + Log.d(Config.LOGTAG, "unable to process clear missed call notification"); } }); break; @@ -722,7 +739,7 @@ public class XmppConnectionService extends Service { return; } c.setMutedTill(System.currentTimeMillis() + 30 * 60 * 1000); - mNotificationService.clear(c); + mNotificationService.clearMessages(c); updateConversation(c); }); case AudioManager.RINGER_MODE_CHANGED_ACTION: @@ -1819,7 +1836,7 @@ public class XmppConnectionService extends Service { private void restoreMessages(Conversation conversation) { conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE)); conversation.findUnsentTextMessages(message -> markMessage(message, Message.STATUS_WAITING)); - conversation.findUnreadMessages(message -> mNotificationService.pushFromBacklog(message)); + conversation.findUnreadMessagesAndCalls(message -> mNotificationService.pushFromBacklog(message)); } public void loadPhoneContacts() { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index bc3487ad67fbf6a681e2dd5d9b87109be324d3ce..4d90b2bf722c6718d88c7fb05f38569746848e1b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -627,6 +627,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web final State target = this.state == State.PROCEED ? State.RETRACTED_RACED : State.RETRACTED; if (transition(target)) { xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); + xmppConnectionService.getNotificationService().pushMissedCallNow(message); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": session with " + id.with + " has been retracted (serverMsgId=" + serverMsgId + ")"); if (serverMsgId != null) { this.message.setServerMsgId(serverMsgId); diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index c6a77aaa9fe3f7aefda8db3e57fa1028892e7fb4..9c7f5238d623996beff7e39937a88e72e0285b81 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -754,6 +754,7 @@ Messages Incoming calls Ongoing calls + Missed calls Silent messages This notification group is used to display notifications that should not trigger any sound. For example when being active on another device (Grace Period). Message notification settings @@ -915,6 +916,10 @@ Outgoing call Outgoing call ยท %s Missed call + Missed call from %s + %1$d missed calls from %2$s + %d missed calls + %1$d missed calls from %2$d contacts Audio call Video call Help