diff --git a/art/ic_missed_call_notification.svg b/art/ic_missed_call_notification.svg
new file mode 100644
index 000000000..78f0acead
--- /dev/null
+++ b/art/ic_missed_call_notification.svg
@@ -0,0 +1,344 @@
+
+
+
+
diff --git a/art/render.rb b/art/render.rb
index 7fb46d138..7ae4ac8ae 100755
--- a/art/render.rb
+++ b/art/render.rb
@@ -28,6 +28,7 @@ images = {
'conversations_mono.svg' => ['conversations/ic_notification', 24],
'quicksy_mono.svg' => ['quicksy/ic_notification', 24],
'flip_camera_android-black-24dp.svg' => ['ic_flip_camera_android_black_24dp', 24],
+ 'ic_missed_call_notification.svg' => ['ic_missed_call_notification', 24],
'ic_send_text_offline.svg' => ['ic_send_text_offline', 36],
'ic_send_text_offline_white.svg' => ['ic_send_text_offline_white', 36],
'ic_send_text_online.svg' => ['ic_send_text_online', 36],
@@ -119,7 +120,7 @@ images.each do |source_filename, settings|
else
path = "../src/#{output_parts[0]}/res/drawable-#{resolution}/#{output_parts[1]}.png"
end
- execute_cmd "#{inkscape} #{source_filename} -C -w #{width} -h #{height} -o #{path}"
+ execute_cmd "#{inkscape} #{source_filename} -C -w #{width} -h #{height} -e #{path}"
top = []
right = []
diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java
index 4a825fbb3..8bb65cc0f 100644
--- a/src/main/java/eu/siacs/conversations/entities/Conversation.java
+++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java
@@ -241,11 +241,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 c9b932415..b6916020d 100644
--- a/src/main/java/eu/siacs/conversations/services/NotificationService.java
+++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java
@@ -90,17 +90,20 @@ public class NotificationService {
private static final long[] CALL_PATTERN = {0, 500, 300, 600};
- 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;
- private static final int DELIVERY_FAILED_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 12;
+ public static final int MISSED_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 12;
+ private static final int DELIVERY_FAILED_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 13;
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;
@@ -221,6 +224,16 @@ 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",
@@ -284,12 +297,18 @@ public class NotificationService {
notificationManager.createNotificationChannel(deliveryFailedChannel);
}
- private 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() {
@@ -320,12 +339,16 @@ 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);
+ }
}
}
@@ -360,6 +383,9 @@ public class NotificationService {
updateNotification(count > 0, conversations);
}
}
+ synchronized (mMissedCalls) {
+ updateMissedCallNotifications(mMissedCalls.keySet());
+ }
}
private List getBacklogConversations(Account account) {
@@ -666,7 +692,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()
@@ -695,7 +721,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);
@@ -705,7 +753,7 @@ public class NotificationService {
}
}
- public void clear(final Conversation conversation) {
+ public void clearMessages(final Conversation conversation) {
synchronized (this.mBacklogMessageCounter) {
this.mBacklogMessageCounter.remove(conversation);
}
@@ -718,6 +766,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()));
}
@@ -797,7 +864,7 @@ public class NotificationService {
}
modifyForSoundVibrationAndLight(
singleBuilder, notifyThis, quiteHours, preferences);
- singleBuilder.setGroup(CONVERSATIONS_GROUP);
+ singleBuilder.setGroup(MESSAGES_GROUP);
setNotificationColor(singleBuilder);
notify(entry.getKey(), NOTIFICATION_ID, singleBuilder.build());
}
@@ -807,6 +874,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();
@@ -867,6 +959,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_missed_call_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_missed_call_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(
@@ -932,7 +1119,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;
@@ -1336,7 +1523,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(
@@ -1356,6 +1543,16 @@ public class NotificationService {
: PendingIntent.FLAG_UPDATE_CURRENT);
}
+ 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(
final Conversation conversation,
final String lastMessageUuid,
@@ -1677,6 +1874,28 @@ public class NotificationService {
}
}
+ 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;
+ }
+ }
private class VibrationRunnable implements Runnable {
@Override
diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
index 79da6d551..245454247 100644
--- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
+++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
@@ -175,7 +175,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";
@@ -670,19 +671,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;
@@ -769,7 +786,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:
@@ -1954,7 +1971,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(mNotificationService::pushFromBacklog);
+ conversation.findUnreadMessagesAndCalls(mNotificationService::pushFromBacklog);
}
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 353851c37..c69fc6b02 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java
@@ -1110,6 +1110,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
rejectCallFromSessionInitiate();
break;
}
+ xmppConnectionService.getNotificationService().pushMissedCallNow(message);
}
private void cancelRingingTimeout() {
@@ -1187,6 +1188,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
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()
diff --git a/src/main/res/drawable-hdpi/ic_missed_call_notification.png b/src/main/res/drawable-hdpi/ic_missed_call_notification.png
new file mode 100644
index 000000000..3608ebd92
Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_missed_call_notification.png differ
diff --git a/src/main/res/drawable-mdpi/ic_missed_call_notification.png b/src/main/res/drawable-mdpi/ic_missed_call_notification.png
new file mode 100644
index 000000000..9c6c37da0
Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_missed_call_notification.png differ
diff --git a/src/main/res/drawable-xhdpi/ic_missed_call_notification.png b/src/main/res/drawable-xhdpi/ic_missed_call_notification.png
new file mode 100644
index 000000000..80cd15819
Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_missed_call_notification.png differ
diff --git a/src/main/res/drawable-xxhdpi/ic_missed_call_notification.png b/src/main/res/drawable-xxhdpi/ic_missed_call_notification.png
new file mode 100644
index 000000000..0072d2ef0
Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_missed_call_notification.png differ
diff --git a/src/main/res/drawable-xxxhdpi/ic_missed_call_notification.png b/src/main/res/drawable-xxxhdpi/ic_missed_call_notification.png
new file mode 100644
index 000000000..b4343bb10
Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_missed_call_notification.png differ
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index 53fb4871d..c51be907e 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -767,6 +767,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).
Failed deliveries
@@ -934,6 +935,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