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.
This commit is contained in:
cyBerta
2019-05-29 12:07:37 +02:00
parent 3efb33441c
commit 451ea1dbad
5 changed files with 428 additions and 366 deletions
@@ -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;
}
}
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<NotificationItem> removedItems = new LinkedList<>();
for (int id : chatIds) {
removedItems.addAll(notificationState.removeNotificationsForChat(id));
}
cancelNotifications(removedItems);
recreateSummaryNotification();
}
@Override
public void removeNotifications(int chatId) {
List<NotificationItem> removedItems = notificationState.removeNotificationsForChat(chatId);
cancelNotifications(removedItems);
recreateSummaryNotification();
}
void cancelNotifications(List<NotificationItem> 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<NotificationItem> removedItems = new LinkedList<>();
for (int id : chatIds) {
removedItems.addAll(notificationState.removeNotificationsForChat(id));
}
cancelNotifications(removedItems);
recreateSummaryNotification();
}
@Override
public void removeNotifications(int chatId) {
List<NotificationItem> removedItems = notificationState.removeNotificationsForChat(chatId);
cancelNotifications(removedItems);
recreateSummaryNotification();
}
void cancelNotifications(List<NotificationItem> 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<NotificationItem> 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<NotificationItem> 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<NotificationItem> 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<NotificationItem> 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<NotificationItem> 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<NotificationItem> 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<NotificationItem> 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<NotificationItem> 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));
}
}
@@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}
}
@@ -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);
}
}
@@ -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);