Remove old notification system and notification rewrite feature flag.

This commit is contained in:
Cody Henthorne
2021-06-08 11:20:19 -04:00
committed by GitHub
parent b6c653ff77
commit c72dd86fed
21 changed files with 69 additions and 2364 deletions

View File

@@ -20,6 +20,7 @@ public class InMemoryMessageRecord extends MessageRecord {
private static final int NO_GROUPS_IN_COMMON_ID = -1;
private static final int UNIVERSAL_EXPIRE_TIMER_ID = -2;
private static final int FORCE_BUBBLE_ID = -3;
private InMemoryMessageRecord(long id,
String body,
@@ -137,4 +138,13 @@ public class InMemoryMessageRecord extends MessageRecord {
return true;
}
}
/**
* Useful for create an empty message record when one is needed.
*/
public static final class ForceConversationBubble extends InMemoryMessageRecord {
public ForceConversationBubble(Recipient conversationRecipient, long threadId) {
super(FORCE_BUBBLE_ID, "", conversationRecipient, threadId, 0);
}
}
}

View File

@@ -34,9 +34,7 @@ import org.thoughtcrime.securesms.messages.BackgroundMessageRetriever;
import org.thoughtcrime.securesms.messages.IncomingMessageObserver;
import org.thoughtcrime.securesms.messages.IncomingMessageProcessor;
import org.thoughtcrime.securesms.net.PipeConnectivityListener;
import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.notifications.v2.MessageNotifierV2;
import org.thoughtcrime.securesms.notifications.OptimizedMessageNotifier;
import org.thoughtcrime.securesms.payments.MobileCoinConfig;
import org.thoughtcrime.securesms.payments.Payments;

View File

@@ -1,103 +0,0 @@
package org.thoughtcrime.securesms.notifications;
import android.app.Notification;
import android.content.Context;
import android.graphics.Color;
import android.net.Uri;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
public abstract class AbstractNotificationBuilder extends NotificationCompat.Builder {
@SuppressWarnings("unused")
private static final String TAG = Log.tag(AbstractNotificationBuilder.class);
public static final int MAX_DISPLAY_LENGTH = 500;
protected Context context;
protected NotificationPrivacyPreference privacy;
public AbstractNotificationBuilder(Context context, NotificationPrivacyPreference privacy) {
super(context);
this.context = context;
this.privacy = privacy;
setChannelId(NotificationChannels.getMessagesChannel(context));
setLed();
}
protected CharSequence getStyledMessage(@NonNull Recipient recipient, @Nullable CharSequence message) {
SpannableStringBuilder builder = new SpannableStringBuilder();
builder.append(Util.getBoldedString(recipient.getDisplayName(context)));
builder.append(": ");
builder.append(message == null ? "" : message);
return builder;
}
public void setAlarms(@Nullable Uri ringtone, RecipientDatabase.VibrateState vibrate) {
Uri defaultRingtone = NotificationChannels.supported() ? NotificationChannels.getMessageRingtone(context) : SignalStore.settings().getMessageNotificationSound();
boolean defaultVibrate = NotificationChannels.supported() ? NotificationChannels.getMessageVibrate(context) : SignalStore.settings().isMessageVibrateEnabled();
if (ringtone == null && !TextUtils.isEmpty(defaultRingtone.toString())) setSound(defaultRingtone);
else if (ringtone != null && !ringtone.toString().isEmpty()) setSound(ringtone);
if (vibrate == RecipientDatabase.VibrateState.ENABLED ||
(vibrate == RecipientDatabase.VibrateState.DEFAULT && defaultVibrate))
{
setDefaults(Notification.DEFAULT_VIBRATE);
}
}
private void setLed() {
String ledColor = SignalStore.settings().getMessageLedColor();
String ledBlinkPattern = SignalStore.settings().getMessageLedBlinkPattern();
String ledBlinkPatternCustom = TextSecurePreferences.getNotificationLedPatternCustom(context);
if (!ledColor.equals("none")) {
String[] blinkPatternArray = parseBlinkPattern(ledBlinkPattern, ledBlinkPatternCustom);
setLights(Color.parseColor(ledColor),
Integer.parseInt(blinkPatternArray[0]),
Integer.parseInt(blinkPatternArray[1]));
}
}
public void setTicker(@NonNull Recipient recipient, @Nullable CharSequence message) {
if (privacy.isDisplayMessage()) {
setTicker(getStyledMessage(recipient, trimToDisplayLength(message)));
} else if (privacy.isDisplayContact()) {
setTicker(getStyledMessage(recipient, context.getString(R.string.AbstractNotificationBuilder_new_message)));
} else {
setTicker(context.getString(R.string.AbstractNotificationBuilder_new_message));
}
}
private String[] parseBlinkPattern(String blinkPattern, String blinkPatternCustom) {
if (blinkPattern.equals("custom"))
blinkPattern = blinkPatternCustom;
return blinkPattern.split(",");
}
protected @NonNull CharSequence trimToDisplayLength(@Nullable CharSequence text) {
text = text == null ? "" : text;
return text.length() <= MAX_DISPLAY_LENGTH ? text
: text.subSequence(0, MAX_DISPLAY_LENGTH);
}
}

View File

