Signal-Android/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java

819 wiersze
34 KiB
Java

/*
* 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.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import android.os.TransactionTooLargeException;
import android.service.notification.StatusBarNotification;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import com.annimon.stream.Stream;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.contactshare.ContactUtil;
import org.thoughtcrime.securesms.conversation.ConversationIntents;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MentionUtil;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.ThreadBodyUtil;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.database.model.ReactionRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.messages.IncomingMessageObserver;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.BubbleUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.MessageRecordUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.SpanUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder;
import org.whispersystems.signalservice.internal.util.Util;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import me.leolin.shortcutbadger.ShortcutBadger;
/**
* Handles posting system notifications for new messages.
*
*
* @author Moxie Marlinspike
*/
public class DefaultMessageNotifier implements MessageNotifier {
private static final String TAG = Log.tag(DefaultMessageNotifier.class);
public static final String EXTRA_REMOTE_REPLY = "extra_remote_reply";
public static final String NOTIFICATION_GROUP = "messages";
private static final String EMOJI_REPLACEMENT_STRING = "__EMOJI__";
public static final long MIN_AUDIBLE_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(2);
public static final long DESKTOP_ACTIVITY_PERIOD = TimeUnit.MINUTES.toMillis(1);
private volatile long visibleThread = -1;
private volatile long lastDesktopActivityTimestamp = -1;
private volatile long lastAudibleNotification = -1;
private final CancelableExecutor executor = new CancelableExecutor();
@Override
public void setVisibleThread(long threadId) {
visibleThread = threadId;
}
@Override
public long getVisibleThread() {
return visibleThread;
}
@Override
public void clearVisibleThread() {
setVisibleThread(-1);
}
@Override
public void setLastDesktopActivityTimestamp(long timestamp) {
lastDesktopActivityTimestamp = timestamp;
}
@Override
public void notifyMessageDeliveryFailed(Context context, Recipient recipient, long threadId) {
if (visibleThread == threadId) {
sendInThreadNotification(context, recipient);
} else {
Intent intent = ConversationIntents.createBuilder(context, recipient.getId(), threadId)
.withDataUri(Uri.parse("custom://" + System.currentTimeMillis()))
.build();
FailedNotificationBuilder builder = new FailedNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context), intent);
((NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE))
.notify((int)threadId, builder.build());
}
}
@Override
public void cancelDelayedNotifications() {
executor.cancel();
}
private static boolean isDisplayingSummaryNotification(@NonNull Context context) {
if (Build.VERSION.SDK_INT >= 23) {
try {
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
StatusBarNotification[] activeNotifications = notificationManager.getActiveNotifications();
for (StatusBarNotification activeNotification : activeNotifications) {
if (activeNotification.getId() == NotificationIds.MESSAGE_SUMMARY) {
return true;
}
}
return false;
} catch (Throwable e) {
// XXX Android ROM Bug, see #6043
Log.w(TAG, e);
return false;
}
} else {
return false;
}
}
private static void cancelOrphanedNotifications(@NonNull Context context, NotificationState notificationState) {
if (Build.VERSION.SDK_INT >= 23) {
try {
NotificationManager notifications = ServiceUtil.getNotificationManager(context);
StatusBarNotification[] activeNotifications = notifications.getActiveNotifications();
for (StatusBarNotification notification : activeNotifications) {
boolean validNotification = false;
if (notification.getId() != NotificationIds.MESSAGE_SUMMARY &&
notification.getId() != KeyCachingService.SERVICE_RUNNING_ID &&
notification.getId() != IncomingMessageObserver.FOREGROUND_ID &&
notification.getId() != NotificationIds.PENDING_MESSAGES &&
!CallNotificationBuilder.isWebRtcNotification(notification.getId()))
{
for (NotificationItem item : notificationState.getNotifications()) {
if (notification.getId() == NotificationIds.getNotificationIdForThread(item.getThreadId())) {
validNotification = true;
break;
}
}
if (!validNotification) {
NotificationCancellationHelper.cancel(context, notification.getId());
}
}
}
} catch (Throwable e) {
// XXX Android ROM Bug, see #6043
Log.w(TAG, e);
}
}
}
@Override
public void updateNotification(@NonNull Context context) {
if (!TextSecurePreferences.isNotificationsEnabled(context)) {
return;
}
updateNotification(context, -1, false, 0, BubbleUtil.BubbleState.HIDDEN);
}
@Override
public void updateNotification(@NonNull Context context, long threadId)
{
if (System.currentTimeMillis() - lastDesktopActivityTimestamp < DESKTOP_ACTIVITY_PERIOD) {
Log.i(TAG, "Scheduling delayed notification...");
executor.execute(new DelayedNotification(context, threadId));
} else {
updateNotification(context, threadId, true);
}
}
@Override
public void updateNotification(@NonNull Context context, long threadId, @NonNull BubbleUtil.BubbleState defaultBubbleState) {
updateNotification(context, threadId, false, 0, defaultBubbleState);
}
@Override
public void updateNotification(@NonNull Context context,
long threadId,
boolean signal)
{
boolean isVisible = visibleThread == threadId;
Recipient recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId);
if (shouldNotify(context, recipient, threadId)) {
if (isVisible) {
sendInThreadNotification(context, recipient);
} else {
updateNotification(context, threadId, signal, 0, BubbleUtil.BubbleState.HIDDEN);
}
}
}
private boolean shouldNotify(@NonNull Context context, @Nullable Recipient recipient, long threadId) {
if (!TextSecurePreferences.isNotificationsEnabled(context)) {
return false;
}
if (recipient == null || !recipient.isMuted()) {
return true;
}
return recipient.isPushV2Group() &&
recipient.getMentionSetting() == RecipientDatabase.MentionSetting.ALWAYS_NOTIFY &&
DatabaseFactory.getMmsDatabase(context).getUnreadMentionCount(threadId) > 0;
}
@Override
public void updateNotification(@NonNull Context context,
long targetThread,
boolean signal,
int reminderCount,
@NonNull BubbleUtil.BubbleState defaultBubbleState)
{
if (!TextSecurePreferences.isNotificationsEnabled(context)) {
return;
}
boolean isReminder = reminderCount > 0;
Cursor telcoCursor = null;
try {
telcoCursor = DatabaseFactory.getMmsSmsDatabase(context).getUnread();
if (telcoCursor == null || telcoCursor.isAfterLast()) {
NotificationCancellationHelper.cancelAllMessageNotifications(context);
updateBadge(context, 0);
clearReminder(context);
return;
}
NotificationState notificationState = constructNotificationState(context, telcoCursor);
if (signal && (System.currentTimeMillis() - lastAudibleNotification) < MIN_AUDIBLE_PERIOD_MILLIS) {
signal = false;
} else if (signal) {
lastAudibleNotification = System.currentTimeMillis();
}
boolean shouldScheduleReminder = signal;
if (notificationState.hasMultipleThreads()) {
if (Build.VERSION.SDK_INT >= 23) {
for (long threadId : notificationState.getThreads()) {
if (targetThread < 1 || targetThread == threadId) {
sendSingleThreadNotification(context,
new NotificationState(notificationState.getNotificationsForThread(threadId)),
signal && (threadId == targetThread),
true,
isReminder,
(threadId == targetThread) ? defaultBubbleState : BubbleUtil.BubbleState.HIDDEN);
}
}
}
sendMultipleThreadNotification(context, notificationState, signal && (Build.VERSION.SDK_INT < 23));
} else {
long thread = notificationState.getNotifications().isEmpty() ? -1 : notificationState.getNotifications().get(0).getThreadId();
BubbleUtil.BubbleState bubbleState = thread == targetThread ? defaultBubbleState : BubbleUtil.BubbleState.HIDDEN;
shouldScheduleReminder = sendSingleThreadNotification(context, notificationState, signal, false, isReminder, bubbleState);
if (isDisplayingSummaryNotification(context)) {
sendMultipleThreadNotification(context, notificationState, false);
}
}
cancelOrphanedNotifications(context, notificationState);
updateBadge(context, notificationState.getMessageCount());
List<Long> smsIds = new LinkedList<>();
List<Long> mmsIds = new LinkedList<>();
for (NotificationItem item : notificationState.getNotifications()) {
if (item.isMms()) {
mmsIds.add(item.getId());
} else {
smsIds.add(item.getId());
}
}
DatabaseFactory.getMmsSmsDatabase(context).setNotifiedTimestamp(System.currentTimeMillis(), smsIds, mmsIds);
if (shouldScheduleReminder) {
scheduleReminder(context, reminderCount);
}
} finally {
if (telcoCursor != null) telcoCursor.close();
}
}
private static boolean sendSingleThreadNotification(@NonNull Context context,
@NonNull NotificationState notificationState,
boolean signal,
boolean bundled,
boolean isReminder,
@NonNull BubbleUtil.BubbleState defaultBubbleState)
{
Log.i(TAG, "sendSingleThreadNotification() signal: " + signal + " bundled: " + bundled);
if (notificationState.getNotifications().isEmpty()) {
if (!bundled) NotificationCancellationHelper.cancelAllMessageNotifications(context);
Log.i(TAG, "[sendSingleThreadNotification] Empty notification state. Skipping.");
return false;
}
NotificationPrivacyPreference notificationPrivacy = TextSecurePreferences.getNotificationPrivacy(context);
SingleRecipientNotificationBuilder builder = new SingleRecipientNotificationBuilder(context, notificationPrivacy);
List<NotificationItem> notifications = notificationState.getNotifications();
Recipient recipient = notifications.get(0).getRecipient();
boolean shouldAlert = signal && (isReminder || Stream.of(notifications).anyMatch(item -> item.getNotifiedTimestamp() == 0));
int notificationId;
if (Build.VERSION.SDK_INT >= 23) {
notificationId = NotificationIds.getNotificationIdForThread(notifications.get(0).getThreadId());
} else {
notificationId = NotificationIds.MESSAGE_SUMMARY;
}
builder.setThread(notifications.get(0).getRecipient());
builder.setMessageCount(notificationState.getMessageCount());
builder.setPrimaryMessageBody(recipient, notifications.get(0).getIndividualRecipient(),
notifications.get(0).getText(), notifications.get(0).getSlideDeck());
builder.setContentIntent(notifications.get(0).getPendingIntent(context));
builder.setDeleteIntent(notificationState.getDeleteIntent(context));
builder.setOnlyAlertOnce(!shouldAlert);
builder.setSortKey(String.valueOf(Long.MAX_VALUE - notifications.get(0).getTimestamp()));
builder.setDefaultBubbleState(defaultBubbleState);
long timestamp = notifications.get(0).getTimestamp();
if (timestamp != 0) builder.setWhen(timestamp);
boolean isSingleNotificationContactJoined = notifications.size() == 1 && notifications.get(0).isJoin();
if (notificationPrivacy.isDisplayMessage() &&
!KeyCachingService.isLocked(context) &&
RecipientUtil.isMessageRequestAccepted(context, recipient.resolve()))
{
ReplyMethod replyMethod = ReplyMethod.forRecipient(context, recipient);
builder.addActions(notificationState.getMarkAsReadIntent(context, notificationId),
notificationState.getQuickReplyIntent(context, notifications.get(0).getRecipient()),
notificationState.getRemoteReplyIntent(context, notifications.get(0).getRecipient(), replyMethod),
replyMethod,
!isSingleNotificationContactJoined && notificationState.canReply());
builder.addAndroidAutoAction(notificationState.getAndroidAutoReplyIntent(context, notifications.get(0).getRecipient()),
notificationState.getAndroidAutoHeardIntent(context, notificationId), notifications.get(0).getTimestamp());
}
if (!KeyCachingService.isLocked(context) && isSingleNotificationContactJoined) {
builder.addTurnOffTheseNotificationsAction(notificationState.getTurnOffTheseNotificationsIntent(context));
}
ListIterator<NotificationItem> iterator = notifications.listIterator(notifications.size());
while(iterator.hasPrevious()) {
NotificationItem item = iterator.previous();
builder.addMessageBody(item.getRecipient(), item.getIndividualRecipient(), item.getText(), item.getTimestamp(), item.getSlideDeck());
}
if (signal) {
builder.setAlarms(notificationState.getRingtone(context), notificationState.getVibrate());
builder.setTicker(notifications.get(0).getIndividualRecipient(),
notifications.get(0).getText());
}
if (Build.VERSION.SDK_INT >= 23) {
builder.setGroup(NOTIFICATION_GROUP);
builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN);
}
Notification notification = builder.build();
try {
NotificationManagerCompat.from(context).notify(notificationId, notification);
Log.i(TAG, "Posted notification.");
} catch (SecurityException e) {
Uri defaultValue = TextSecurePreferences.getNotificationRingtone(context);
if (!defaultValue.equals(notificationState.getRingtone(context))) {
Log.e(TAG, "Security exception when posting notification with custom ringtone", e);
clearNotificationRingtone(context, notifications.get(0).getRecipient());
} else {
throw e;
}
}
return shouldAlert;
}
private static void sendMultipleThreadNotification(@NonNull Context context,
@NonNull NotificationState notificationState,
boolean signal)
{
Log.i(TAG, "sendMultiThreadNotification() signal: " + signal);
if (notificationState.getNotifications().isEmpty()) {
Log.i(TAG, "[sendMultiThreadNotification] Empty notification state. Skipping.");
return;
}
NotificationPrivacyPreference notificationPrivacy = TextSecurePreferences.getNotificationPrivacy(context);
MultipleRecipientNotificationBuilder builder = new MultipleRecipientNotificationBuilder(context, notificationPrivacy);
List<NotificationItem> notifications = notificationState.getNotifications();
boolean shouldAlert = signal && Stream.of(notifications).anyMatch(item -> item.getNotifiedTimestamp() == 0);
builder.setMessageCount(notificationState.getMessageCount(), notificationState.getThreadCount());
builder.setMostRecentSender(notifications.get(0).getIndividualRecipient());
builder.setDeleteIntent(notificationState.getDeleteIntent(context));
builder.setOnlyAlertOnce(!shouldAlert);
if (Build.VERSION.SDK_INT >= 23) {
builder.setGroup(NOTIFICATION_GROUP);
builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN);
}
long timestamp = notifications.get(0).getTimestamp();
if (timestamp != 0) builder.setWhen(timestamp);
if (notificationPrivacy.isDisplayMessage()) {
builder.addActions(notificationState.getMarkAsReadIntent(context, NotificationIds.MESSAGE_SUMMARY));
}
ListIterator<NotificationItem> iterator = notifications.listIterator(notifications.size());
while(iterator.hasPrevious()) {
NotificationItem item = iterator.previous();
builder.addMessageBody(item.getIndividualRecipient(), item.getText());
}
if (signal) {
builder.setAlarms(notificationState.getRingtone(context), notificationState.getVibrate());
builder.setTicker(notifications.get(0).getIndividualRecipient(),
notifications.get(0).getText());
}
Notification notification = builder.build();
try {
NotificationManagerCompat.from(context).notify(NotificationIds.MESSAGE_SUMMARY, builder.build());
Log.i(TAG, "Posted notification. " + notification.toString());
} catch (SecurityException securityException) {
Uri defaultValue = TextSecurePreferences.getNotificationRingtone(context);
if (!defaultValue.equals(notificationState.getRingtone(context))) {
Log.e(TAG, "Security exception when posting notification with custom ringtone", securityException);
clearNotificationRingtone(context, notifications.get(0).getRecipient());
} else {
throw securityException;
}
} catch (RuntimeException runtimeException) {
Throwable cause = runtimeException.getCause();
if (cause instanceof TransactionTooLargeException) {
Log.e(TAG, "Transaction too large", runtimeException);
} else {
throw runtimeException;
}
}
}
private static void sendInThreadNotification(Context context, Recipient recipient) {
if (!TextSecurePreferences.isInThreadNotifications(context) ||
ServiceUtil.getAudioManager(context).getRingerMode() != AudioManager.RINGER_MODE_NORMAL)
{
return;
}
Uri uri = null;
if (recipient != null) {
uri = NotificationChannels.supported() ? NotificationChannels.getMessageRingtone(context, recipient) : recipient.getMessageRingtone();
}
if (uri == null) {
uri = NotificationChannels.supported() ? NotificationChannels.getMessageRingtone(context) : TextSecurePreferences.getNotificationRingtone(context);
}
if (uri.toString().isEmpty()) {
Log.d(TAG, "ringtone uri is empty");
return;
}
Ringtone ringtone = RingtoneManager.getRingtone(context, uri);
if (ringtone == null) {
Log.w(TAG, "ringtone is null");
return;
}
if (Build.VERSION.SDK_INT >= 21) {
ringtone.setAudioAttributes(new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN)
.setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT)
.build());
} else {
ringtone.setStreamType(AudioManager.STREAM_NOTIFICATION);
}
ringtone.play();
}
private static NotificationState constructNotificationState(@NonNull Context context,
@NonNull Cursor cursor)
{
NotificationState notificationState = new NotificationState();
MmsSmsDatabase.Reader reader = DatabaseFactory.getMmsSmsDatabase(context).readerFor(cursor);
MessageRecord record;
while ((record = reader.getNext()) != null) {
long id = record.getId();
boolean mms = record.isMms() || record.isMmsNotification();
Recipient recipient = record.getIndividualRecipient().resolve();
Recipient conversationRecipient = record.getRecipient().resolve();
long threadId = record.getThreadId();
CharSequence body = MentionUtil.updateBodyWithDisplayNames(context, record);
Recipient threadRecipients = null;
SlideDeck slideDeck = null;
long timestamp = record.getTimestamp();
long receivedTimestamp = record.getDateReceived();
boolean isUnreadMessage = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.READ)) == 0;
boolean hasUnreadReactions = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.REACTIONS_UNREAD)) == 1;
long lastReactionRead = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.REACTIONS_LAST_SEEN));
long notifiedTimestamp = record.getNotifiedTimestamp();
if (threadId != -1) {
threadRecipients = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId);
}
if (isUnreadMessage) {
boolean canReply = false;
if (KeyCachingService.isLocked(context)) {
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_locked_message));
} else if (record.isMms() && !((MmsMessageRecord) record).getSharedContacts().isEmpty()) {
Contact contact = ((MmsMessageRecord) record).getSharedContacts().get(0);
body = ContactUtil.getStringSummary(context, contact);
} else if (record.isMms() && ((MmsMessageRecord) record).isViewOnce()) {
body = SpanUtil.italic(context.getString(getViewOnceDescription((MmsMessageRecord) record)));
} else if (record.isRemoteDelete()) {
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_this_message_was_deleted));;
} else if (record.isMms() && !record.isMmsNotification() && !((MmsMessageRecord) record).getSlideDeck().getSlides().isEmpty()) {
body = ThreadBodyUtil.getFormattedBodyFor(context, record);
slideDeck = ((MmsMessageRecord) record).getSlideDeck();
canReply = true;
} else if (record.isGroupCall()) {
body = new SpannableString(MessageRecord.getGroupCallUpdateDescription(context, record.getBody(), false).getString());
canReply = false;
} else {
canReply = true;
}
boolean includeMessage = true;
if (threadRecipients != null && threadRecipients.isMuted()) {
boolean mentionsOverrideMute = threadRecipients.getMentionSetting() == RecipientDatabase.MentionSetting.ALWAYS_NOTIFY;
includeMessage = mentionsOverrideMute && record.hasSelfMention();
}
if (threadRecipients == null || includeMessage) {
notificationState.addNotification(new NotificationItem(id, mms, recipient, conversationRecipient, threadRecipients, threadId, body, timestamp, receivedTimestamp, slideDeck, false, record.isJoined(), canReply, notifiedTimestamp));
}
}
if (hasUnreadReactions) {
CharSequence originalBody = body;
for (ReactionRecord reaction : record.getReactions()) {
Recipient reactionSender = Recipient.resolved(reaction.getAuthor());
if (reactionSender.equals(Recipient.self()) || !record.isOutgoing() || reaction.getDateReceived() <= lastReactionRead) {
continue;
}
if (KeyCachingService.isLocked(context)) {
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_locked_message));
} else {
String text = SpanUtil.italic(getReactionMessageBody(context, record, originalBody)).toString();
String[] parts = text.split(EMOJI_REPLACEMENT_STRING);
SpannableStringBuilder builder = new SpannableStringBuilder();
for (int i = 0; i < parts.length; i++) {
builder.append(SpanUtil.italic(parts[i]));
if (i != parts.length -1) {
builder.append(reaction.getEmoji());
}
}
if (text.endsWith(EMOJI_REPLACEMENT_STRING)) {
builder.append(reaction.getEmoji());
}
body = builder;
}
if (threadRecipients == null || !threadRecipients.isMuted()) {
notificationState.addNotification(new NotificationItem(id, mms, reactionSender, conversationRecipient, threadRecipients, threadId, body, reaction.getDateReceived(), receivedTimestamp, null, true, record.isJoined(), false, 0));
}
}
}
}
reader.close();
return notificationState;
}
private static CharSequence getReactionMessageBody(@NonNull Context context, @NonNull MessageRecord record, @NonNull CharSequence body) {
boolean bodyIsEmpty = TextUtils.isEmpty(body);
if (MessageRecordUtil.hasSharedContact(record)) {
Contact contact = ((MmsMessageRecord) record).getSharedContacts().get(0);
CharSequence summary = ContactUtil.getStringSummary(context, contact);
return context.getString(R.string.MessageNotifier_reacted_s_to_s, EMOJI_REPLACEMENT_STRING, summary);
} else if (MessageRecordUtil.hasSticker(record)) {
return context.getString(R.string.MessageNotifier_reacted_s_to_your_sticker, EMOJI_REPLACEMENT_STRING);
} else if (record.isMms() && record.isViewOnce()){
return context.getString(R.string.MessageNotifier_reacted_s_to_your_view_once_media, EMOJI_REPLACEMENT_STRING);
} else if (!bodyIsEmpty) {
return context.getString(R.string.MessageNotifier_reacted_s_to_s, EMOJI_REPLACEMENT_STRING, body);
} else if (MessageRecordUtil.isMediaMessage(record) && MediaUtil.isVideoType(getMessageContentType((MmsMessageRecord) record))) {
return context.getString(R.string.MessageNotifier_reacted_s_to_your_video, EMOJI_REPLACEMENT_STRING);
} else if (MessageRecordUtil.isMediaMessage(record) && MediaUtil.isImageType(getMessageContentType((MmsMessageRecord) record))) {
return context.getString(R.string.MessageNotifier_reacted_s_to_your_image, EMOJI_REPLACEMENT_STRING);
} else if (MessageRecordUtil.isMediaMessage(record) && MediaUtil.isAudioType(getMessageContentType((MmsMessageRecord) record))) {
return context.getString(R.string.MessageNotifier_reacted_s_to_your_audio, EMOJI_REPLACEMENT_STRING);
} else if (MessageRecordUtil.isMediaMessage(record)) {
return context.getString(R.string.MessageNotifier_reacted_s_to_your_file, EMOJI_REPLACEMENT_STRING);
} else {
return context.getString(R.string.MessageNotifier_reacted_s_to_s, EMOJI_REPLACEMENT_STRING, body);
}
}
private static @StringRes int getViewOnceDescription(@NonNull MmsMessageRecord messageRecord) {
final String contentType = getMessageContentType(messageRecord);
if (MediaUtil.isImageType(contentType)) {
return R.string.MessageNotifier_view_once_photo;
}
return R.string.MessageNotifier_view_once_video;
}
private static String getMessageContentType(@NonNull MmsMessageRecord messageRecord) {
Slide thumbnailSlide = messageRecord.getSlideDeck().getThumbnailSlide();
if (thumbnailSlide == null) {
String slideContentType = messageRecord.getSlideDeck().getFirstSlideContentType();
if (slideContentType != null) {
return slideContentType;
}
Log.w(TAG, "Could not distinguish view-once content type from message record, defaulting to JPEG");
return MediaUtil.IMAGE_JPEG;
}
return thumbnailSlide.getContentType();
}
private static void updateBadge(Context context, int count) {
try {
if (count == 0) ShortcutBadger.removeCount(context);
else ShortcutBadger.applyCount(context, count);
} catch (Throwable t) {
// NOTE :: I don't totally trust this thing, so I'm catching
// everything.
Log.w(TAG, t);
}
}
private static void scheduleReminder(Context context, int count) {
if (count >= TextSecurePreferences.getRepeatAlertsCount(context)) {
return;
}
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Intent alarmIntent = new Intent(context, ReminderReceiver.class);
alarmIntent.putExtra("reminder_count", count);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, PendingIntent.FLAG_CANCEL_CURRENT);
long timeout = TimeUnit.MINUTES.toMillis(2);
alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + timeout, pendingIntent);
}
private static void clearNotificationRingtone(@NonNull Context context, @NonNull Recipient recipient) {
SignalExecutors.BOUNDED.execute(() -> {
DatabaseFactory.getRecipientDatabase(context).setMessageRingtone(recipient.getId(), null);
NotificationChannels.updateMessageRingtone(context, recipient, null);
});
}
@Override
public void clearReminder(@NonNull Context context) {
Intent alarmIntent = new Intent(context, ReminderReceiver.class);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, PendingIntent.FLAG_CANCEL_CURRENT);
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
alarmManager.cancel(pendingIntent);
}
private static class DelayedNotification implements Runnable {
private static final long DELAY = TimeUnit.SECONDS.toMillis(5);
private final AtomicBoolean canceled = new AtomicBoolean(false);
private final Context context;
private final long threadId;
private final long delayUntil;
private DelayedNotification(Context context, long threadId) {
this.context = context;
this.threadId = threadId;
this.delayUntil = System.currentTimeMillis() + DELAY;
}
@Override
public void run() {
long delayMillis = delayUntil - System.currentTimeMillis();
Log.i(TAG, "Waiting to notify: " + delayMillis);
if (delayMillis > 0) {
Util.sleep(delayMillis);
}
if (!canceled.get()) {
Log.i(TAG, "Not canceled, notifying...");
ApplicationDependencies.getMessageNotifier().updateNotification(context, threadId, true);
ApplicationDependencies.getMessageNotifier().cancelDelayedNotifications();
} else {
Log.w(TAG, "Canceled, not notifying...");
}
}
public void cancel() {
canceled.set(true);
}
}
private static class CancelableExecutor {
private final Executor executor = Executors.newSingleThreadExecutor();
private final Set<DelayedNotification> tasks = new HashSet<>();
public void execute(final DelayedNotification runnable) {
synchronized (tasks) {
tasks.add(runnable);
}
Runnable wrapper = () -> {
runnable.run();
synchronized (tasks) {
tasks.remove(runnable);
}
};
executor.execute(wrapper);
}
public void cancel() {
synchronized (tasks) {
for (DelayedNotification task : tasks) {
task.cancel();
}
}
}
}
}