From a6b88ba9e95deaeeeff6a4433d1e79a28d74cd25 Mon Sep 17 00:00:00 2001 From: Dmitry Markin Date: Mon, 29 Aug 2022 13:41:35 +0300 Subject: [PATCH] Add missed call notifications Co-authored-by: Daniel Gultsch --- art/ic_missed_call_notification.svg | 344 ++++++++++++++++++ art/render.rb | 3 +- .../conversations/entities/Conversation.java | 4 +- .../services/NotificationService.java | 241 +++++++++++- .../services/XmppConnectionService.java | 31 +- .../xmpp/jingle/JingleRtpConnection.java | 2 + .../ic_missed_call_notification.png | Bin 0 -> 810 bytes .../ic_missed_call_notification.png | Bin 0 -> 589 bytes .../ic_missed_call_notification.png | Bin 0 -> 1151 bytes .../ic_missed_call_notification.png | Bin 0 -> 1680 bytes .../ic_missed_call_notification.png | Bin 0 -> 2179 bytes src/main/res/values/strings.xml | 5 + 12 files changed, 609 insertions(+), 21 deletions(-) create mode 100644 art/ic_missed_call_notification.svg create mode 100644 src/main/res/drawable-hdpi/ic_missed_call_notification.png create mode 100644 src/main/res/drawable-mdpi/ic_missed_call_notification.png create mode 100644 src/main/res/drawable-xhdpi/ic_missed_call_notification.png create mode 100644 src/main/res/drawable-xxhdpi/ic_missed_call_notification.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_missed_call_notification.png 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + 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 0000000000000000000000000000000000000000..3608ebd92478ee32c52ad4feba3b261218ebc50d GIT binary patch literal 810 zcmV+_1J(SAP)oxe((33 z_x;c1Kk%Os%1%%NuoBn+tN`W%GXR12KtJ#lxF16JP(RhNBJJui^_9i=soqhy)?1`S zJ+6K$D3Zkkb$vZ#7OKxCAd+TSJ*Q5uD$}LDtCLKcyXwrcGOL|fi}JXyPAe+YsSZt| zOv2S7GR^9f2FN7r%@aDg=>bQ%a@8{Iq-9HsW>?#QYd}Zpa8@fu$f!-gWnfiC+{pFSJ>!)7 zOdQi>^y-1STzy@nO{uw8#)Pg?w6pvuX$-yaYA_Z%R~V_c*zmNni15Pk{=x z#rwTEl`2!2gzuwUwzh=Ocv4klR0l>2wHo!Ys8E*FePfy!C(PnPEVC$M?2l9` zPgxn&(^*1?OD0LU)$V&>2*xt5men4R43~u;iKlQXi?eanvk(!Uty+Z`#3n@)3yp#bDn2DYi`|9EFzXxg-f5P-bMHC# z+}W886hhqui~@gvk0FG=rM3SfJ+9tY-@6(s>YRE*omdO;LG`I+_^sYmx0aI6sNV|7 zV=Sl#`jJo99jz4~)Uh`5ed^Z%$Q!&+*X0Cehd|!oLc<%HO$FXG??b>6us?+0`Z{nn zAzuU@WeRr#=QHs+U;@}yzwZT(0j~g1yNSiexwh1!Ei>P)o=)P|02&8-+Ptb$z$~yC z*pMv01WwlRo5|XEO!6t`Ks{AIPpCgyI^Sh=22wl#7^yXuTMS%MJ0XPUzzpy+(|H$| z%GCaf1(w=I1YAu-x&`C_@8S_%PVCLA7uDBMtP|z8jU~pJSYS-;^(j#86q0{ex2*D` z-YyVO{Zf!~ui+huG_LpAiv~Liaz?XJ^>A%(P%NwYYezkzE)GHDP&y(2LJ0Fz_sAWf zS1^t?O!v)D$JHz9Gpqk{&+0_KRi9XW-jnJ%tNUyva`13~=2{z?RBx%D)rFx8L_!!cl58m?00000NkvXXu0mjfUpoA) literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..80cd15819fcca113e91cd6a57468a3062926bfed GIT binary patch literal 1151 zcmV-_1c3XAP)dAf*T*3p6S`T|i9B zAdVfPHxWXzpo}6asy`rl&@Mz52ASp^8>>}brSqec?BB4RTxkz&Ie0o@W+3fAU zthM*r``8CItToL1=KGzQH8Xn#{^vy10o6-^<-h`9F>o4~0geJkB4W16`g%5?UI9D+ z+zH$YT+$}{EN~Fm1AGR29T8`G)leEW)b;9@>KVJ|nEJAM*&xN=t{&(?bTejZMlSBB z@DcSbb*^IZDM!@X`XPRWdZ>@WQ)bm?suaIU{c}L#Q{GUAdli3!dU77b=e(zOH&d$1 z)D!b4KIf%w#9yd>GvCT<%&8k&X&q{j{ycC?Y3KbRfH&18g>swCIDgM?qc`QXR_+=N zdho5a3B*X^GzxCB#hu_W<8@$hS6qxgdESD^SU)h&WkPk$S3N)+u0J zMC?`f0=t0KE%M%xzBKm0d~jAgKtxOeZ@0-mYO*GzJ+Pvi+RZjxuK?FItE@xuI`Or5 z4+5`uD7PH=zEQ}*0*lEbElgal{#bHhnpW2|#2Gi+PIqE`UM3wvBOP2fie6Tmxrm4p zz}>*l1&dDsYa?P$5^nJLQQF$P%^y+&2f7H9(yAx+QC-VJ){}qaFKK|U0O{?t zZfi~8qY+_599LIIgn5RV_-k@iDNY--^0mO=CiOESpyF^;`A0_1lL@^*42w`l`CgCU?W?kh)I&$o6}u zrzD71VTUS@8SJlk)??!*n{0mB(A6Sh= RVebF{002ovPDHLkV1lNL8CC!Q literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0072d2ef0d9253358efbc88d9ed7882b77e27be1 GIT binary patch literal 1680 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 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