From 451ea1dbad903df14044caf5705f4530c63152ce Mon Sep 17 00:00:00 2001 From: cyBerta Date: Wed, 29 May 2019 12:07:37 +0200 Subject: [PATCH] Don't use notification's / notification channel's sound and vibration functionality at all. Instead we're handling sound and vibration alerts separately. It allows us to dynamically change the 'signal' flag turning on and off sounds on Android O+ without posting into different channels for loud and silent notifications. This way we keep the grouped notification view instead of falling back to the pre API 23 summary notificatoin view. --- .../AbstractNotificationBuilder.java | 72 +- .../notifications/MessageNotifier.java | 660 +++++++++--------- .../notifications/MessageNotifierApi23.java | 56 ++ .../notifications/MessageNotifierCompat.java | 2 +- .../MessageNotifierPreApi23.java | 4 +- 5 files changed, 428 insertions(+), 366 deletions(-) create mode 100644 src/org/thoughtcrime/securesms/notifications/MessageNotifierApi23.java diff --git a/src/org/thoughtcrime/securesms/notifications/AbstractNotificationBuilder.java b/src/org/thoughtcrime/securesms/notifications/AbstractNotificationBuilder.java index ca8b1ecd5..6f50e8409 100644 --- a/src/org/thoughtcrime/securesms/notifications/AbstractNotificationBuilder.java +++ b/src/org/thoughtcrime/securesms/notifications/AbstractNotificationBuilder.java @@ -24,16 +24,19 @@ import java.math.BigInteger; import java.security.MessageDigest; import java.util.List; -public abstract class AbstractNotificationBuilder extends NotificationCompat.Builder { +abstract class AbstractNotificationBuilder extends NotificationCompat.Builder { @SuppressWarnings("unused") private static final String TAG = AbstractNotificationBuilder.class.getSimpleName(); protected Context context; protected NotificationPrivacyPreference privacy; + private int notificationId; + private Uri ringtone; + private boolean vibrate; AbstractNotificationBuilder(Context context, NotificationPrivacyPreference privacy, boolean signal) { - super(context, createMsgNotificationChannel(context, signal)); + super(context, createMsgNotificationChannel(context)); this.context = context; this.privacy = privacy; @@ -50,18 +53,17 @@ public abstract class AbstractNotificationBuilder extends NotificationCompat.Bui return builder; } + // Alarms are not set in the notification or the notification channel but handled separately + // by the MessageNotifier. It allows us to dynamically turn on and off the sounds and as well as + // to change vibration and sounds during runtime void setAlarms(@Nullable Uri ringtone, Prefs.VibrateState vibrate) { Uri defaultRingtone = Prefs.getNotificationRingtone(context); boolean defaultVibrate = Prefs.isNotificationVibrateEnabled(context); + if (ringtone == null && !TextUtils.isEmpty(defaultRingtone.toString())) this.ringtone = defaultRingtone; + else if (ringtone != null && !ringtone.toString().isEmpty()) this.ringtone = ringtone; - if (ringtone == null && !TextUtils.isEmpty(defaultRingtone.toString())) setSound(defaultRingtone); - else if (ringtone != null && !ringtone.toString().isEmpty()) setSound(ringtone); - - if (vibrate == Prefs.VibrateState.ENABLED || - (vibrate == Prefs.VibrateState.DEFAULT && defaultVibrate)) - { - setDefaults(Notification.DEFAULT_VIBRATE); - } + this.vibrate = (vibrate == Prefs.VibrateState.ENABLED || + (vibrate == Prefs.VibrateState.DEFAULT && defaultVibrate)); } private void setLed() { @@ -79,8 +81,8 @@ public abstract class AbstractNotificationBuilder extends NotificationCompat.Bui argb = Color.rgb(0xFF, 0xFF, 0xFF); } setLights(argb, - Integer.parseInt(blinkPatternArray[0]), - Integer.parseInt(blinkPatternArray[1])); + Integer.parseInt(blinkPatternArray[0]), + Integer.parseInt(blinkPatternArray[1])); } } @@ -106,10 +108,9 @@ public abstract class AbstractNotificationBuilder extends NotificationCompat.Bui // - NotificationChannels have default values that have a higher precedence as the Notification.Builder setting // - once created, NotificationChannels cannot be modified programmatically // - NotificationChannels can be deleted, however, on re-creation it becomes un-deleted with the old settings - // - the idea is that sound, led, vibrate is edited by the user - // via the ACTION_CHANNEL_NOTIFICATION_SETTINGS intent that takes the channelId + // - the idea is that sound and vibrate are handled outside of the scope of the notification channel - private static String createMsgNotificationChannel(Context context, boolean signal) { + private static String createMsgNotificationChannel(Context context) { String chBase = "ch_msg2_"; String chId = chBase + "unsupported"; @@ -119,16 +120,11 @@ public abstract class AbstractNotificationBuilder extends NotificationCompat.Bui // get all values we'll use as settings for the NotificationChannel String ledColor = Prefs.getNotificationLedColor(context); - boolean defaultVibrate = Prefs.isNotificationVibrateEnabled(context); - Uri ringtone = Prefs.getNotificationRingtone(context); // compute hash from these settings String hash = ""; MessageDigest md = MessageDigest.getInstance("SHA-256"); md.update(ledColor.getBytes()); - md.update(defaultVibrate ? (byte) 1 : (byte) 0); - md.update(ringtone.toString().getBytes()); - md.update(signal ? (byte) 1 : (byte) 0); hash = String.format("%X", new BigInteger(1, md.digest())).substring(0, 16); // get channel name @@ -139,7 +135,7 @@ public abstract class AbstractNotificationBuilder extends NotificationCompat.Bui notificationManager.deleteNotificationChannel(oldChId); } catch (Exception e) { - ; // channel not created before + // channel not created before } Prefs.setStringPreference(context, "ch_curr_" + chBase, chId); } @@ -157,7 +153,7 @@ public abstract class AbstractNotificationBuilder extends NotificationCompat.Bui // we cannot change the settings, however, this is handled by using different values for chId if(!channelExists) { NotificationChannel channel = new NotificationChannel(chId, - "New messages", NotificationManager.IMPORTANCE_DEFAULT); + "New messages", NotificationManager.IMPORTANCE_DEFAULT); channel.setDescription("Informs about new messages."); if (!ledColor.equals("none")) { @@ -174,19 +170,8 @@ public abstract class AbstractNotificationBuilder extends NotificationCompat.Bui channel.enableLights(false); } - if (signal) { - channel.enableVibration(defaultVibrate); - - if (!TextUtils.isEmpty(ringtone.toString())) { - channel.setSound(ringtone, - new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN) - .setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT) - .build()); - } - } else { - channel.setSound(null, null); - channel.enableVibration(false); - } + channel.setSound(null, null); + channel.enableVibration(false); notificationManager.createNotificationChannel(channel); } @@ -202,4 +187,21 @@ public abstract class AbstractNotificationBuilder extends NotificationCompat.Bui private static boolean notificationChannelsSupported() { return Build.VERSION.SDK_INT >= 26; } + + + public void setNotificationId(int notificationId) { + this.notificationId = notificationId; + } + + public int getNotificationId() { + return this.notificationId; + } + + public Uri getRingtone() { + return this.ringtone; + } + + public boolean getVibrate() { + return this.vibrate; + } } diff --git a/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java b/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java index 4065f2b95..2e8c03f2b 100644 --- a/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java +++ b/src/org/thoughtcrime/securesms/notifications/MessageNotifier.java @@ -1,27 +1,12 @@ -/* - * Copyright (C) 2011 Whisper Systems - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ package org.thoughtcrime.securesms.notifications; -import android.annotation.TargetApi; import android.app.NotificationManager; import android.content.Context; import android.media.AudioManager; +import android.media.RingtoneManager; import android.media.SoundPool; -import android.service.notification.StatusBarNotification; +import android.net.Uri; +import android.os.Vibrator; import android.support.annotation.NonNull; import android.support.v4.app.NotificationManagerCompat; import android.text.TextUtils; @@ -46,334 +31,353 @@ import java.util.concurrent.TimeUnit; import static org.thoughtcrime.securesms.notifications.MessageNotifierCompat.NO_VISIBLE_CHAT_ID; import static org.thoughtcrime.securesms.notifications.MessageNotifierCompat.SUMMARY_NOTIFICATION_ID; +abstract class MessageNotifier implements IMessageNotifier { -/** - * Handles posting system notifications for new messages. - * - * - * @author Moxie Marlinspike - */ + static final String TAG = org.thoughtcrime.securesms.notifications.MessageNotifierApi23.class.getSimpleName(); -@TargetApi(23) -class MessageNotifier implements IMessageNotifier { + private static final String NOTIFICATION_GROUP = "messages"; + private static final long MIN_AUDIBLE_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(20); + private static final long STARTUP_SILENCE_DELTA = TimeUnit.MINUTES.toMillis(1); + private static final long INITIAL_STARTUP = System.currentTimeMillis(); - private static final String TAG = MessageNotifier.class.getSimpleName(); + static volatile int visibleChatId = NO_VISIBLE_CHAT_ID; + static volatile long lastAudibleNotification = -1; + final NotificationState notificationState; + final Context appContext; + private final SoundPool soundPool; + private final int soundIn; + private final int soundOut; + private boolean soundInLoaded; + private boolean soundOutLoaded; - private static final String NOTIFICATION_GROUP = "messages"; - private static final long MIN_AUDIBLE_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(20); - private static final long STARTUP_SILENCE_DELTA = TimeUnit.MINUTES.toMillis(1); - private static final long INITIAL_STARTUP = System.currentTimeMillis(); + MessageNotifier(Context context) { + appContext = context.getApplicationContext(); + soundPool = new SoundPool(3, AudioManager.STREAM_SYSTEM, 0); + soundIn = soundPool.load(context, R.raw.sound_in, 1); + soundOut = soundPool.load(context, R.raw.sound_out, 1); + notificationState = new NotificationState(); - static volatile int visibleChatId = NO_VISIBLE_CHAT_ID; - static volatile long lastAudibleNotification = -1; - final NotificationState notificationState; - final Context appContext; - private final SoundPool soundPool; - private final int soundIn; - private final int soundOut; - private boolean soundInLoaded; - private boolean soundOutLoaded; + soundPool.setOnLoadCompleteListener((soundPool, sampleId, status) -> { + if (status == 0) { + if (sampleId == soundIn) { + soundInLoaded = true; + } else if (sampleId == soundOut) { + soundOutLoaded = true; + } + } + }); + } - MessageNotifier(Context context) { - appContext = context.getApplicationContext(); - soundPool = new SoundPool(3, AudioManager.STREAM_SYSTEM, 0); - soundIn = soundPool.load(context, R.raw.sound_in, 1); - soundOut = soundPool.load(context, R.raw.sound_out, 1); - notificationState = new NotificationState(); - - soundPool.setOnLoadCompleteListener((soundPool, sampleId, status) -> { - if (status == 0) { - if (sampleId == soundIn) { - soundInLoaded = true; - } else if (sampleId == soundOut) { - soundOutLoaded = true; + @Override + public void playSendSound() { + if (Prefs.isInChatNotifications(appContext) && soundOutLoaded) { + soundPool.play(soundOut, 1.0f, 1.0f, 1, 0, 1.0f); } - } - }); - } - - @Override - public void playSendSound() { - if (Prefs.isInChatNotifications(appContext) && soundOutLoaded) { - soundPool.play(soundOut, 1.0f, 1.0f, 1, 0, 1.0f); - } - } - - @Override - public void updateVisibleChat(int chatId) { - visibleChatId = chatId; - if (visibleChatId != NO_VISIBLE_CHAT_ID) { - removeNotifications(visibleChatId); - } - } - - @Override - public void updateNotification(int chatId, int messageId) { - boolean isVisible = visibleChatId == chatId; - ApplicationDcContext dcContext = DcHelper.getContext(appContext); - - if (isVisible) { - dcContext.marknoticedChat(chatId); } - if (!Prefs.isNotificationsEnabled(appContext) || - Prefs.isChatMuted(appContext, chatId)) + @Override + public void updateVisibleChat(int chatId) { + visibleChatId = chatId; + if (visibleChatId != NO_VISIBLE_CHAT_ID) { + removeNotifications(visibleChatId); + } + } + + @Override + public void updateNotification(int chatId, int messageId) { + boolean isVisible = visibleChatId == chatId; + ApplicationDcContext dcContext = DcHelper.getContext(appContext); + + if (isVisible) { + dcContext.marknoticedChat(chatId); + } + + if (!Prefs.isNotificationsEnabled(appContext) || + Prefs.isChatMuted(appContext, chatId)) + { + return; + } + + if (isVisible) { + sendInChatNotification(chatId); + } else if (visibleChatId != NO_VISIBLE_CHAT_ID) { + //different chat + sendNotifications(chatId, messageId, false); + } else { + //app is in background or different Activity is on top + sendNotifications(chatId, messageId, true); + } + } + + /** + * On notification privacy preference changed, + * the notification state needs to be updated. + */ + @Override + public void onNotificationPrivacyChanged() { + if (!Prefs.isNotificationsEnabled(appContext)) { + return; + } + + clearNotifications(); + ApplicationDcContext dcContext = DcHelper.getContext(appContext); + int[] freshMessages = dcContext.getFreshMsgs(); + for (int message : freshMessages) { + DcMsg record = dcContext.getMsg(message); + updateNotification(record.getChatId(), record.getId()); + } + } + + @Override + public void removeNotifications(int[] chatIds) { + List removedItems = new LinkedList<>(); + for (int id : chatIds) { + removedItems.addAll(notificationState.removeNotificationsForChat(id)); + } + cancelNotifications(removedItems); + recreateSummaryNotification(); + } + + @Override + public void removeNotifications(int chatId) { + List removedItems = notificationState.removeNotificationsForChat(chatId); + cancelNotifications(removedItems); + recreateSummaryNotification(); + } + + void cancelNotifications(List removedItems) { + NotificationManager notifications = ServiceUtil.getNotificationManager(appContext); + for (NotificationItem item : removedItems) { + notifications.cancel(item.getId()); + } + } + + private void recreateSummaryNotification() { + NotificationManager notifications = ServiceUtil.getNotificationManager(appContext); + notifications.cancel(SUMMARY_NOTIFICATION_ID); + + if (notificationState.hasMultipleChats()) { + for (Integer id : notificationState.getChats()) { + sendSingleChatNotification(appContext, new NotificationState(notificationState.getNotificationsForChat(id)), false, true); + } + sendMultipleChatNotification(appContext, notificationState, false); + } else { + sendSingleChatNotification(appContext, notificationState, false, false); + } + } + + void cancelActiveNotifications() { + NotificationManager notifications = ServiceUtil.getNotificationManager(appContext); + notifications.cancel(SUMMARY_NOTIFICATION_ID); + } + + void sendNotifications(int chatId, int messageId, boolean signal) { + ApplicationDcContext dcContext = DcHelper.getContext(appContext); + if (signal = isSignalAllowed(signal)) { + lastAudibleNotification = System.currentTimeMillis(); + } + + addMessageToNotificationState(dcContext, chatId, messageId); + if (notificationState.hasMultipleChats()) { + for (int id : notificationState.getChats()) { + sendSingleChatNotification(appContext, new NotificationState(notificationState.getNotificationsForChat(id)), false, true); + } + sendMultipleChatNotification(appContext, notificationState, signal); + } else { + sendSingleChatNotification(appContext, notificationState, signal, false); + } + } + + boolean isSignalAllowed(boolean signalRequested) { + long now = System.currentTimeMillis(); + return signalRequested && ( + now - INITIAL_STARTUP) > STARTUP_SILENCE_DELTA && + (now - lastAudibleNotification) > MIN_AUDIBLE_PERIOD_MILLIS; + } + + private void clearNotifications() { + notificationState.reset(); + cancelActiveNotifications(); + } + + void sendSingleChatNotification(@NonNull Context context, + @NonNull NotificationState notificationState, + boolean signal, + boolean bundled) { - return; + AbstractNotificationBuilder notificationBuilder = createSingleChatNotification(context, notificationState, signal, bundled); + if (notificationBuilder != null) + notify(context, notificationBuilder.getNotificationId(), notificationBuilder, signal); } - if (isVisible) { - sendInChatNotification(chatId); - } else if (visibleChatId != NO_VISIBLE_CHAT_ID) { - //different chat - sendNotifications(chatId, messageId, false); - } else { - //app is in background or different Activity is on top - sendNotifications(chatId, messageId, true); - } - } - - /** - * On notification privacy preference changed, - * the notification state needs to be updated. - */ - @Override - public void onNotificationPrivacyChanged() { - if (!Prefs.isNotificationsEnabled(appContext)) { - return; - } - - clearNotifications(); - ApplicationDcContext dcContext = DcHelper.getContext(appContext); - int[] freshMessages = dcContext.getFreshMsgs(); - for (int message : freshMessages) { - DcMsg record = dcContext.getMsg(message); - updateNotification(record.getChatId(), record.getId()); - } - } - - @Override - public void removeNotifications(int[] chatIds) { - List removedItems = new LinkedList<>(); - for (int id : chatIds) { - removedItems.addAll(notificationState.removeNotificationsForChat(id)); - } - cancelNotifications(removedItems); - recreateSummaryNotification(); - } - - @Override - public void removeNotifications(int chatId) { - List removedItems = notificationState.removeNotificationsForChat(chatId); - cancelNotifications(removedItems); - recreateSummaryNotification(); - } - - void cancelNotifications(List removedItems) { - NotificationManager notifications = ServiceUtil.getNotificationManager(appContext); - for (NotificationItem item : removedItems) { - notifications.cancel(item.getId()); - } - } - - private void recreateSummaryNotification() { - NotificationManager notifications = ServiceUtil.getNotificationManager(appContext); - notifications.cancel(SUMMARY_NOTIFICATION_ID); - - if (notificationState.hasMultipleChats()) { - for (Integer id : notificationState.getChats()) { - sendSingleChatNotification(appContext, new NotificationState(notificationState.getNotificationsForChat(id)), false, true); - } - sendMultipleChatNotification(appContext, notificationState, false); - } else { - sendSingleChatNotification(appContext, notificationState, false, false); - } - } - - private void cancelActiveNotifications() { - NotificationManager notifications = ServiceUtil.getNotificationManager(appContext); - notifications.cancel(SUMMARY_NOTIFICATION_ID); - try { - StatusBarNotification[] activeNotifications = notifications.getActiveNotifications(); - - for (StatusBarNotification activeNotification : activeNotifications) { - notifications.cancel(activeNotification.getId()); - } - } catch (Throwable e) { - // XXX Appears to be a ROM bug, see #6043 - Log.w(TAG, e); - notifications.cancelAll(); - } - } - - void sendNotifications(int chatId, int messageId, boolean signal) { - ApplicationDcContext dcContext = DcHelper.getContext(appContext); - if (signal = isSignalAllowed(signal)) { - lastAudibleNotification = System.currentTimeMillis(); - } - - addMessageToNotificationState(dcContext, chatId, messageId); - if (notificationState.hasMultipleChats()) { - for (int id : notificationState.getChats()) { - sendSingleChatNotification(appContext, new NotificationState(notificationState.getNotificationsForChat(id)), false, true); - } - sendMultipleChatNotification(appContext, notificationState, signal); - } else { - sendSingleChatNotification(appContext, notificationState, signal, false); - } - } - - boolean isSignalAllowed(boolean signalRequested) { - long now = System.currentTimeMillis(); - return signalRequested && ( - now - INITIAL_STARTUP) > STARTUP_SILENCE_DELTA && - (now - lastAudibleNotification) > MIN_AUDIBLE_PERIOD_MILLIS; - } - - private void clearNotifications() { - notificationState.reset(); - cancelActiveNotifications(); - } - - void sendSingleChatNotification(@NonNull Context context, - @NonNull NotificationState notificationState, - boolean signal, - boolean bundled) - { - if (notificationState.getNotifications().isEmpty()) { - if (!bundled) cancelActiveNotifications(); - return; - } - - SingleRecipientNotificationBuilder builder = new SingleRecipientNotificationBuilder(context, Prefs.getNotificationPrivacy(context), signal); - List notifications = notificationState.getNotifications(); - NotificationItem firstItem = notifications.get(0); - Recipient recipient = firstItem.getRecipient(); - int chatId = firstItem.getChatId(); - int notificationId = (SUMMARY_NOTIFICATION_ID + (bundled ? chatId : 0)); - - builder.setChat(firstItem.getRecipient()); - builder.setMessageCount(notificationState.getMessageCount()); - builder.setPrimaryMessageBody(recipient, firstItem.getIndividualRecipient(), - firstItem.getText(""), firstItem.getSlideDeck()); - builder.setContentIntent(firstItem.getPendingIntent(context)); - builder.setGroup(NOTIFICATION_GROUP); - builder.setDeleteIntent(notificationState.getMarkAsReadIntent(context, chatId, notificationId)); - - long timestamp = firstItem.getTimestamp(); - if (timestamp != 0) builder.setWhen(timestamp); - - builder.addActions(notificationState.getMarkAsReadIntent(context, chatId, notificationId), - notificationState.getQuickReplyIntent(context, recipient), - notificationState.getRemoteReplyIntent(context, recipient)); - - ListIterator iterator = notifications.listIterator(notifications.size()); - - while(iterator.hasPrevious()) { - NotificationItem item = iterator.previous(); - builder.addMessageBody(item.getRecipient(), item.getIndividualRecipient(), item.getText()); - } - - if (signal) { - builder.setAlarms(notificationState.getRingtone(context), notificationState.getVibrate(context)); - builder.setTicker(firstItem.getIndividualRecipient(), - firstItem.getText()); - } - - if (!bundled) { - builder.setGroupSummary(true); - } - - NotificationManagerCompat.from(context).notify(notificationId, builder.build()); - } - - void sendMultipleChatNotification(@NonNull Context context, - @NonNull NotificationState notificationState, - boolean signal) - { - MultipleRecipientNotificationBuilder builder = new MultipleRecipientNotificationBuilder(context, Prefs.getNotificationPrivacy(context), signal); - List notifications = notificationState.getNotifications(); - NotificationItem firstItem = notifications.get(0); - - builder.setMessageCount(notificationState.getMessageCount(), notificationState.getChatCount()); - builder.setMostRecentSender(firstItem.getIndividualRecipient()); - builder.setGroup(NOTIFICATION_GROUP); - builder.setDeleteIntent(notificationState.getMarkAsReadIntent(context, 0, SUMMARY_NOTIFICATION_ID)); - - long timestamp = firstItem.getTimestamp(); - if (timestamp != 0) builder.setWhen(timestamp); - - builder.addActions(notificationState.getMarkAsReadIntent(context, 0, SUMMARY_NOTIFICATION_ID)); - - ListIterator iterator = notifications.listIterator(notifications.size()); - - while(iterator.hasPrevious()) { - NotificationItem item = iterator.previous(); - builder.addMessageBody(item.getRecipient(), item.getIndividualRecipient(), item.getText()); - } - - if (signal) { - builder.setAlarms(notificationState.getRingtone(context), notificationState.getVibrate(context)); - builder.setTicker(firstItem.getIndividualRecipient(), - firstItem.getText()); - } - - NotificationManagerCompat.from(context).notify(SUMMARY_NOTIFICATION_ID, builder.build()); - } - - private void sendInChatNotification(int chatId) { - if (!Prefs.isInChatNotifications(appContext) || - ServiceUtil.getAudioManager(appContext).getRingerMode() != AudioManager.RINGER_MODE_NORMAL) + void sendMultipleChatNotification(@NonNull Context context, + @NonNull NotificationState notificationState, + boolean signal) { - return; + AbstractNotificationBuilder notificationBuilder = createMultipleChatNotification(context, notificationState, signal); + if (notificationBuilder != null) + notify(context, SUMMARY_NOTIFICATION_ID, notificationBuilder, signal); } - if(Prefs.isChatMuted(appContext, chatId)) { - Log.d(TAG, "chat muted"); - return; + protected AbstractNotificationBuilder createSingleChatNotification(@NonNull Context context, + @NonNull NotificationState notificationState, + boolean signal, + boolean bundled) { + if (notificationState.getNotifications().isEmpty()) { + if (!bundled) cancelActiveNotifications(); + return null; + } + + SingleRecipientNotificationBuilder builder = new SingleRecipientNotificationBuilder(context, Prefs.getNotificationPrivacy(context), signal); + List notifications = notificationState.getNotifications(); + NotificationItem firstItem = notifications.get(0); + Recipient recipient = firstItem.getRecipient(); + int chatId = firstItem.getChatId(); + int notificationId = (SUMMARY_NOTIFICATION_ID + (bundled ? chatId : 0)); + + builder.setNotificationId(notificationId); + builder.setChat(firstItem.getRecipient()); + builder.setMessageCount(notificationState.getMessageCount()); + builder.setPrimaryMessageBody(recipient, firstItem.getIndividualRecipient(), + firstItem.getText(""), firstItem.getSlideDeck()); + builder.setContentIntent(firstItem.getPendingIntent(context)); + builder.setGroup(NOTIFICATION_GROUP); + builder.setDeleteIntent(notificationState.getMarkAsReadIntent(context, chatId, notificationId)); + + long timestamp = firstItem.getTimestamp(); + if (timestamp != 0) builder.setWhen(timestamp); + + builder.addActions(notificationState.getMarkAsReadIntent(context, chatId, notificationId), + notificationState.getQuickReplyIntent(context, recipient), + notificationState.getRemoteReplyIntent(context, recipient)); + + ListIterator iterator = notifications.listIterator(notifications.size()); + + while(iterator.hasPrevious()) { + NotificationItem item = iterator.previous(); + builder.addMessageBody(item.getRecipient(), item.getIndividualRecipient(), item.getText()); + } + + if (signal) { + builder.setAlarms(notificationState.getRingtone(context), notificationState.getVibrate(context)); + builder.setTicker(firstItem.getIndividualRecipient(), + firstItem.getText()); + } + + if (!bundled) { + builder.setGroupSummary(true); + } + + return builder; + + } - if (soundInLoaded) { - soundPool.play(soundIn, 1.0f, 1.0f, 1, 0, 1.0f); - } - } + void playNotificationSound(Uri ringtone, boolean vibrate) { + RingtoneManager.getRingtone(appContext, ringtone).play(); + if (vibrate) { - void addMessageToNotificationState(ApplicationDcContext dcContext, int chatId, int msgId) { - if (Prefs.isChatMuted(appContext, chatId)) { - return; - } - - DcMsg record = dcContext.getMsg(msgId); - if (record.isInfo()) { - return; - } - - int id = record.getId(); - CharSequence body = record.getDisplayBody(); - DcMsg dcMsg = dcContext.getMsg(msgId); - Recipient chatRecipient = new Recipient(appContext, dcContext.getChat(dcMsg.getChatId()), null); - Recipient individualRecipient = new Recipient(appContext, null, dcContext.getContact(dcMsg.getFromId())); - SlideDeck slideDeck = new SlideDeck(dcContext.context, record); - long timestamp = record.getTimestamp(); - - - if(slideDeck.getSlides().isEmpty()) - slideDeck = null; - - // TODO: if message content should be hidden on screen lock, do it here. - if (record.hasFile() && TextUtils.isEmpty(body)) { - String summaryText = record.getSummarytext(100); - if (summaryText.isEmpty()) { - body = SpanUtil.italic(appContext.getString(R.string.notify_media_message)); - } else { - body = SpanUtil.italic(summaryText); - } - } else if (record.hasFile() && !record.isMediaPending()) { - String message = appContext.getString(R.string.notify_media_message_with_text, body); - int italicLength = message.length() - body.length(); - body = SpanUtil.italic(message, italicLength); + Vibrator v = (Vibrator) appContext.getSystemService(Context.VIBRATOR_SERVICE); + v.vibrate(100); + v.vibrate(200); + } } - notificationState.addNotification(new NotificationItem(id, chatRecipient, individualRecipient, chatId, body, timestamp, slideDeck)); - } + + protected AbstractNotificationBuilder createMultipleChatNotification(@NonNull Context context, + @NonNull NotificationState notificationState, + boolean signal) { + MultipleRecipientNotificationBuilder builder = new MultipleRecipientNotificationBuilder(context, Prefs.getNotificationPrivacy(context), signal); + List notifications = notificationState.getNotifications(); + NotificationItem firstItem = notifications.get(0); + + builder.setMessageCount(notificationState.getMessageCount(), notificationState.getChatCount()); + builder.setMostRecentSender(firstItem.getIndividualRecipient()); + builder.setGroup(NOTIFICATION_GROUP); + builder.setDeleteIntent(notificationState.getMarkAsReadIntent(context, 0, SUMMARY_NOTIFICATION_ID)); + + long timestamp = firstItem.getTimestamp(); + if (timestamp != 0) builder.setWhen(timestamp); + + builder.addActions(notificationState.getMarkAsReadIntent(context, 0, SUMMARY_NOTIFICATION_ID)); + + ListIterator iterator = notifications.listIterator(notifications.size()); + + while(iterator.hasPrevious()) { + NotificationItem item = iterator.previous(); + builder.addMessageBody(item.getRecipient(), item.getIndividualRecipient(), item.getText()); + } + + if (signal) { + builder.setAlarms(notificationState.getRingtone(context), notificationState.getVibrate(context)); + builder.setTicker(firstItem.getIndividualRecipient(), + firstItem.getText()); + } + + return builder; + } + + private void notify(Context context, int notificationId, AbstractNotificationBuilder notificationBuilder, boolean signal) { + if (signal) { + playNotificationSound(notificationBuilder.getRingtone(), notificationBuilder.getVibrate()); + } + NotificationManagerCompat.from(context).notify(notificationId, notificationBuilder.build()); + } + + private void sendInChatNotification(int chatId) { + if (!Prefs.isInChatNotifications(appContext) || + ServiceUtil.getAudioManager(appContext).getRingerMode() != AudioManager.RINGER_MODE_NORMAL) + { + return; + } + + if(Prefs.isChatMuted(appContext, chatId)) { + Log.d(TAG, "chat muted"); + return; + } + + if (soundInLoaded) { + soundPool.play(soundIn, 1.0f, 1.0f, 1, 0, 1.0f); + } + } + + void addMessageToNotificationState(ApplicationDcContext dcContext, int chatId, int msgId) { + if (Prefs.isChatMuted(appContext, chatId)) { + return; + } + + DcMsg record = dcContext.getMsg(msgId); + if (record.isInfo()) { + return; + } + + int id = record.getId(); + CharSequence body = record.getDisplayBody(); + DcMsg dcMsg = dcContext.getMsg(msgId); + Recipient chatRecipient = new Recipient(appContext, dcContext.getChat(dcMsg.getChatId()), null); + Recipient individualRecipient = new Recipient(appContext, null, dcContext.getContact(dcMsg.getFromId())); + SlideDeck slideDeck = new SlideDeck(dcContext.context, record); + long timestamp = record.getTimestamp(); + + + if(slideDeck.getSlides().isEmpty()) + slideDeck = null; + + // TODO: if message content should be hidden on screen lock, do it here. + if (record.hasFile() && TextUtils.isEmpty(body)) { + String summaryText = record.getSummarytext(100); + if (summaryText.isEmpty()) { + body = SpanUtil.italic(appContext.getString(R.string.notify_media_message)); + } else { + body = SpanUtil.italic(summaryText); + } + } else if (record.hasFile() && !record.isMediaPending()) { + String message = appContext.getString(R.string.notify_media_message_with_text, body); + int italicLength = message.length() - body.length(); + body = SpanUtil.italic(message, italicLength); + } + + notificationState.addNotification(new NotificationItem(id, chatRecipient, individualRecipient, chatId, body, timestamp, slideDeck)); + } } + diff --git a/src/org/thoughtcrime/securesms/notifications/MessageNotifierApi23.java b/src/org/thoughtcrime/securesms/notifications/MessageNotifierApi23.java new file mode 100644 index 000000000..2461392f9 --- /dev/null +++ b/src/org/thoughtcrime/securesms/notifications/MessageNotifierApi23.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2011 Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.thoughtcrime.securesms.notifications; + +import android.annotation.TargetApi; +import android.app.NotificationManager; +import android.content.Context; +import android.service.notification.StatusBarNotification; +import android.util.Log; + +import org.thoughtcrime.securesms.util.ServiceUtil; + + +/** + * Handles posting system notifications for new messages. + * + */ + +@TargetApi(23) +class MessageNotifierApi23 extends MessageNotifier { + + MessageNotifierApi23(Context context) { + super(context); + } + + void cancelActiveNotifications() { + super.cancelActiveNotifications(); + NotificationManager notifications = ServiceUtil.getNotificationManager(appContext); + try { + + StatusBarNotification[] activeNotifications = notifications.getActiveNotifications(); + for (StatusBarNotification activeNotification : activeNotifications) { + notifications.cancel(activeNotification.getId()); + } + } catch (Throwable e) { + // XXX Appears to be a ROM bug, see #6043 + Log.w(TAG, e); + notifications.cancelAll(); + } + } + + } diff --git a/src/org/thoughtcrime/securesms/notifications/MessageNotifierCompat.java b/src/org/thoughtcrime/securesms/notifications/MessageNotifierCompat.java index f2dd4abc9..610319668 100644 --- a/src/org/thoughtcrime/securesms/notifications/MessageNotifierCompat.java +++ b/src/org/thoughtcrime/securesms/notifications/MessageNotifierCompat.java @@ -20,7 +20,7 @@ public class MessageNotifierCompat { if (Build.VERSION.SDK_INT < 23) { instance = new MessageNotifierPreApi23(context); } else { - instance = new MessageNotifier(context); + instance = new MessageNotifierApi23(context); } } diff --git a/src/org/thoughtcrime/securesms/notifications/MessageNotifierPreApi23.java b/src/org/thoughtcrime/securesms/notifications/MessageNotifierPreApi23.java index 8e6b37760..eb63ce5a9 100644 --- a/src/org/thoughtcrime/securesms/notifications/MessageNotifierPreApi23.java +++ b/src/org/thoughtcrime/securesms/notifications/MessageNotifierPreApi23.java @@ -10,7 +10,7 @@ import java.util.List; import static org.thoughtcrime.securesms.notifications.MessageNotifierCompat.SUMMARY_NOTIFICATION_ID; -public class MessageNotifierPreApi23 extends MessageNotifier { +class MessageNotifierPreApi23 extends MessageNotifier { MessageNotifierPreApi23(Context context) { super(context); @@ -39,7 +39,7 @@ public class MessageNotifierPreApi23 extends MessageNotifier { void sendNotifications(int chatId, int messageId, boolean signal) { ApplicationDcContext dcContext = DcHelper.getContext(appContext); if (signal = isSignalAllowed(signal)) { - lastAudibleNotification = System.currentTimeMillis();; + lastAudibleNotification = System.currentTimeMillis(); } addMessageToNotificationState(dcContext, chatId, messageId);