@@ -1,77 +0,0 @@
/*
* 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.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessageDatabase.MarkedMessageInfo;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import java.util.LinkedList;
import java.util.List;
/**
* Marks an Android Auto as read after the driver have listened to it
*/
public class AndroidAutoHeardReceiver extends BroadcastReceiver {
public static final String TAG = Log.tag(AndroidAutoHeardReceiver.class);
public static final String HEARD_ACTION = "org.thoughtcrime.securesms.notifications.ANDROID_AUTO_HEARD";
public static final String THREAD_IDS_EXTRA = "car_heard_thread_ids";
public static final String NOTIFICATION_ID_EXTRA = "car_notification_id";
@SuppressLint("StaticFieldLeak")
@Override
public void onReceive(final Context context, Intent intent)
{
if (!HEARD_ACTION.equals(intent.getAction()))
return;
final long[] threadIds = intent.getLongArrayExtra(THREAD_IDS_EXTRA);
if (threadIds != null) {
int notificationId = intent.getIntExtra(NOTIFICATION_ID_EXTRA, -1);
NotificationCancellationHelper.cancelLegacy(context, notificationId);
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
List<MarkedMessageInfo> messageIdsCollection = new LinkedList<>();
for (long threadId : threadIds) {
Log.i(TAG, "Marking meassage as read: " + threadId);
List<MarkedMessageInfo> messageIds = DatabaseFactory.getThreadDatabase(context).setRead(threadId, true);
messageIdsCollection.addAll(messageIds);
}
ApplicationDependencies.getMessageNotifier().updateNotification(context);
MarkReadReceiver.process(context, messageIdsCollection);
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
}

View File

@@ -1,119 +0,0 @@
/*
* 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.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import androidx.core.app.RemoteInput;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessageDatabase.MarkedMessageInfo;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/**
* Get the response text from the Android Auto and sends an message as a reply
*/
public class AndroidAutoReplyReceiver extends BroadcastReceiver {
public static final String TAG = Log.tag(AndroidAutoReplyReceiver.class);
public static final String REPLY_ACTION = "org.thoughtcrime.securesms.notifications.ANDROID_AUTO_REPLY";
public static final String RECIPIENT_EXTRA = "car_recipient";
public static final String VOICE_REPLY_KEY = "car_voice_reply_key";
public static final String THREAD_ID_EXTRA = "car_reply_thread_id";
@SuppressLint("StaticFieldLeak")
@Override
public void onReceive(final Context context, Intent intent)
{
if (!REPLY_ACTION.equals(intent.getAction())) return;
Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
if (remoteInput == null) return;
final long threadId = intent.getLongExtra(THREAD_ID_EXTRA, -1);
final CharSequence responseText = getMessageText(intent);
final Recipient recipient = Recipient.resolved(intent.getParcelableExtra(RECIPIENT_EXTRA));
if (responseText != null) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
long replyThreadId;
int subscriptionId = recipient.getDefaultSubscriptionId().or(-1);
long expiresIn = recipient.getExpireMessages() * 1000L;
if (recipient.resolve().isGroup()) {
Log.w(TAG, "GroupRecipient, Sending media message");
OutgoingMediaMessage reply = new OutgoingMediaMessage(recipient,
responseText.toString(),
new LinkedList<>(),
System.currentTimeMillis(),
subscriptionId,
expiresIn,
false,
0,
null,
Collections.emptyList(),
Collections.emptyList(),
Collections.emptyList(),
Collections.emptyList(),
Collections.emptyList());
replyThreadId = MessageSender.send(context, reply, threadId, false, null);
} else {
Log.w(TAG, "Sending regular message ");
OutgoingTextMessage reply = new OutgoingTextMessage(recipient, responseText.toString(), expiresIn, subscriptionId);
replyThreadId = MessageSender.send(context, reply, threadId, false, null);
}
List<MarkedMessageInfo> messageIds = DatabaseFactory.getThreadDatabase(context).setRead(replyThreadId, true);
ApplicationDependencies.getMessageNotifier().updateNotification(context);
MarkReadReceiver.process(context, messageIds);
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
private CharSequence getMessageText(Intent intent) {
Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
if (remoteInput != null) {
return remoteInput.getCharSequence(VOICE_REPLY_KEY);
}
return null;
}
}

View File

@@ -1,837 +0,0 @@
/*
* 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.graphics.BitmapFactory;
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.keyvalue.SignalStore;
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(@NonNull Context context, @NonNull 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, SignalStore.settings().getMessageNotificationsPrivacy(), intent);
((NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE))
.notify((int)threadId, builder.build());
}
}
@Override
public void notifyProofRequired(@NonNull Context context, @NonNull Recipient recipient, long threadId) {
if (visibleThread == threadId) {
sendInThreadNotification(context, recipient);
} else {
Log.w(TAG, "[Proof Required] Not notifying on old notifier.");
}
}
@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 (!SignalStore.settings().isMessageNotificationsEnabled()) {
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 (!SignalStore.settings().isMessageNotificationsEnabled()) {
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 (!SignalStore.settings().isMessageNotificationsEnabled()) {
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 = SignalStore.settings().getMessageNotificationsPrivacy();
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 = SignalStore.settings().getMessageNotificationSound();
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 = SignalStore.settings().getMessageNotificationsPrivacy();
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 = SignalStore.settings().getMessageNotificationSound();
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 (!SignalStore.settings().isMessageNotificationsInChatSoundsEnabled() ||
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) : SignalStore.settings().getMessageNotificationSound();
}
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 (!RecipientUtil.isMessageRequestAccepted(context, threadId)) {
body = SpanUtil.italic(context.getString(R.string.SingleRecipientNotificationBuilder_message_request));
} else 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 >= SignalStore.settings().getMessageNotificationsRepeatAlerts()) {
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);
}
@Override
public void addStickyThread(long threadId, long earliestTimestamp) {}
@Override
public void removeStickyThread(long threadId) {}
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();
}
}
}
}
}

View File

@@ -1,31 +0,0 @@
package org.thoughtcrime.securesms.notifications;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.BitmapFactory;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
public class FailedNotificationBuilder extends AbstractNotificationBuilder {
public FailedNotificationBuilder(Context context, NotificationPrivacyPreference privacy, Intent intent) {
super(context, privacy);
setSmallIcon(R.drawable.ic_notification);
setLargeIcon(BitmapFactory.decodeResource(context.getResources(),
R.drawable.ic_action_warning_red));
setContentTitle(context.getString(R.string.MessageNotifier_message_delivery_failed));
setContentText(context.getString(R.string.MessageNotifier_failed_to_deliver_message));
setTicker(context.getString(R.string.MessageNotifier_error_delivering_message));
setContentIntent(PendingIntent.getActivity(context, 0, intent, 0));
setAutoCancel(true);
setAlarms(null, RecipientDatabase.VibrateState.DEFAULT);
setChannelId(NotificationChannels.FAILURES);
}
}

View File

@@ -1,94 +0,0 @@
package org.thoughtcrime.securesms.notifications;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import java.util.LinkedList;
import java.util.List;
public class MultipleRecipientNotificationBuilder extends AbstractNotificationBuilder {
private final List<CharSequence> messageBodies = new LinkedList<>();
public MultipleRecipientNotificationBuilder(Context context, NotificationPrivacyPreference privacy) {
super(context, privacy);
setColor(context.getResources().getColor(R.color.core_ultramarine));
setSmallIcon(R.drawable.ic_notification);
setContentTitle(context.getString(R.string.app_name));
// TODO [greyson] Navigation
setContentIntent(PendingIntent.getActivity(context, 0, MainActivity.clearTop(context), 0));
setCategory(NotificationCompat.CATEGORY_MESSAGE);
setGroupSummary(true);
if (!NotificationChannels.supported()) {
setPriority(TextSecurePreferences.getNotificationPriority(context));
}
}
public void setMessageCount(int messageCount, int threadCount) {
setSubText(context.getString(R.string.MessageNotifier_d_new_messages_in_d_conversations,
messageCount, threadCount));
setContentInfo(String.valueOf(messageCount));
setNumber(messageCount);
}
public void setMostRecentSender(Recipient recipient) {
if (privacy.isDisplayContact()) {
setContentText(context.getString(R.string.MessageNotifier_most_recent_from_s,
recipient.getDisplayName(context)));
}
if (recipient.getNotificationChannel() != null) {
setChannelId(recipient.getNotificationChannel());
}
}
public void addActions(PendingIntent markAsReadIntent) {
NotificationCompat.Action markAllAsReadAction = new NotificationCompat.Action(R.drawable.check,
context.getString(R.string.MessageNotifier_mark_all_as_read),
markAsReadIntent);
addAction(markAllAsReadAction);
extend(new NotificationCompat.WearableExtender().addAction(markAllAsReadAction));
}
public void addMessageBody(@NonNull Recipient sender, @Nullable CharSequence body) {
if (privacy.isDisplayMessage()) {
messageBodies.add(getStyledMessage(sender, body));
} else if (privacy.isDisplayContact()) {
messageBodies.add(Util.getBoldedString(sender.getDisplayName(context)));
}
if (privacy.isDisplayContact() && sender.getContactUri() != null) {
addPerson(sender.getContactUri().toString());
}
}
@Override
public Notification build() {
if (privacy.isDisplayMessage() || privacy.isDisplayContact()) {
NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
for (CharSequence body : messageBodies) {
style.addLine(trimToDisplayLength(body));
}
setStyle(style);
}
return super.build();
}
}

View File

@@ -14,6 +14,7 @@ import com.annimon.stream.Stream;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.notifications.v2.MessageNotifierV2;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.BubbleUtil;
import org.thoughtcrime.securesms.util.ConversationUtil;
@@ -44,7 +45,7 @@ public final class NotificationCancellationHelper {
/**
* Cancels all Message-Based notifications. Specifically, this is any notification that is not the
* summary notification assigned to the {@link DefaultMessageNotifier#NOTIFICATION_GROUP} group.
* summary notification assigned to the {@link MessageNotifierV2#NOTIFICATION_GROUP} group.
*
* We utilize our wrapped cancellation methods and a counter to make sure that we do not lose
* bubble notifications that do not have unread messages in them.
@@ -110,7 +111,7 @@ public final class NotificationCancellationHelper {
@RequiresApi(23)
private static boolean isSingleThreadNotification(@NonNull StatusBarNotification statusBarNotification) {
return statusBarNotification.getId() != NotificationIds.MESSAGE_SUMMARY &&
Objects.equals(statusBarNotification.getNotification().getGroup(), DefaultMessageNotifier.NOTIFICATION_GROUP);
Objects.equals(statusBarNotification.getNotification().getGroup(), MessageNotifierV2.NOTIFICATION_GROUP);
}
/**

View File

@@ -1,131 +0,0 @@
package org.thoughtcrime.securesms.notifications;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.TaskStackBuilder;
import org.thoughtcrime.securesms.conversation.ConversationIntents;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.recipients.Recipient;
public class NotificationItem {
private final long id;
private final boolean mms;
@NonNull private final Recipient conversationRecipient;
@NonNull private final Recipient individualRecipient;
@Nullable private final Recipient threadRecipient;
private final long threadId;
@Nullable private final CharSequence text;
private final long timestamp;
private final long messageReceivedTimestamp;
@Nullable private final SlideDeck slideDeck;
private final boolean jumpToMessage;
private final boolean isJoin;
private final boolean canReply;
private final long notifiedTimestamp;
public NotificationItem(long id,
boolean mms,
@NonNull Recipient individualRecipient,
@NonNull Recipient conversationRecipient,
@Nullable Recipient threadRecipient,
long threadId,
@Nullable CharSequence text,
long timestamp,
long messageReceivedTimestamp,
@Nullable SlideDeck slideDeck,
boolean jumpToMessage,
boolean isJoin,
boolean canReply,
long notifiedTimestamp)
{
this.id = id;
this.mms = mms;
this.individualRecipient = individualRecipient;
this.conversationRecipient = conversationRecipient;
this.threadRecipient = threadRecipient;
this.text = text;
this.threadId = threadId;
this.timestamp = timestamp;
this.messageReceivedTimestamp = messageReceivedTimestamp;
this.slideDeck = slideDeck;
this.jumpToMessage = jumpToMessage;
this.isJoin = isJoin;
this.canReply = canReply;
this.notifiedTimestamp = notifiedTimestamp;
}
public @NonNull Recipient getRecipient() {
return threadRecipient == null ? conversationRecipient : threadRecipient;
}
public @NonNull Recipient getIndividualRecipient() {
return individualRecipient;
}
public @Nullable CharSequence getText() {
return text;
}
public long getTimestamp() {
return timestamp;
}
public long getThreadId() {
return threadId;
}
public @Nullable SlideDeck getSlideDeck() {
return slideDeck;
}
public PendingIntent getPendingIntent(Context context) {
Recipient recipient = threadRecipient != null ? threadRecipient : conversationRecipient;
int startingPosition = jumpToMessage ? getStartingPosition(context, threadId, messageReceivedTimestamp) : -1;
Intent intent = ConversationIntents.createBuilder(context, recipient.getId(), threadId)
.withStartingPosition(startingPosition)
.build();
makeIntentUniqueToPreventMerging(intent);
return TaskStackBuilder.create(context)
.addNextIntentWithParentStack(intent)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
}
public long getId() {
return id;
}
public boolean isMms() {
return mms;
}
public boolean isJoin() {
return isJoin;
}
private static int getStartingPosition(@NonNull Context context, long threadId, long receivedTimestampMs) {
return DatabaseFactory.getMmsSmsDatabase(context).getMessagePositionInConversation(threadId, receivedTimestampMs);
}
private static void makeIntentUniqueToPreventMerging(@NonNull Intent intent) {
intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
}
public boolean canReply() {
return canReply;
}
public long getNotifiedTimestamp() {
return notifiedTimestamp;
}
}

View File

@@ -1,213 +0,0 @@
package org.thoughtcrime.securesms.notifications;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.contacts.TurnOffContactJoinedNotificationsActivity;
import org.thoughtcrime.securesms.conversation.ConversationIntents;
import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState;
import org.thoughtcrime.securesms.recipients.Recipient;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
public class NotificationState {
private static final String TAG = Log.tag(NotificationState.class);
private final Comparator<NotificationItem> notificationItemComparator = (a, b) -> -Long.compare(a.getTimestamp(), b.getTimestamp());
private final List<NotificationItem> notifications = new LinkedList<>();
private final LinkedHashSet<Long> threads = new LinkedHashSet<>();
public NotificationState() {}
public NotificationState(@NonNull List<NotificationItem> items) {
for (NotificationItem item : items) {
addNotification(item);
}
}
public void addNotification(NotificationItem item) {
notifications.add(item);
Collections.sort(notifications, notificationItemComparator);
threads.remove(item.getThreadId());
threads.add(item.getThreadId());
}
public @Nullable Uri getRingtone(@NonNull Context context) {
if (!notifications.isEmpty()) {
Recipient recipient = notifications.get(0).getRecipient();
if (recipient != null) {
return NotificationChannels.supported() ? NotificationChannels.getMessageRingtone(context, recipient)
: recipient.resolve().getMessageRingtone();
}
}
return null;
}
public VibrateState getVibrate() {
if (!notifications.isEmpty()) {
Recipient recipient = notifications.get(0).getRecipient();
if (recipient != null) {
return recipient.resolve().getMessageVibrate();
}
}
return VibrateState.DEFAULT;
}
public boolean hasMultipleThreads() {
return threads.size() > 1;
}
public Collection<Long> getThreads() {
return threads;
}
public int getThreadCount() {
return threads.size();
}
public int getMessageCount() {
return notifications.size();
}
public List<NotificationItem> getNotifications() {
return notifications;
}
public List<NotificationItem> getNotificationsForThread(long threadId) {
List<NotificationItem> list = new LinkedList<>();
for (NotificationItem item : notifications) {
if (item.getThreadId() == threadId) list.add(item);
}
Collections.sort(list, notificationItemComparator);
return list;
}
public PendingIntent getTurnOffTheseNotificationsIntent(Context context) {
long threadId = threads.iterator().next();
return PendingIntent.getActivity(context,
0,
TurnOffContactJoinedNotificationsActivity.newIntent(context, threadId),
PendingIntent.FLAG_UPDATE_CURRENT);
}
public PendingIntent getMarkAsReadIntent(Context context, int notificationId) {
long[] threadArray = new long[threads.size()];
int index = 0;
StringBuilder threadString = new StringBuilder();
for (long thread : threads) {
threadString.append(thread).append(" ");
threadArray[index++] = thread;
}
Log.i(TAG, "Added threads: " + threadString.toString());
Intent intent = new Intent(MarkReadReceiver.CLEAR_ACTION);
intent.setClass(context, MarkReadReceiver.class);
intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
intent.putExtra(MarkReadReceiver.THREAD_IDS_EXTRA, threadArray);
intent.putExtra(MarkReadReceiver.NOTIFICATION_ID_EXTRA, notificationId);
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
public PendingIntent getRemoteReplyIntent(Context context, Recipient recipient, ReplyMethod replyMethod) {
if (threads.size() != 1) throw new AssertionError("We only support replies to single thread notifications!");
Intent intent = new Intent(RemoteReplyReceiver.REPLY_ACTION);
intent.setClass(context, RemoteReplyReceiver.class);
intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
intent.putExtra(RemoteReplyReceiver.RECIPIENT_EXTRA, recipient.getId());
intent.putExtra(RemoteReplyReceiver.REPLY_METHOD, replyMethod);
intent.setPackage(context.getPackageName());
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
public PendingIntent getAndroidAutoReplyIntent(Context context, Recipient recipient) {
if (threads.size() != 1) throw new AssertionError("We only support replies to single thread notifications!");
Intent intent = new Intent(AndroidAutoReplyReceiver.REPLY_ACTION);
intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
intent.setClass(context, AndroidAutoReplyReceiver.class);
intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
intent.putExtra(AndroidAutoReplyReceiver.RECIPIENT_EXTRA, recipient.getId());
intent.putExtra(AndroidAutoReplyReceiver.THREAD_ID_EXTRA, (long)threads.toArray()[0]);
intent.setPackage(context.getPackageName());
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
public PendingIntent getAndroidAutoHeardIntent(Context context, int notificationId) {
long[] threadArray = new long[threads.size()];
int index = 0;
for (long thread : threads) {
Log.i(TAG, "getAndroidAutoHeardIntent Added thread: " + thread);
threadArray[index++] = thread;
}
Intent intent = new Intent(AndroidAutoHeardReceiver.HEARD_ACTION);
intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
intent.setClass(context, AndroidAutoHeardReceiver.class);
intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
intent.putExtra(AndroidAutoHeardReceiver.THREAD_IDS_EXTRA, threadArray);
intent.putExtra(AndroidAutoHeardReceiver.NOTIFICATION_ID_EXTRA, notificationId);
intent.setPackage(context.getPackageName());
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
public PendingIntent getQuickReplyIntent(Context context, Recipient recipient) {
if (threads.size() != 1) throw new AssertionError("We only support replies to single thread notifications! " + threads.size());
Intent intent = ConversationIntents.createPopUpBuilder(context, recipient.getId(), (long) threads.toArray()[0])
.withDataUri(Uri.parse("custom://"+System.currentTimeMillis()))
.build();
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
public PendingIntent getDeleteIntent(Context context) {
int index = 0;
long[] ids = new long[notifications.size()];
boolean[] mms = new boolean[ids.length];
for (NotificationItem notificationItem : notifications) {
ids[index] = notificationItem.getId();
mms[index++] = notificationItem.isMms();
}
Intent intent = new Intent(context, DeleteNotificationReceiver.class);
intent.setAction(DeleteNotificationReceiver.DELETE_NOTIFICATION_ACTION);
intent.putExtra(DeleteNotificationReceiver.EXTRA_IDS, ids);
intent.putExtra(DeleteNotificationReceiver.EXTRA_MMS, mms);
intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
public boolean canReply() {
return notifications.size() >= 1 && notifications.get(0).canReply();
}
}

View File

@@ -11,7 +11,6 @@ import org.signal.core.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.notifications.v2.MessageNotifierV2;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.BubbleUtil;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.LeakyBucketLimiter;
import org.thoughtcrime.securesms.util.Util;
@@ -21,14 +20,11 @@ import org.thoughtcrime.securesms.util.Util;
public class OptimizedMessageNotifier implements MessageNotifier {
private final LeakyBucketLimiter limiter;
private final DefaultMessageNotifier messageNotifierV1;
private final MessageNotifierV2 messageNotifierV2;
private final MessageNotifierV2 messageNotifierV2;
@MainThread
public OptimizedMessageNotifier(@NonNull Application context) {
this.limiter = new LeakyBucketLimiter(5, 1000, new Handler(SignalExecutors.getAndStartHandlerThread("signal-notifier").getLooper()));
this.messageNotifierV1 = new DefaultMessageNotifier();
this.messageNotifierV2 = new MessageNotifierV2(context);
}
@@ -119,10 +115,6 @@ public class OptimizedMessageNotifier implements MessageNotifier {
}
private MessageNotifier getNotifier() {
if (FeatureFlags.useNewNotificationSystem()) {
return messageNotifierV2;
} else {
return messageNotifierV1;
}
return messageNotifierV2;
}
}

View File

@@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessageDatabase.MarkedMessageInfo;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.notifications.v2.MessageNotifierV2;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sms.MessageSender;
@@ -61,7 +62,7 @@ public class RemoteReplyReceiver extends BroadcastReceiver {
final RecipientId recipientId = intent.getParcelableExtra(RECIPIENT_EXTRA);
final ReplyMethod replyMethod = (ReplyMethod) intent.getSerializableExtra(REPLY_METHOD);
final CharSequence responseText = remoteInput.getCharSequence(DefaultMessageNotifier.EXTRA_REMOTE_REPLY);
final CharSequence responseText = remoteInput.getCharSequence(MessageNotifierV2.EXTRA_REMOTE_REPLY);
if (recipientId == null) throw new AssertionError("No recipientId specified");
if (replyMethod == null) throw new AssertionError("No reply method specified");

View File

@@ -1,455 +0,0 @@
package org.thoughtcrime.securesms.notifications;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.text.SpannableStringBuilder;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.view.ContextThemeWrapper;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationCompat.Action;
import androidx.core.app.Person;
import androidx.core.app.RemoteInput;
import androidx.core.graphics.drawable.IconCompat;
import com.annimon.stream.Stream;
import com.bumptech.glide.load.MultiTransformation;
import com.bumptech.glide.load.Transformation;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.bitmap.CircleCrop;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
import org.thoughtcrime.securesms.conversation.ConversationIntents;
import org.thoughtcrime.securesms.conversation.colors.AvatarColor;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
import org.thoughtcrime.securesms.mms.GlideApp;
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.util.AvatarUtil;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.BlurTransformation;
import org.thoughtcrime.securesms.util.BubbleUtil;
import org.thoughtcrime.securesms.util.ConversationUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutionException;
public class SingleRecipientNotificationBuilder extends AbstractNotificationBuilder {
private static final String TAG = Log.tag(SingleRecipientNotificationBuilder.class);
private static final int BIG_PICTURE_DIMEN = 500;
private static final int LARGE_ICON_DIMEN = 250;
private final List<NotificationCompat.MessagingStyle.Message> messages = new LinkedList<>();
private SlideDeck slideDeck;
private CharSequence contentTitle;
private CharSequence contentText;
private Recipient threadRecipient;
private BubbleUtil.BubbleState defaultBubbleState;
public SingleRecipientNotificationBuilder(@NonNull Context context, @NonNull NotificationPrivacyPreference privacy)
{
super(new ContextThemeWrapper(context, R.style.TextSecure_LightTheme), privacy);
setSmallIcon(R.drawable.ic_notification);
setColor(context.getResources().getColor(R.color.core_ultramarine));
setCategory(NotificationCompat.CATEGORY_MESSAGE);
if (!NotificationChannels.supported()) {
setPriority(TextSecurePreferences.getNotificationPriority(context));
}
}
public void setThread(@NonNull Recipient recipient) {
String channelId = recipient.getNotificationChannel();
setChannelId(channelId != null ? channelId : NotificationChannels.getMessagesChannel(context));
if (privacy.isDisplayContact()) {
setContentTitle(recipient.getDisplayName(context));
if (recipient.getContactUri() != null) {
addPerson(recipient.getContactUri().toString());
}
setLargeIcon(getContactDrawable(recipient));
} else {
setContentTitle(context.getString(R.string.SingleRecipientNotificationBuilder_signal));
setLargeIcon(new GeneratedContactPhoto("Unknown", R.drawable.ic_profile_outline_40).asDrawable(context, AvatarColor.UNKNOWN.colorInt()));
}
setShortcutId(ConversationUtil.getShortcutId(recipient));
}
private Drawable getContactDrawable(@NonNull Recipient recipient) {
ContactPhoto contactPhoto = recipient.getContactPhoto();
FallbackContactPhoto fallbackContactPhoto = recipient.getFallbackContactPhoto();
if (contactPhoto != null) {
try {
List<Transformation<Bitmap>> transforms = new ArrayList<>();
if (recipient.shouldBlurAvatar()) {
transforms.add(new BlurTransformation(ApplicationDependencies.getApplication(), 0.25f, BlurTransformation.MAX_RADIUS));
}
transforms.add(new CircleCrop());
return GlideApp.with(context.getApplicationContext())
.load(contactPhoto)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.transform(new MultiTransformation<>(transforms))
.submit(context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width),
context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_height))
.get();
} catch (InterruptedException | ExecutionException e) {
return fallbackContactPhoto.asDrawable(context, recipient.getAvatarColor().colorInt());
}
} else {
return fallbackContactPhoto.asDrawable(context, recipient.getAvatarColor().colorInt());
}
}
public void setMessageCount(int messageCount) {
setContentInfo(String.valueOf(messageCount));
setNumber(messageCount);
}
public void setPrimaryMessageBody(@NonNull Recipient threadRecipients,
@NonNull Recipient individualRecipient,
@NonNull CharSequence message,
@Nullable SlideDeck slideDeck)
{
SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
if (privacy.isDisplayContact() && threadRecipients.isGroup()) {
stringBuilder.append(Util.getBoldedString(individualRecipient.getDisplayName(context) + ": "));
}
if (privacy.isDisplayMessage()) {
setContentText(stringBuilder.append(message));
this.slideDeck = slideDeck;
} else {
setContentText(stringBuilder.append(context.getString(R.string.SingleRecipientNotificationBuilder_new_message)));
}
}
public void addAndroidAutoAction(@NonNull PendingIntent androidAutoReplyIntent,
@NonNull PendingIntent androidAutoHeardIntent, long timestamp)
{
if (contentTitle == null || contentText == null)
return;
RemoteInput remoteInput = new RemoteInput.Builder(AndroidAutoReplyReceiver.VOICE_REPLY_KEY)
.setLabel(context.getString(R.string.MessageNotifier_reply))
.build();
NotificationCompat.CarExtender.UnreadConversation.Builder unreadConversationBuilder =
new NotificationCompat.CarExtender.UnreadConversation.Builder(contentTitle.toString())
.addMessage(contentText.toString())
.setLatestTimestamp(timestamp)
.setReadPendingIntent(androidAutoHeardIntent)
.setReplyAction(androidAutoReplyIntent, remoteInput);
extend(new NotificationCompat.CarExtender().setUnreadConversation(unreadConversationBuilder.build()));
}
public void addTurnOffTheseNotificationsAction(@NonNull PendingIntent turnOffTheseNotificationsIntent) {
Action turnOffTheseNotifications = new Action(R.drawable.check,
context.getString(R.string.MessageNotifier_turn_off_these_notifications),
turnOffTheseNotificationsIntent);
addAction(turnOffTheseNotifications);
}
public void addActions(@NonNull PendingIntent markReadIntent,
@NonNull PendingIntent quickReplyIntent,
@NonNull PendingIntent wearableReplyIntent,
@NonNull ReplyMethod replyMethod,
boolean replyEnabled)
{
NotificationCompat.WearableExtender extender = new NotificationCompat.WearableExtender();
Action markAsReadAction = new Action(R.drawable.check,
context.getString(R.string.MessageNotifier_mark_read),
markReadIntent);
addAction(markAsReadAction);
extender.addAction(markAsReadAction);
if (replyEnabled) {
String actionName = context.getString(R.string.MessageNotifier_reply);
String label = context.getString(replyMethodLongDescription(replyMethod));
Action replyAction = new Action(R.drawable.ic_reply_white_36dp,
actionName,
quickReplyIntent);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
replyAction = new Action.Builder(R.drawable.ic_reply_white_36dp,
actionName,
wearableReplyIntent)
.addRemoteInput(new RemoteInput.Builder(DefaultMessageNotifier.EXTRA_REMOTE_REPLY)
.setLabel(label)
.build())
.build();
}
Action wearableReplyAction = new Action.Builder(R.drawable.ic_reply,
actionName,
wearableReplyIntent)
.addRemoteInput(new RemoteInput.Builder(DefaultMessageNotifier.EXTRA_REMOTE_REPLY)
.setLabel(label)
.build())
.build();
addAction(replyAction);
extend(extender.addAction(wearableReplyAction));
}
}
@StringRes
private static int replyMethodLongDescription(@NonNull ReplyMethod replyMethod) {
switch (replyMethod) {
case GroupMessage:
return R.string.MessageNotifier_reply;
case SecureMessage:
return R.string.MessageNotifier_signal_message;
case UnsecuredSmsMessage:
return R.string.MessageNotifier_unsecured_sms;
default:
return R.string.MessageNotifier_reply;
}
}
public void addMessageBody(@NonNull Recipient threadRecipient,
@NonNull Recipient individualRecipient,
@Nullable CharSequence messageBody,
long timestamp,
@Nullable SlideDeck slideDeck)
{
SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
Person.Builder personBuilder = new Person.Builder()
.setKey(ConversationUtil.getShortcutId(individualRecipient))
.setBot(false);
this.threadRecipient = threadRecipient;
if (privacy.isDisplayContact()) {
personBuilder.setName(individualRecipient.getDisplayName(context));
personBuilder.setUri(individualRecipient.isSystemContact() ? individualRecipient.getContactUri().toString() : null);
Bitmap bitmap = getLargeBitmap(getContactDrawable(individualRecipient));
if (bitmap != null) {
personBuilder.setIcon(IconCompat.createWithBitmap(bitmap));
}
} else {
personBuilder.setName("");
}
final CharSequence text;
if (privacy.isDisplayMessage()) {
text = messageBody == null ? "" : messageBody;
} else {
text = stringBuilder.append(context.getString(R.string.SingleRecipientNotificationBuilder_new_message));
}
Uri dataUri = null;
String mimeType = null;
if (slideDeck != null && slideDeck.getThumbnailSlide() != null) {
Slide thumbnail = slideDeck.getThumbnailSlide();
if (Build.VERSION.SDK_INT >= 28) {
dataUri = thumbnail.getPublicUri();
} else {
dataUri = thumbnail.getUri();
}
mimeType = thumbnail.getContentType();
}
messages.add(new NotificationCompat.MessagingStyle.Message(text, timestamp, personBuilder.build()).setData(mimeType, dataUri));
}
public void setDefaultBubbleState(@NonNull BubbleUtil.BubbleState bubbleState) {
this.defaultBubbleState = bubbleState;
}
@Override
public Notification build() {
if (privacy.isDisplayMessage()) {
Optional<Uri> largeIconUri = getLargeIconUri(slideDeck);
Optional<Uri> bigPictureUri = getBigPictureUri(slideDeck);
if (messages.size() == 1 && largeIconUri.isPresent()) {
setLargeIcon(getNotificationPicture(largeIconUri.get(), LARGE_ICON_DIMEN));
}
if (messages.size() == 1 && bigPictureUri.isPresent() && Build.VERSION.SDK_INT < ConversationUtil.CONVERSATION_SUPPORT_VERSION) {
setStyle(new NotificationCompat.BigPictureStyle()
.bigPicture(getNotificationPicture(bigPictureUri.get(), BIG_PICTURE_DIMEN))
.setSummaryText(getBigText()));
} else {
if (Build.VERSION.SDK_INT >= 24) {
applyMessageStyle();
if (Build.VERSION.SDK_INT >= ConversationUtil.CONVERSATION_SUPPORT_VERSION) {
applyBubbleMetadata();
}
} else {
applyLegacy();
}
}
}
return super.build();
}
private void applyMessageStyle() {
ConversationUtil.pushShortcutForRecipientIfNeededSync(context, threadRecipient);
NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(ConversationUtil.buildPersonCompat(context, Recipient.self()));
if (threadRecipient.isGroup()) {
if (privacy.isDisplayContact()) {
messagingStyle.setConversationTitle(threadRecipient.getDisplayName(context));
} else {
messagingStyle.setConversationTitle(context.getString(R.string.SingleRecipientNotificationBuilder_signal));
}
messagingStyle.setGroupConversation(true);
}
Stream.of(messages).forEach(messagingStyle::addMessage);
setStyle(messagingStyle);
}
private void applyBubbleMetadata() {
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(threadRecipient);
PendingIntent intent = PendingIntent.getActivity(context, 0, ConversationIntents.createBubbleIntent(context, threadRecipient.getId(), threadId), 0);
NotificationCompat.BubbleMetadata bubbleMetadata = new NotificationCompat.BubbleMetadata.Builder()
.setAutoExpandBubble(defaultBubbleState == BubbleUtil.BubbleState.SHOWN)
.setDesiredHeight(600)
.setIcon(AvatarUtil.getIconCompatForShortcut(context, threadRecipient))
.setSuppressNotification(defaultBubbleState == BubbleUtil.BubbleState.SHOWN)
.setIntent(intent)
.build();
setBubbleMetadata(bubbleMetadata);
}
private void applyLegacy() {
setStyle(new NotificationCompat.BigTextStyle().bigText(getBigText()));
}
private void setLargeIcon(@Nullable Drawable drawable) {
if (drawable != null) {
setLargeIcon(getLargeBitmap(drawable));
}
}
private @Nullable Bitmap getLargeBitmap(@Nullable Drawable drawable) {
if (drawable != null) {
int largeIconTargetSize = context.getResources().getDimensionPixelSize(R.dimen.contact_photo_target_size);
return BitmapUtil.createFromDrawable(drawable, largeIconTargetSize, largeIconTargetSize);
}
return null;
}
private static Optional<Uri> getLargeIconUri(@Nullable SlideDeck slideDeck) {
if (slideDeck == null) {
return Optional.absent();
}
Slide thumbnailSlide = Optional.fromNullable(slideDeck.getThumbnailSlide()).or(Optional.fromNullable(slideDeck.getStickerSlide())).orNull();
return getThumbnailUri(thumbnailSlide);
}
private static Optional<Uri> getBigPictureUri(@Nullable SlideDeck slideDeck) {
if (slideDeck == null) {
return Optional.absent();
}
Slide thumbnailSlide = slideDeck.getThumbnailSlide();
return getThumbnailUri(thumbnailSlide);
}
private static Optional<Uri> getThumbnailUri(@Nullable Slide slide) {
if (slide != null && !slide.isInProgress() && slide.getUri() != null) {
return Optional.of(slide.getUri());
} else {
return Optional.absent();
}
}
private Bitmap getNotificationPicture(@NonNull Uri uri, int dimension)
{
try {
return GlideApp.with(context.getApplicationContext())
.asBitmap()
.load(new DecryptableStreamUriLoader.DecryptableUri(uri))
.diskCacheStrategy(DiskCacheStrategy.NONE)
.submit(dimension, dimension)
.get();
} catch (InterruptedException | ExecutionException e) {
Log.w(TAG, e);
return Bitmap.createBitmap(dimension, dimension, Bitmap.Config.RGB_565);
}
}
@Override
public NotificationCompat.Builder setContentTitle(CharSequence contentTitle) {
this.contentTitle = contentTitle;
return super.setContentTitle(contentTitle);
}
public NotificationCompat.Builder setContentText(CharSequence contentText) {
this.contentText = trimToDisplayLength(contentText);
return super.setContentText(this.contentText);
}
private CharSequence getBigText() {
SpannableStringBuilder content = new SpannableStringBuilder();
for (int i = 0; i < messages.size(); i++) {
content.append(getBigTextFor(messages.get(i)));
if (i < messages.size() - 1) {
content.append('\n');
}
}
return content;
}
private CharSequence getBigTextFor(NotificationCompat.MessagingStyle.Message message) {
SpannableStringBuilder content = new SpannableStringBuilder();
if (message.getPerson() != null && message.getPerson().getName() != null && threadRecipient.isGroup()) {
content.append(Util.getBoldedString(message.getPerson().getName().toString())).append(": ");
}
return trimToDisplayLength(content.append(message.getText()));
}
}

View File

@@ -18,7 +18,6 @@ import org.thoughtcrime.securesms.database.MessageDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.messages.IncomingMessageObserver
import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier
import org.thoughtcrime.securesms.notifications.MessageNotifier
import org.thoughtcrime.securesms.notifications.MessageNotifier.ReminderReceiver
import org.thoughtcrime.securesms.notifications.NotificationCancellationHelper
@@ -89,7 +88,7 @@ class MessageNotifierV2(context: Application) : MessageNotifier {
}
override fun updateNotification(context: Context, threadId: Long) {
if (System.currentTimeMillis() - lastDesktopActivityTimestamp < DefaultMessageNotifier.DESKTOP_ACTIVITY_PERIOD) {
if (System.currentTimeMillis() - lastDesktopActivityTimestamp < DESKTOP_ACTIVITY_PERIOD) {
Log.i(TAG, "Scheduling delayed notification...")
executor.enqueue(context, threadId)
} else {
@@ -264,7 +263,13 @@ class MessageNotifierV2(context: Application) : MessageNotifier {
companion object {
val TAG: String = Log.tag(MessageNotifierV2::class.java)
private val REMINDER_TIMEOUT: Long = TimeUnit.MINUTES.toMillis(2)
val MIN_AUDIBLE_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(2)
val DESKTOP_ACTIVITY_PERIOD = TimeUnit.MINUTES.toMillis(1)
const val EXTRA_REMOTE_REPLY = "extra_remote_reply"
const val NOTIFICATION_GROUP = "messages"
private fun updateBadge(context: Context, count: Int) {
try {

View File

@@ -1,19 +1,15 @@
package org.thoughtcrime.securesms.notifications.v2
import android.annotation.TargetApi
import android.app.Notification
import android.app.PendingIntent
import android.app.Person
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.drawable.Icon
import android.net.Uri
import android.os.Build
import android.text.TextUtils
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import androidx.core.app.NotificationCompat
import androidx.core.app.RemoteInput
@@ -22,7 +18,6 @@ import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.conversation.ConversationIntents
import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.notifications.ReplyMethod
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference
@@ -171,7 +166,7 @@ sealed class NotificationBuilder(protected val context: Context) {
/**
* Notification builder using solely androidx/compat libraries.
*/
class NotificationBuilderCompat(context: Context) : NotificationBuilder(context) {
private class NotificationBuilderCompat(context: Context) : NotificationBuilder(context) {
val builder: NotificationCompat.Builder = NotificationCompat.Builder(context, NotificationChannels.getMessagesChannel(context))
override fun addActions(replyMethod: ReplyMethod, conversation: NotificationConversation) {
@@ -194,7 +189,7 @@ sealed class NotificationBuilder(protected val context: Context) {
val label: String = context.getString(replyMethod.toLongDescription())
val replyAction: NotificationCompat.Action = if (Build.VERSION.SDK_INT >= 24) {
NotificationCompat.Action.Builder(R.drawable.ic_reply_white_36dp, actionName, remoteReply)
.addRemoteInput(RemoteInput.Builder(DefaultMessageNotifier.EXTRA_REMOTE_REPLY).setLabel(label).build())
.addRemoteInput(RemoteInput.Builder(MessageNotifierV2.EXTRA_REMOTE_REPLY).setLabel(label).build())
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
.setShowsUserInterface(false)
.build()
@@ -203,7 +198,7 @@ sealed class NotificationBuilder(protected val context: Context) {
}
val wearableReplyAction = NotificationCompat.Action.Builder(R.drawable.ic_reply, actionName, remoteReply)
.addRemoteInput(RemoteInput.Builder(DefaultMessageNotifier.EXTRA_REMOTE_REPLY).setLabel(label).build())
.addRemoteInput(RemoteInput.Builder(MessageNotifierV2.EXTRA_REMOTE_REPLY).setLabel(label).build())
.build()
builder.addAction(replyAction)
@@ -451,228 +446,6 @@ sealed class NotificationBuilder(protected val context: Context) {
builder.setSubText(subText)
}
}
/**
* Notification builder using solely on device OS libraries.
*/
@TargetApi(28)
class NotificationBuilderOS(context: Context) : NotificationBuilder(context) {
val builder: Notification.Builder = Notification.Builder(context, NotificationChannels.getMessagesChannel(context))
override fun addActions(replyMethod: ReplyMethod, conversation: NotificationConversation) {
val markAsRead: PendingIntent = conversation.getMarkAsReadIntent(context)
val markAsReadAction: Notification.Action = Notification.Action.Builder(context.getIcon(R.drawable.check), context.getString(R.string.MessageNotifier_mark_read), markAsRead)
.setSemanticAction(Notification.Action.SEMANTIC_ACTION_MARK_AS_READ)
.build()
val extender: Notification.WearableExtender = Notification.WearableExtender()
builder.addAction(markAsReadAction)
extender.addAction(markAsReadAction)
if (conversation.mostRecentNotification.canReply(context)) {
val remoteReply: PendingIntent = conversation.getRemoteReplyIntent(context, replyMethod)
val actionName: String = context.getString(R.string.MessageNotifier_reply)
val label: String = context.getString(replyMethod.toLongDescription())
val replyAction: Notification.Action = Notification.Action.Builder(context.getIcon(R.drawable.ic_reply_white_36dp), actionName, remoteReply)
.addRemoteInput(android.app.RemoteInput.Builder(DefaultMessageNotifier.EXTRA_REMOTE_REPLY).setLabel(label).build())
.setSemanticAction(Notification.Action.SEMANTIC_ACTION_REPLY)
.build()
val wearableReplyAction = Notification.Action.Builder(context.getIcon(R.drawable.ic_reply), actionName, remoteReply)
.addRemoteInput(android.app.RemoteInput.Builder(DefaultMessageNotifier.EXTRA_REMOTE_REPLY).setLabel(label).build())
.build()
builder.addAction(replyAction)
extender.addAction(wearableReplyAction)
}
builder.extend(extender)
}
override fun addMarkAsReadActionActual(state: NotificationStateV2) {
val markAllAsReadAction: Notification.Action = Notification.Action.Builder(
context.getIcon(R.drawable.check),
context.getString(R.string.MessageNotifier_mark_all_as_read),
state.getMarkAsReadIntent(context)
).build()
builder.addAction(markAllAsReadAction)
builder.extend(Notification.WearableExtender().addAction(markAllAsReadAction))
}
override fun addTurnOffJoinedNotificationsAction(pendingIntent: PendingIntent) {
val turnOffTheseNotifications: Notification.Action = Notification.Action.Builder(
context.getIcon(R.drawable.check),
context.getString(R.string.MessageNotifier_turn_off_these_notifications),
pendingIntent
).build()
builder.addAction(turnOffTheseNotifications)
}
override fun addMessagesActual(conversation: NotificationConversation, includeShortcut: Boolean) {
val self: Person = Person.Builder()
.setBot(false)
.setName(if (includeShortcut) Recipient.self().getDisplayName(context) else context.getString(R.string.SingleRecipientNotificationBuilder_you))
.setIcon(if (includeShortcut) Recipient.self().getContactDrawable(context).toLargeBitmap(context).toIcon() else null)
.setKey(ConversationUtil.getShortcutId(Recipient.self().id))
.build()
val messagingStyle: Notification.MessagingStyle = Notification.MessagingStyle(self)
messagingStyle.conversationTitle = conversation.getConversationTitle(context)
messagingStyle.isGroupConversation = conversation.isGroup
conversation.notificationItems.forEach { notificationItem ->
val personBuilder: Person.Builder = Person.Builder()
.setBot(false)
.setName(notificationItem.getPersonName(context))
.setUri(notificationItem.getPersonUri(context))
.setIcon(notificationItem.getPersonIcon(context).toIcon())
if (includeShortcut) {
personBuilder.setKey(ConversationUtil.getShortcutId(notificationItem.individualRecipient))
}
val (dataUri: Uri?, mimeType: String?) = notificationItem.getThumbnailInfo(context)
messagingStyle.addMessage(Notification.MessagingStyle.Message(notificationItem.getPrimaryText(context), notificationItem.timestamp, personBuilder.build()).setData(mimeType, dataUri))
}
builder.style = messagingStyle
}
override fun setBubbleMetadataActual(conversation: NotificationConversation, bubbleState: BubbleUtil.BubbleState) {
if (Build.VERSION.SDK_INT < ConversationUtil.CONVERSATION_SUPPORT_VERSION) {
return
}
val intent = PendingIntent.getActivity(
context,
0,
ConversationIntents.createBubbleIntent(context, conversation.recipient.id, conversation.threadId),
0
)
val bubbleMetadata = Notification.BubbleMetadata.Builder(intent, AvatarUtil.getIconForShortcut(context, conversation.recipient))
.setAutoExpandBubble(bubbleState === BubbleUtil.BubbleState.SHOWN)
.setDesiredHeight(600)
.setSuppressNotification(bubbleState === BubbleUtil.BubbleState.SHOWN)
.build()
builder.setBubbleMetadata(bubbleMetadata)
}
override fun addMessagesActual(state: NotificationStateV2) {
// Intentionally left blank
}
override fun setLights(@ColorInt color: Int, onTime: Int, offTime: Int) {
// Intentionally left blank
}
override fun setAlarms(recipient: Recipient?) {
// Intentionally left blank
}
override fun setSmallIcon(drawable: Int) {
builder.setSmallIcon(drawable)
}
override fun setColor(@ColorInt color: Int) {
builder.setColor(color)
}
override fun setCategory(category: String) {
builder.setCategory(category)
}
override fun setGroup(group: String) {
builder.setGroup(group)
}
override fun setGroupAlertBehavior(behavior: Int) {
builder.setGroupAlertBehavior(behavior)
}
override fun setChannelId(channelId: String) {
builder.setChannelId(channelId)
}
override fun setContentTitle(contentTitle: CharSequence) {
builder.setContentTitle(contentTitle)
}
override fun setLargeIcon(largeIcon: Bitmap?) {
builder.setLargeIcon(largeIcon)
}
override fun setShortcutIdActual(shortcutId: String) {
builder.setShortcutId(shortcutId)
}
@Suppress("DEPRECATION")
override fun setContentInfo(contentInfo: String) {
builder.setContentInfo(contentInfo)
}
override fun setNumber(number: Int) {
builder.setNumber(number)
}
override fun setContentText(contentText: CharSequence?) {
builder.setContentText(contentText)
}
override fun setTicker(ticker: CharSequence?) {
builder.setTicker(ticker)
}
override fun setContentIntent(pendingIntent: PendingIntent?) {
builder.setContentIntent(pendingIntent)
}
override fun setDeleteIntent(deleteIntent: PendingIntent?) {
builder.setDeleteIntent(deleteIntent)
}
override fun setSortKey(sortKey: String) {
builder.setSortKey(sortKey)
}
override fun setOnlyAlertOnce(onlyAlertOnce: Boolean) {
builder.setOnlyAlertOnce(onlyAlertOnce)
}
override fun setPriority(priority: Int) {
// Intentionally left blank
}
override fun setAutoCancel(autoCancel: Boolean) {
builder.setAutoCancel(autoCancel)
}
override fun build(): Notification {
return builder.build()
}
override fun addPersonActual(recipient: Recipient) {
builder.addPerson(ConversationUtil.buildPerson(context, recipient))
}
override fun setWhen(timestamp: Long) {
builder.setWhen(timestamp)
builder.setShowWhen(true)
}
override fun setGroupSummary(isGroupSummary: Boolean) {
builder.setGroupSummary(isGroupSummary)
}
override fun setSubText(subText: String) {
builder.setSubText(subText)
}
}
}
private fun Bitmap?.toIconCompat(): IconCompat? {
@@ -683,20 +456,6 @@ private fun Bitmap?.toIconCompat(): IconCompat? {
}
}
@RequiresApi(23)
private fun Bitmap?.toIcon(): Icon? {
return if (this != null) {
Icon.createWithBitmap(this)
} else {
null
}
}
@RequiresApi(23)
private fun Context.getIcon(@DrawableRes drawableRes: Int): Icon {
return Icon.createWithResource(this, drawableRes)
}
@StringRes
private fun ReplyMethod.toLongDescription(): Int {
return when (this) {

View File

@@ -21,8 +21,8 @@ import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.conversation.ConversationIntents
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.notifications.NotificationIds
import org.thoughtcrime.securesms.recipients.Recipient
@@ -181,7 +181,7 @@ object NotificationFactory {
setSmallIcon(R.drawable.ic_notification)
setColor(ContextCompat.getColor(context, R.color.core_ultramarine))
setCategory(NotificationCompat.CATEGORY_MESSAGE)
setGroup(DefaultMessageNotifier.NOTIFICATION_GROUP)
setGroup(MessageNotifierV2.NOTIFICATION_GROUP)
setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
setChannelId(conversation.getChannelId(context))
setContentTitle(conversation.getContentTitle(context))
@@ -225,7 +225,7 @@ object NotificationFactory {
setSmallIcon(R.drawable.ic_notification)
setColor(ContextCompat.getColor(context, R.color.core_ultramarine))
setCategory(NotificationCompat.CATEGORY_MESSAGE)
setGroup(DefaultMessageNotifier.NOTIFICATION_GROUP)
setGroup(MessageNotifierV2.NOTIFICATION_GROUP)
setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
setChannelId(NotificationChannels.getMessagesChannel(context))
setContentTitle(context.getString(R.string.app_name))
@@ -253,7 +253,7 @@ object NotificationFactory {
private fun notifyInThread(context: Context, recipient: Recipient, lastAudibleNotification: Long) {
if (!SignalStore.settings().isMessageNotificationsInChatSoundsEnabled ||
ServiceUtil.getAudioManager(context).ringerMode != AudioManager.RINGER_MODE_NORMAL ||
(System.currentTimeMillis() - lastAudibleNotification) < DefaultMessageNotifier.MIN_AUDIBLE_PERIOD_MILLIS
(System.currentTimeMillis() - lastAudibleNotification) < MessageNotifierV2.MIN_AUDIBLE_PERIOD_MILLIS
) {
return
}
@@ -343,6 +343,38 @@ object NotificationFactory {
NotificationManagerCompat.from(context).safelyNotify(context, recipient, threadId.toInt(), builder.build())
}
@JvmStatic
fun notifyToBubbleConversation(context: Context, recipient: Recipient, threadId: Long) {
val builder: NotificationBuilder = NotificationBuilder.create(context)
val conversation = NotificationConversation(
recipient = recipient,
threadId = threadId,
notificationItems = listOf(
MessageNotification(
threadRecipient = recipient,
record = InMemoryMessageRecord.ForceConversationBubble(recipient, threadId)
)
)
)
builder.apply {
setSmallIcon(R.drawable.ic_notification)
setColor(ContextCompat.getColor(context, R.color.core_ultramarine))
setCategory(NotificationCompat.CATEGORY_MESSAGE)
setChannelId(conversation.getChannelId(context))
setContentTitle(conversation.getContentTitle(context))
setLargeIcon(conversation.getContactLargeIcon(context).toLargeBitmap(context))
addPerson(conversation.recipient)
setShortcutId(ConversationUtil.getShortcutId(conversation.recipient))
addMessages(conversation)
setBubbleMetadata(conversation, BubbleUtil.BubbleState.SHOWN)
}
Log.d(TAG, "Posting Notification for requested bubble")
NotificationManagerCompat.from(context).safelyNotify(context, recipient, conversation.notificationId, builder.build())
}
private fun NotificationManagerCompat.safelyNotify(context: Context, threadRecipient: Recipient?, notificationId: Int, notification: Notification) {
try {
notify(notificationId, notification)

View File

@@ -19,7 +19,6 @@ import org.thoughtcrime.securesms.database.model.ReactionRecord
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.mms.SlideDeck
import org.thoughtcrime.securesms.notifications.AbstractNotificationBuilder
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientUtil
import org.thoughtcrime.securesms.service.KeyCachingService
@@ -30,6 +29,7 @@ import org.thoughtcrime.securesms.util.Util
private val TAG: String = Log.tag(NotificationItemV2::class.java)
private const val EMOJI_REPLACEMENT_STRING = "__EMOJI__"
private const val MAX_DISPLAY_LENGTH = 500
/**
* Base for messaged-based notifications. Represents a single notification.
@@ -145,10 +145,10 @@ sealed class NotificationItemV2(val threadRecipient: Recipient, protected val re
private fun CharSequence?.trimToDisplayLength(): CharSequence {
val text: CharSequence = this ?: ""
return if (text.length <= AbstractNotificationBuilder.MAX_DISPLAY_LENGTH) {
return if (text.length <= MAX_DISPLAY_LENGTH) {
text
} else {
text.subSequence(0, AbstractNotificationBuilder.MAX_DISPLAY_LENGTH)
text.subSequence(0, MAX_DISPLAY_LENGTH)
}
}

View File

@@ -18,9 +18,8 @@ import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier;
import org.thoughtcrime.securesms.notifications.NotificationIds;
import org.thoughtcrime.securesms.notifications.SingleRecipientNotificationBuilder;
import org.thoughtcrime.securesms.notifications.v2.NotificationFactory;
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
@@ -90,16 +89,8 @@ public final class BubbleUtil {
if (activeThreadNotification != null && activeThreadNotification.deleteIntent != null) {
ApplicationDependencies.getMessageNotifier().updateNotification(context, threadId, BubbleState.SHOWN);
} else {
Recipient recipient = Recipient.resolved(recipientId);
SingleRecipientNotificationBuilder builder = new SingleRecipientNotificationBuilder(context, SignalStore.settings().getMessageNotificationsPrivacy());
builder.addMessageBody(recipient, recipient, "", System.currentTimeMillis(), null);
builder.setThread(recipient);
builder.setDefaultBubbleState(BubbleState.SHOWN);
builder.setGroup(DefaultMessageNotifier.NOTIFICATION_GROUP);
Log.d(TAG, "Posting Notification for requested bubble");
notificationManager.notify(NotificationIds.getNotificationIdForThread(threadId), builder.build());
Recipient recipient = Recipient.resolved(recipientId);
NotificationFactory.notifyToBubbleConversation(context, recipient, threadId);
}
}
});

View File

@@ -1,7 +1,5 @@
package org.thoughtcrime.securesms.util;
import android.app.Application;
import android.os.Build;
import android.text.TextUtils;
import androidx.annotation.NonNull;
@@ -77,7 +75,6 @@ public final class FeatureFlags {
private static final String ANIMATED_STICKER_MIN_TOTAL_MEMORY = "android.animatedStickerMinTotalMemory";
private static final String MESSAGE_PROCESSOR_ALARM_INTERVAL = "android.messageProcessor.alarmIntervalMins";
private static final String MESSAGE_PROCESSOR_DELAY = "android.messageProcessor.foregroundDelayMs";
private static final String NOTIFICATION_REWRITE = "android.notificationRewrite";
private static final String MP4_GIF_SEND_SUPPORT = "android.mp4GifSendSupport";
private static final String MEDIA_QUALITY_LEVELS = "android.mediaQuality.levels";
private static final String RETRY_RECEIPT_LIFESPAN = "android.retryReceiptLifespan";
@@ -114,7 +111,6 @@ public final class FeatureFlags {
ANIMATED_STICKER_MIN_TOTAL_MEMORY,
MESSAGE_PROCESSOR_ALARM_INTERVAL,
MESSAGE_PROCESSOR_DELAY,
NOTIFICATION_REWRITE,
MP4_GIF_SEND_SUPPORT,
MEDIA_QUALITY_LEVELS,
RETRY_RECEIPT_LIFESPAN,
@@ -163,7 +159,6 @@ public final class FeatureFlags {
MESSAGE_PROCESSOR_ALARM_INTERVAL,
MESSAGE_PROCESSOR_DELAY,
GV1_FORCED_MIGRATE,
NOTIFICATION_REWRITE,
MP4_GIF_SEND_SUPPORT,
MEDIA_QUALITY_LEVELS,
RETRY_RECEIPT_LIFESPAN,
@@ -354,11 +349,6 @@ public final class FeatureFlags {
return getInteger(ANIMATED_STICKER_MIN_TOTAL_MEMORY, (int) ByteUnit.GIGABYTES.toMegabytes(3));
}
/** Whether or not to use the new notification system. */
public static boolean useNewNotificationSystem() {
return Build.VERSION.SDK_INT >= 26 || getBoolean(NOTIFICATION_REWRITE, false);
}
public static boolean mp4GifSendSupport() {
return getBoolean(MP4_GIF_SEND_SUPPORT, false);
}