Move all files to natural position.

This commit is contained in:
Alan Evans
2020-01-06 10:52:48 -05:00
parent 0df36047e7
commit 9ebe920195
3016 changed files with 6 additions and 36 deletions

View File

@@ -0,0 +1,100 @@
package org.thoughtcrime.securesms.notifications;
import android.app.Notification;
import android.content.Context;
import android.graphics.Color;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.RecipientDatabase;
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 = AbstractNotificationBuilder.class.getSimpleName();
private 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.toShortString(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) : TextSecurePreferences.getNotificationRingtone(context);
boolean defaultVibrate = NotificationChannels.supported() ? NotificationChannels.getMessageVibrate(context) : TextSecurePreferences.isNotificationVibrateEnabled(context);
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 = TextSecurePreferences.getNotificationLedColor(context);
String ledBlinkPattern = TextSecurePreferences.getNotificationLedPattern(context);
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

@@ -0,0 +1,77 @@
/*
* 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 androidx.core.app.NotificationManagerCompat;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
import org.whispersystems.libsignal.logging.Log;
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 = AndroidAutoHeardReceiver.class.getSimpleName();
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);
NotificationManagerCompat.from(context).cancel(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);
}
MessageNotifier.updateNotification(context);
MarkReadReceiver.process(context, messageIdsCollection);
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
}

View File

@@ -0,0 +1,104 @@
/*
* 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.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
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 org.whispersystems.libsignal.logging.Log;
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 = AndroidAutoReplyReceiver.class.getSimpleName();
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("AndroidAutoReplyReceiver", "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());
replyThreadId = MessageSender.send(context, reply, threadId, false, null);
} else {
Log.w("AndroidAutoReplyReceiver", "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);
MessageNotifier.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

@@ -0,0 +1,41 @@
package org.thoughtcrime.securesms.notifications;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import org.thoughtcrime.securesms.database.DatabaseFactory;
public class DeleteNotificationReceiver extends BroadcastReceiver {
public static String DELETE_NOTIFICATION_ACTION = "org.thoughtcrime.securesms.DELETE_NOTIFICATION";
public static String EXTRA_IDS = "message_ids";
public static String EXTRA_MMS = "is_mms";
@Override
public void onReceive(final Context context, Intent intent) {
if (DELETE_NOTIFICATION_ACTION.equals(intent.getAction())) {
MessageNotifier.clearReminder(context);
final long[] ids = intent.getLongArrayExtra(EXTRA_IDS);
final boolean[] mms = intent.getBooleanArrayExtra(EXTRA_MMS);
if (ids == null || mms == null || ids.length != mms.length) return;
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
for (int i=0;i<ids.length;i++) {
if (!mms[i]) DatabaseFactory.getSmsDatabase(context).markAsNotified(ids[i]);
else DatabaseFactory.getMmsDatabase(context).markAsNotified(ids[i]);
}
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
}

View File

@@ -0,0 +1,97 @@
package org.thoughtcrime.securesms.notifications;
import android.app.NotificationManager;
import android.content.Context;
import android.database.Cursor;
import android.os.Build;
import android.provider.ContactsContract;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.WorkerThread;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.ServiceUtil;
import java.util.concurrent.TimeUnit;
public final class DoNotDisturbUtil {
private static final String TAG = Log.tag(DoNotDisturbUtil.class);
private DoNotDisturbUtil() {
}
@WorkerThread
public static boolean shouldDisturbUserWithCall(@NonNull Context context, @NonNull Recipient recipient) {
if (Build.VERSION.SDK_INT <= 23) return true;
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
switch (notificationManager.getCurrentInterruptionFilter()) {
case NotificationManager.INTERRUPTION_FILTER_PRIORITY:
return handlePriority(context, notificationManager, recipient);
case NotificationManager.INTERRUPTION_FILTER_NONE:
case NotificationManager.INTERRUPTION_FILTER_ALARMS:
return false;
default:
return true;
}
}
@RequiresApi(23)
private static boolean handlePriority(@NonNull Context context, @NonNull NotificationManager notificationManager, @NonNull Recipient recipient) {
if (Build.VERSION.SDK_INT < 28 && !notificationManager.isNotificationPolicyAccessGranted()) {
Log.w(TAG, "Notification Policy is not granted");
return true;
}
final NotificationManager.Policy policy = notificationManager.getNotificationPolicy();
final boolean areCallsPrioritized = (policy.priorityCategories & NotificationManager.Policy.PRIORITY_CATEGORY_CALLS) != 0;
final boolean isRepeatCallerEnabled = (policy.priorityCategories & NotificationManager.Policy.PRIORITY_CATEGORY_REPEAT_CALLERS) != 0;
if (!areCallsPrioritized && !isRepeatCallerEnabled) {
return false;
}
if (areCallsPrioritized && !isRepeatCallerEnabled) {
return isContactPriority(context, recipient, policy.priorityCallSenders);
}
if (!areCallsPrioritized) {
return isRepeatCaller(context, recipient);
}
return isContactPriority(context, recipient, policy.priorityCallSenders) || isRepeatCaller(context, recipient);
}
private static boolean isContactPriority(@NonNull Context context, @NonNull Recipient recipient, int priority) {
switch (priority) {
case NotificationManager.Policy.PRIORITY_SENDERS_ANY:
return true;
case NotificationManager.Policy.PRIORITY_SENDERS_CONTACTS:
return recipient.resolve().isSystemContact();
case NotificationManager.Policy.PRIORITY_SENDERS_STARRED:
return isContactStarred(context, recipient);
}
Log.w(TAG, "Unknown priority " + priority);
return true;
}
private static boolean isContactStarred(@NonNull Context context, @NonNull Recipient recipient) {
if (!recipient.resolve().isSystemContact()) return false;
try (Cursor cursor = context.getContentResolver().query(recipient.resolve().getContactUri(), new String[]{ContactsContract.Contacts.STARRED}, null, null, null)) {
if (cursor == null || !cursor.moveToFirst()) return false;
return cursor.getInt(cursor.getColumnIndex(ContactsContract.Contacts.STARRED)) == 1;
}
}
private static boolean isRepeatCaller(@NonNull Context context, @NonNull Recipient recipient) {
return DatabaseFactory.getThreadDatabase(context).hasCalledSince(recipient, System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(15));
}
}

View File

@@ -0,0 +1,31 @@
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.icon_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

@@ -0,0 +1,13 @@
package org.thoughtcrime.securesms.notifications;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
public class LocaleChangedReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
NotificationChannels.create(context);
}
}

View File

@@ -0,0 +1,102 @@
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 androidx.annotation.NonNull;
import androidx.core.app.NotificationManagerCompat;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase.ExpirationInfo;
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob;
import org.thoughtcrime.securesms.jobs.SendReadReceiptJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
public class MarkReadReceiver extends BroadcastReceiver {
private static final String TAG = MarkReadReceiver.class.getSimpleName();
public static final String CLEAR_ACTION = "org.thoughtcrime.securesms.notifications.CLEAR";
public static final String THREAD_IDS_EXTRA = "thread_ids";
public static final String NOTIFICATION_ID_EXTRA = "notification_id";
@SuppressLint("StaticFieldLeak")
@Override
public void onReceive(final Context context, Intent intent) {
if (!CLEAR_ACTION.equals(intent.getAction()))
return;
final long[] threadIds = intent.getLongArrayExtra(THREAD_IDS_EXTRA);
if (threadIds != null) {
NotificationManagerCompat.from(context).cancel(intent.getIntExtra(NOTIFICATION_ID_EXTRA, -1));
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
List<MarkedMessageInfo> messageIdsCollection = new LinkedList<>();
for (long threadId : threadIds) {
Log.i(TAG, "Marking as read: " + threadId);
List<MarkedMessageInfo> messageIds = DatabaseFactory.getThreadDatabase(context).setRead(threadId, true);
messageIdsCollection.addAll(messageIds);
}
process(context, messageIdsCollection);
MessageNotifier.updateNotification(context);
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
public static void process(@NonNull Context context, @NonNull List<MarkedMessageInfo> markedReadMessages) {
if (markedReadMessages.isEmpty()) return;
List<SyncMessageId> syncMessageIds = new LinkedList<>();
for (MarkedMessageInfo messageInfo : markedReadMessages) {
scheduleDeletion(context, messageInfo.getExpirationInfo());
syncMessageIds.add(messageInfo.getSyncMessageId());
}
ApplicationDependencies.getJobManager().add(new MultiDeviceReadUpdateJob(syncMessageIds));
Map<RecipientId, List<SyncMessageId>> recipientIdMap = Stream.of(markedReadMessages)
.map(MarkedMessageInfo::getSyncMessageId)
.collect(Collectors.groupingBy(SyncMessageId::getRecipientId));
for (Map.Entry<RecipientId, List<SyncMessageId>> entry : recipientIdMap.entrySet()) {
List<Long> timestamps = Stream.of(entry.getValue()).map(SyncMessageId::getTimetamp).toList();
ApplicationDependencies.getJobManager().add(new SendReadReceiptJob(entry.getKey(), timestamps));
}
}
private static void scheduleDeletion(Context context, ExpirationInfo expirationInfo) {
if (expirationInfo.getExpiresIn() > 0 && expirationInfo.getExpireStarted() <= 0) {
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
if (expirationInfo.isMms()) DatabaseFactory.getMmsDatabase(context).markExpireStarted(expirationInfo.getId());
else DatabaseFactory.getSmsDatabase(context).markExpireStarted(expirationInfo.getId());
expirationManager.scheduleDeletion(expirationInfo.getId(), expirationInfo.isMms(), expirationInfo.getExpiresIn());
}
}
}

View File

@@ -0,0 +1,721 @@
/*
* 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.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.service.notification.StatusBarNotification;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.contactshare.ContactUtil;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
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.logging.Log;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.service.IncomingMessageObserver;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.MediaUtil;
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.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 MessageNotifier {
private static final String TAG = MessageNotifier.class.getSimpleName();
public static final String EXTRA_REMOTE_REPLY = "extra_remote_reply";
private static final String EMOJI_REPLACEMENT_STRING = "__EMOJI__";
private static final int SUMMARY_NOTIFICATION_ID = 1338;
private static final int PENDING_MESSAGES_ID = 1111;
private static final String NOTIFICATION_GROUP = "messages";
private static final long MIN_AUDIBLE_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(2);
private static final long DESKTOP_ACTIVITY_PERIOD = TimeUnit.MINUTES.toMillis(1);
private volatile static long visibleThread = -1;
private volatile static long lastDesktopActivityTimestamp = -1;
private volatile static long lastAudibleNotification = -1;
private static final CancelableExecutor executor = new CancelableExecutor();
public static void setVisibleThread(long threadId) {
visibleThread = threadId;
}
public static void setLastDesktopActivityTimestamp(long timestamp) {
lastDesktopActivityTimestamp = timestamp;
}
public static void notifyMessageDeliveryFailed(Context context, Recipient recipient, long threadId) {
if (visibleThread == threadId) {
sendInThreadNotification(context, recipient);
} else {
Intent intent = new Intent(context, ConversationActivity.class);
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId());
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
intent.setData((Uri.parse("custom://" + System.currentTimeMillis())));
FailedNotificationBuilder builder = new FailedNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context), intent);
((NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE))
.notify((int)threadId, builder.build());
}
}
public static void notifyMessagesPending(Context context) {
if (!TextSecurePreferences.isNotificationsEnabled(context) || ApplicationContext.getInstance(context).isAppVisible()) {
return;
}
PendingMessageNotificationBuilder builder = new PendingMessageNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context));
ServiceUtil.getNotificationManager(context).notify(PENDING_MESSAGES_ID, builder.build());
}
public static void cancelMessagesPending(Context context) {
ServiceUtil.getNotificationManager(context).cancel(PENDING_MESSAGES_ID);
}
public static void cancelDelayedNotifications() {
executor.cancel();
}
private static void cancelActiveNotifications(@NonNull Context context) {
NotificationManager notifications = ServiceUtil.getNotificationManager(context);
notifications.cancel(SUMMARY_NOTIFICATION_ID);
if (Build.VERSION.SDK_INT >= 23) {
try {
StatusBarNotification[] activeNotifications = notifications.getActiveNotifications();
for (StatusBarNotification activeNotification : activeNotifications) {
if (!CallNotificationBuilder.isWebRtcNotification(activeNotification.getId())) {
notifications.cancel(activeNotification.getId());
}
}
} catch (Throwable e) {
// XXX Appears to be a ROM bug, see #6043
Log.w(TAG, e);
notifications.cancelAll();
}
}
}
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() == SUMMARY_NOTIFICATION_ID) {
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() != SUMMARY_NOTIFICATION_ID &&
notification.getId() != KeyCachingService.SERVICE_RUNNING_ID &&
notification.getId() != IncomingMessageObserver.FOREGROUND_ID &&
notification.getId() != PENDING_MESSAGES_ID &&
!CallNotificationBuilder.isWebRtcNotification(notification.getId()))
{
for (NotificationItem item : notificationState.getNotifications()) {
if (notification.getId() == (SUMMARY_NOTIFICATION_ID + item.getThreadId())) {
validNotification = true;
break;
}
}
if (!validNotification) {
notifications.cancel(notification.getId());
}
}
}
} catch (Throwable e) {
// XXX Android ROM Bug, see #6043
Log.w(TAG, e);
}
}
}
public static void updateNotification(@NonNull Context context) {
if (!TextSecurePreferences.isNotificationsEnabled(context)) {
return;
}
updateNotification(context, -1, false, 0);
}
public static 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);
}
}
public static void updateNotification(@NonNull Context context,
long threadId,
boolean signal)
{
boolean isVisible = visibleThread == threadId;
ThreadDatabase threads = DatabaseFactory.getThreadDatabase(context);
Recipient recipients = DatabaseFactory.getThreadDatabase(context)
.getRecipientForThreadId(threadId);
if (isVisible) {
List<MarkedMessageInfo> messageIds = threads.setRead(threadId, false);
MarkReadReceiver.process(context, messageIds);
}
if (!TextSecurePreferences.isNotificationsEnabled(context) ||
(recipients != null && recipients.isMuted()))
{
return;
}
if (isVisible) {
sendInThreadNotification(context, threads.getRecipientForThreadId(threadId));
} else {
updateNotification(context, threadId, signal, 0);
}
}
private static void updateNotification(@NonNull Context context,
long targetThread,
boolean signal,
int reminderCount)
{
Cursor telcoCursor = null;
Cursor pushCursor = null;
try {
telcoCursor = DatabaseFactory.getMmsSmsDatabase(context).getUnread();
pushCursor = DatabaseFactory.getPushDatabase(context).getPending();
if ((telcoCursor == null || telcoCursor.isAfterLast()) &&
(pushCursor == null || pushCursor.isAfterLast()))
{
cancelActiveNotifications(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();
}
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);
}
}
}
sendMultipleThreadNotification(context, notificationState, signal && (Build.VERSION.SDK_INT < 23));
} else {
sendSingleThreadNotification(context, notificationState, signal, false);
if (isDisplayingSummaryNotification(context)) {
sendMultipleThreadNotification(context, notificationState, false);
}
}
cancelOrphanedNotifications(context, notificationState);
updateBadge(context, notificationState.getMessageCount());
if (signal) {
scheduleReminder(context, reminderCount);
}
} finally {
if (telcoCursor != null) telcoCursor.close();
if (pushCursor != null) pushCursor.close();
}
}
private static void sendSingleThreadNotification(@NonNull Context context,
@NonNull NotificationState notificationState,
boolean signal,
boolean bundled)
{
Log.i(TAG, "sendSingleThreadNotification() signal: " + signal + " bundled: " + bundled);
if (notificationState.getNotifications().isEmpty()) {
if (!bundled) cancelActiveNotifications(context);
Log.i(TAG, "Empty notification state. Skipping.");
return;
}
SingleRecipientNotificationBuilder builder = new SingleRecipientNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context));
List<NotificationItem> notifications = notificationState.getNotifications();
Recipient recipient = notifications.get(0).getRecipient();
int notificationId;
if (Build.VERSION.SDK_INT >= 23) {
notificationId = (int) (SUMMARY_NOTIFICATION_ID + notifications.get(0).getThreadId());
} else {
notificationId = SUMMARY_NOTIFICATION_ID;
}
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(!signal);
builder.setSortKey(String.valueOf(Long.MAX_VALUE - notifications.get(0).getTimestamp()));
long timestamp = notifications.get(0).getTimestamp();
if (timestamp != 0) builder.setWhen(timestamp);
if (!KeyCachingService.isLocked(context) && RecipientUtil.isRecipientMessageRequestAccepted(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);
builder.addAndroidAutoAction(notificationState.getAndroidAutoReplyIntent(context, notifications.get(0).getRecipient()),
notificationState.getAndroidAutoHeardIntent(context, notificationId), notifications.get(0).getTimestamp());
}
ListIterator<NotificationItem> iterator = notifications.listIterator(notifications.size());
while(iterator.hasPrevious()) {
NotificationItem item = iterator.previous();
builder.addMessageBody(item.getRecipient(), item.getIndividualRecipient(), item.getText(), item.getTimestamp());
}
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();
NotificationManagerCompat.from(context).notify(notificationId, notification);
Log.i(TAG, "Posted notification. " + notification.toString());
}
private static void sendMultipleThreadNotification(@NonNull Context context,
@NonNull NotificationState notificationState,
boolean signal)
{
Log.i(TAG, "sendMultiThreadNotification() signal: " + signal);
MultipleRecipientNotificationBuilder builder = new MultipleRecipientNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context));
List<NotificationItem> notifications = notificationState.getNotifications();
builder.setMessageCount(notificationState.getMessageCount(), notificationState.getThreadCount());
builder.setMostRecentSender(notifications.get(0).getIndividualRecipient());
builder.setDeleteIntent(notificationState.getDeleteIntent(context));
builder.setOnlyAlertOnce(!signal);
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);
builder.addActions(notificationState.getMarkAsReadIntent(context, SUMMARY_NOTIFICATION_ID));
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();
NotificationManagerCompat.from(context).notify(SUMMARY_NOTIFICATION_ID, builder.build());
Log.i(TAG, "Posted notification. " + notification.toString());
}
private static void sendInThreadNotification(Context context, Recipient recipient) {
if (!TextSecurePreferences.isInThreadNotifications(context) ||
ServiceUtil.getAudioManager(context).getRingerMode() != AudioManager.RINGER_MODE_NORMAL)
{
return;
}
Uri uri = null;
if (recipient != null) {
uri = NotificationChannels.supported() ? NotificationChannels.getMessageRingtone(context, recipient) : recipient.getMessageRingtone();
}
if (uri == null) {
uri = NotificationChannels.supported() ? NotificationChannels.getMessageRingtone(context) : TextSecurePreferences.getNotificationRingtone(context);
}
if (uri.toString().isEmpty()) {
Log.d(TAG, "ringtone uri is empty");
return;
}
Ringtone ringtone = RingtoneManager.getRingtone(context, uri);
if (ringtone == null) {
Log.w(TAG, "ringtone is null");
return;
}
if (Build.VERSION.SDK_INT >= 21) {
ringtone.setAudioAttributes(new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN)
.setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT)
.build());
} else {
ringtone.setStreamType(AudioManager.STREAM_NOTIFICATION);
}
ringtone.play();
}
private static NotificationState constructNotificationState(@NonNull Context context,
@NonNull Cursor cursor)
{
NotificationState notificationState = new NotificationState();
MmsSmsDatabase.Reader reader = DatabaseFactory.getMmsSmsDatabase(context).readerFor(cursor);
MessageRecord record;
while ((record = reader.getNext()) != null) {
long id = record.getId();
boolean mms = record.isMms() || record.isMmsNotification();
Recipient recipient = record.getIndividualRecipient().resolve();
Recipient conversationRecipient = record.getRecipient().resolve();
long threadId = record.getThreadId();
CharSequence body = record.getDisplayBody(context);
Recipient threadRecipients = null;
SlideDeck slideDeck = null;
long timestamp = record.getTimestamp();
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));
if (threadId != -1) {
threadRecipients = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId);
}
if (isUnreadMessage) {
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).getSlideDeck().getStickerSlide() != null) {
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_sticker));
slideDeck = ((MmsMessageRecord) record).getSlideDeck();
} else if (record.isMms() && ((MmsMessageRecord) record).isViewOnce()) {
body = SpanUtil.italic(context.getString(getViewOnceDescription((MmsMessageRecord) record)));
} else if (record.isMms() && TextUtils.isEmpty(body) && !((MmsMessageRecord) record).getSlideDeck().getSlides().isEmpty()) {
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_media_message));
slideDeck = ((MediaMmsMessageRecord) record).getSlideDeck();
} else if (record.isMms() && !record.isMmsNotification() && !((MmsMessageRecord) record).getSlideDeck().getSlides().isEmpty()) {
String message = context.getString(R.string.MessageNotifier_media_message_with_text, body);
int italicLength = message.length() - body.length();
body = SpanUtil.italic(message, italicLength);
slideDeck = ((MediaMmsMessageRecord) record).getSlideDeck();
}
if (threadRecipients == null || !threadRecipients.isMuted()) {
notificationState.addNotification(new NotificationItem(id, mms, recipient, conversationRecipient, threadRecipients, threadId, body, timestamp, slideDeck));
}
}
if (hasUnreadReactions) {
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(context.getString(R.string.MessageNotifier_reacted_to_your_message, EMOJI_REPLACEMENT_STRING)).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(), null));
}
}
}
}
reader.close();
return notificationState;
}
private static @StringRes int getViewOnceDescription(@NonNull MmsMessageRecord messageRecord) {
final String contentType = getMessageContentType(messageRecord);
if (MediaUtil.isImageType(contentType)) {
return R.string.MessageNotifier_disappearing_photo;
}
return R.string.MessageNotifier_disappearing_video;
}
private static String getMessageContentType(@NonNull MmsMessageRecord messageRecord) {
Slide thumbnailSlide = messageRecord.getSlideDeck().getThumbnailSlide();
if (thumbnailSlide == null) {
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("MessageNotifier", t);
}
}
private static void scheduleReminder(Context context, int count) {
if (count >= TextSecurePreferences.getRepeatAlertsCount(context)) {
return;
}
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Intent alarmIntent = new Intent(ReminderReceiver.REMINDER_ACTION);
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);
}
public static void clearReminder(Context context) {
Intent alarmIntent = new Intent(ReminderReceiver.REMINDER_ACTION);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, PendingIntent.FLAG_CANCEL_CURRENT);
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
alarmManager.cancel(pendingIntent);
}
public static class ReminderReceiver extends BroadcastReceiver {
public static final String REMINDER_ACTION = "org.thoughtcrime.securesms.MessageNotifier.REMINDER_ACTION";
@SuppressLint("StaticFieldLeak")
@Override
public void onReceive(final Context context, final Intent intent) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
int reminderCount = intent.getIntExtra("reminder_count", 0);
MessageNotifier.updateNotification(context, -1, true, reminderCount + 1);
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
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...");
MessageNotifier.updateNotification(context, threadId, true);
MessageNotifier.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 = new Runnable() {
@Override
public void run() {
runnable.run();
synchronized (tasks) {
tasks.remove(runnable);
}
}
};
executor.execute(wrapper);
}
public void cancel() {
synchronized (tasks) {
for (DelayedNotification task : tasks) {
task.cancel();
}
}
}
}
}

View File

@@ -0,0 +1,93 @@
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.textsecure_primary));
setSmallIcon(R.drawable.icon_notification);
setContentTitle(context.getString(R.string.app_name));
// TODO [greyson] Navigation
setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, MainActivity.class), 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.toShortString(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.toShortString(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

@@ -0,0 +1,593 @@
package org.thoughtcrime.securesms.notifications;
import android.annotation.TargetApi;
import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.media.AudioAttributes;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.provider.Settings;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import android.text.TextUtils;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.logging.Log;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class NotificationChannels {
private static final String TAG = NotificationChannels.class.getSimpleName();
private static class Version {
static final int MESSAGES_CATEGORY = 2;
static final int CALLS_PRIORITY_BUMP = 3;
}
private static final int VERSION = 3;
private static final String CATEGORY_MESSAGES = "messages";
private static final String CONTACT_PREFIX = "contact_";
private static final String MESSAGES_PREFIX = "messages_";
public static final String CALLS = "calls_v3";
public static final String FAILURES = "failures";
public static final String APP_UPDATES = "app_updates";
public static final String BACKUPS = "backups_v2";
public static final String LOCKED_STATUS = "locked_status_v2";
public static final String OTHER = "other_v2";
/**
* Ensures all of the notification channels are created. No harm in repeat calls. Call is safely
* ignored for API < 26.
*/
public static synchronized void create(@NonNull Context context) {
if (!supported()) {
return;
}
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
int oldVersion = TextSecurePreferences.getNotificationChannelVersion(context);
if (oldVersion != VERSION) {
onUpgrade(notificationManager, oldVersion, VERSION);
TextSecurePreferences.setNotificationChannelVersion(context, VERSION);
}
onCreate(context, notificationManager);
AsyncTask.SERIAL_EXECUTOR.execute(() -> {
ensureCustomChannelConsistency(context);
});
}
/**
* Recreates all notification channels for contacts with custom notifications enabled. Should be
* safe to call repeatedly. Needs to be executed on a background thread.
*/
@WorkerThread
public static synchronized void restoreContactNotificationChannels(@NonNull Context context) {
if (!NotificationChannels.supported()) {
return;
}
RecipientDatabase db = DatabaseFactory.getRecipientDatabase(context);
try (RecipientDatabase.RecipientReader reader = db.getRecipientsWithNotificationChannels()) {
Recipient recipient;
while ((recipient = reader.getNext()) != null) {
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
if (!channelExists(notificationManager.getNotificationChannel(recipient.getNotificationChannel()))) {
String id = createChannelFor(context, recipient);
db.setNotificationChannel(recipient.getId(), id);
}
}
}
ensureCustomChannelConsistency(context);
}
/**
* @return The channel ID for the default messages channel.
*/
public static synchronized @NonNull String getMessagesChannel(@NonNull Context context) {
return getMessagesChannelId(TextSecurePreferences.getNotificationMessagesChannelVersion(context));
}
/**
* @return Whether or not notification channels are supported.
*/
public static boolean supported() {
return Build.VERSION.SDK_INT >= 26;
}
/**
* @return A name suitable to be displayed as the notification channel title.
*/
public static @NonNull String getChannelDisplayNameFor(@NonNull Context context,
@Nullable String systemName,
@Nullable String profileName,
@Nullable String username,
@NonNull String address)
{
if (!TextUtils.isEmpty(systemName)) {
return systemName;
} else if (!TextUtils.isEmpty(profileName)) {
return profileName;
} else if (!TextUtils.isEmpty(username)) {
return username;
} else if (!TextUtils.isEmpty(address)) {
return address;
} else {
return context.getString(R.string.NotificationChannel_missing_display_name);
}
}
/**
* Creates a channel for the specified recipient.
* @return The channel ID for the newly-created channel.
*/
public static synchronized @Nullable String createChannelFor(@NonNull Context context, @NonNull Recipient recipient) {
if (recipient.getId().isUnknown()) return null;
VibrateState vibrateState = recipient.getMessageVibrate();
boolean vibrationEnabled = vibrateState == VibrateState.DEFAULT ? TextSecurePreferences.isNotificationVibrateEnabled(context) : vibrateState == VibrateState.ENABLED;
Uri messageRingtone = recipient.getMessageRingtone() != null ? recipient.getMessageRingtone() : getMessageRingtone(context);
String displayName = recipient.getDisplayName(context);
return createChannelFor(context, generateChannelIdFor(recipient), displayName, messageRingtone, vibrationEnabled);
}
/**
* More verbose version of {@link #createChannelFor(Context, Recipient)}.
*/
public static synchronized @Nullable String createChannelFor(@NonNull Context context,
@NonNull String channelId,
@NonNull String displayName,
@Nullable Uri messageSound,
boolean vibrationEnabled)
{
if (!supported()) {
return null;
}
NotificationChannel channel = new NotificationChannel(channelId, displayName, NotificationManager.IMPORTANCE_HIGH);
setLedPreference(channel, TextSecurePreferences.getNotificationLedColor(context));
channel.setGroup(CATEGORY_MESSAGES);
channel.enableVibration(vibrationEnabled);
if (messageSound != null) {
channel.setSound(messageSound, new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN)
.setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT)
.build());
}
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
notificationManager.createNotificationChannel(channel);
return channelId;
}
/**
* Deletes the channel generated for the provided recipient. Safe to call even if there was never
* a channel made for that recipient.
*/
public static synchronized void deleteChannelFor(@NonNull Context context, @NonNull Recipient recipient) {
if (!supported()) {
return;
}
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
String channel = recipient.getNotificationChannel();
if (channel != null) {
Log.i(TAG, "Deleting channel");
notificationManager.deleteNotificationChannel(channel);
}
}
/**
* Navigates the user to the system settings for the desired notification channel.
*/
public static void openChannelSettings(@NonNull Context context, @NonNull String channelId) {
if (!supported()) {
return;
}
Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
intent.putExtra(Settings.EXTRA_CHANNEL_ID, channelId);
intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName());
context.startActivity(intent);
}
/**
* Updates the LED color for message notifications and all contact-specific message notification
* channels. Performs database operations and should therefore be invoked on a background thread.
*/
@WorkerThread
public static synchronized void updateMessagesLedColor(@NonNull Context context, @NonNull String color) {
if (!supported()) {
return;
}
Log.i(TAG, "Updating LED color.");
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
updateMessageChannel(context, channel -> setLedPreference(channel, color));
updateAllRecipientChannelLedColors(context, notificationManager, color);
ensureCustomChannelConsistency(context);
}
/**
* @return The message ringtone set for the default message channel.
*/
public static synchronized @NonNull Uri getMessageRingtone(@NonNull Context context) {
if (!supported()) {
return Uri.EMPTY;
}
Uri sound = ServiceUtil.getNotificationManager(context).getNotificationChannel(getMessagesChannel(context)).getSound();
return sound == null ? Uri.EMPTY : sound;
}
public static synchronized @Nullable Uri getMessageRingtone(@NonNull Context context, @NonNull Recipient recipient) {
if (!supported() || recipient.resolve().getNotificationChannel() == null) {
return null;
}
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
NotificationChannel channel = notificationManager.getNotificationChannel(recipient.getNotificationChannel());
if (!channelExists(channel)) {
Log.w(TAG, "Recipient had no channel. Returning null.");
return null;
}
return channel.getSound();
}
/**
* Update the message ringtone for the default message channel.
*/
public static synchronized void updateMessageRingtone(@NonNull Context context, @Nullable Uri uri) {
if (!supported()) {
return;
}
Log.i(TAG, "Updating default message ringtone with URI: " + String.valueOf(uri));
updateMessageChannel(context, channel -> {
channel.setSound(uri == null ? Settings.System.DEFAULT_NOTIFICATION_URI : uri, getRingtoneAudioAttributes());
});
}
/**
* Updates the message ringtone for a specific recipient. If that recipient has no channel, this
* does nothing.
*
* This has to update the database, and therefore should be run on a background thread.
*/
@WorkerThread
public static synchronized void updateMessageRingtone(@NonNull Context context, @NonNull Recipient recipient, @Nullable Uri uri) {
if (!supported() || recipient.getNotificationChannel() == null) {
return;
}
Log.i(TAG, "Updating recipient message ringtone with URI: " + String.valueOf(uri));
String newChannelId = generateChannelIdFor(recipient);
boolean success = updateExistingChannel(ServiceUtil.getNotificationManager(context),
recipient.getNotificationChannel(),
generateChannelIdFor(recipient),
channel -> channel.setSound(uri == null ? Settings.System.DEFAULT_NOTIFICATION_URI : uri, getRingtoneAudioAttributes()));
DatabaseFactory.getRecipientDatabase(context).setNotificationChannel(recipient.getId(), success ? newChannelId : null);
ensureCustomChannelConsistency(context);
}
/**
* @return The vibrate settings for the default message channel.
*/
public static synchronized boolean getMessageVibrate(@NonNull Context context) {
if (!supported()) {
return false;
}
return ServiceUtil.getNotificationManager(context).getNotificationChannel(getMessagesChannel(context)).shouldVibrate();
}
/**
* @return The vibrate setting for a specific recipient. If that recipient has no channel, this
* will return the setting for the default message channel.
*/
public static synchronized boolean getMessageVibrate(@NonNull Context context, @NonNull Recipient recipient) {
if (!supported()) {
return getMessageVibrate(context);
}
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
NotificationChannel channel = notificationManager.getNotificationChannel(recipient.getNotificationChannel());
if (!channelExists(channel)) {
Log.w(TAG, "Recipient didn't have a channel. Returning message default.");
return getMessageVibrate(context);
}
return channel.shouldVibrate();
}
/**
* Sets the vibrate property for the default message channel.
*/
public static synchronized void updateMessageVibrate(@NonNull Context context, boolean enabled) {
if (!supported()) {
return;
}
Log.i(TAG, "Updating default vibrate with value: " + enabled);
updateMessageChannel(context, channel -> channel.enableVibration(enabled));
}
/**
* Updates the message ringtone for a specific recipient. If that recipient has no channel, this
* does nothing.
*
* This has to update the database and should therefore be run on a background thread.
*/
@WorkerThread
public static synchronized void updateMessageVibrate(@NonNull Context context, @NonNull Recipient recipient, VibrateState vibrateState) {
if (!supported() || recipient.getNotificationChannel() == null) {
return ;
}
Log.i(TAG, "Updating recipient vibrate with value: " + vibrateState);
boolean enabled = vibrateState == VibrateState.DEFAULT ? getMessageVibrate(context) : vibrateState == VibrateState.ENABLED;
String newChannelId = generateChannelIdFor(recipient);
boolean success = updateExistingChannel(ServiceUtil.getNotificationManager(context),
recipient.getNotificationChannel(),
newChannelId,
channel -> channel.enableVibration(enabled));
DatabaseFactory.getRecipientDatabase(context).setNotificationChannel(recipient.getId(), success ? newChannelId : null);
ensureCustomChannelConsistency(context);
}
/**
* Updates the name of an existing channel to match the recipient's current name. Will have no
* effect if the recipient doesn't have an existing valid channel.
*/
public static synchronized void updateContactChannelName(@NonNull Context context, @NonNull Recipient recipient) {
if (!supported() || recipient.getNotificationChannel() == null) {
return;
}
Log.i(TAG, "Updating contact channel name");
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
if (notificationManager.getNotificationChannel(recipient.getNotificationChannel()) == null) {
Log.w(TAG, "Tried to update the name of a channel, but that channel doesn't exist.");
return;
}
NotificationChannel channel = new NotificationChannel(recipient.getNotificationChannel(),
recipient.getDisplayName(context),
NotificationManager.IMPORTANCE_HIGH);
channel.setGroup(CATEGORY_MESSAGES);
notificationManager.createNotificationChannel(channel);
}
@TargetApi(26)
@WorkerThread
public static synchronized void ensureCustomChannelConsistency(@NonNull Context context) {
Log.d(TAG, "ensureCustomChannelConsistency()");
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
RecipientDatabase db = DatabaseFactory.getRecipientDatabase(context);
List<Recipient> customRecipients = new ArrayList<>();
Set<String> customChannelIds = new HashSet<>();
try (RecipientDatabase.RecipientReader reader = db.getRecipientsWithNotificationChannels()) {
Recipient recipient;
while ((recipient = reader.getNext()) != null) {
customRecipients.add(recipient);
customChannelIds.add(recipient.getNotificationChannel());
}
}
Set<String> existingChannelIds = Stream.of(notificationManager.getNotificationChannels()).map(NotificationChannel::getId).collect(Collectors.toSet());
for (NotificationChannel existingChannel : notificationManager.getNotificationChannels()) {
if (existingChannel.getId().startsWith(CONTACT_PREFIX) && !customChannelIds.contains(existingChannel.getId())) {
Log.i(TAG, "Consistency: Deleting channel '"+ existingChannel.getId() + "' because the DB has no record of it.");
notificationManager.deleteNotificationChannel(existingChannel.getId());
} else if (existingChannel.getId().startsWith(MESSAGES_PREFIX) && !existingChannel.getId().equals(getMessagesChannel(context))) {
Log.i(TAG, "Consistency: Deleting channel '"+ existingChannel.getId() + "' because it's out of date.");
notificationManager.deleteNotificationChannel(existingChannel.getId());
}
}
for (Recipient customRecipient : customRecipients) {
if (!existingChannelIds.contains(customRecipient.getNotificationChannel())) {
Log.i(TAG, "Consistency: Removing custom channel '"+ customRecipient.getNotificationChannel() + "' because the system doesn't have it.");
db.setNotificationChannel(customRecipient.getId(), null);
}
}
}
@TargetApi(26)
private static void onCreate(@NonNull Context context, @NonNull NotificationManager notificationManager) {
NotificationChannelGroup messagesGroup = new NotificationChannelGroup(CATEGORY_MESSAGES, context.getResources().getString(R.string.NotificationChannel_group_messages));
notificationManager.createNotificationChannelGroup(messagesGroup);
NotificationChannel messages = new NotificationChannel(getMessagesChannel(context), context.getString(R.string.NotificationChannel_messages), NotificationManager.IMPORTANCE_HIGH);
NotificationChannel calls = new NotificationChannel(CALLS, context.getString(R.string.NotificationChannel_calls), NotificationManager.IMPORTANCE_HIGH);
NotificationChannel failures = new NotificationChannel(FAILURES, context.getString(R.string.NotificationChannel_failures), NotificationManager.IMPORTANCE_HIGH);
NotificationChannel backups = new NotificationChannel(BACKUPS, context.getString(R.string.NotificationChannel_backups), NotificationManager.IMPORTANCE_LOW);
NotificationChannel lockedStatus = new NotificationChannel(LOCKED_STATUS, context.getString(R.string.NotificationChannel_locked_status), NotificationManager.IMPORTANCE_LOW);
NotificationChannel other = new NotificationChannel(OTHER, context.getString(R.string.NotificationChannel_other), NotificationManager.IMPORTANCE_LOW);
messages.setGroup(CATEGORY_MESSAGES);
messages.enableVibration(TextSecurePreferences.isNotificationVibrateEnabled(context));
messages.setSound(TextSecurePreferences.getNotificationRingtone(context), getRingtoneAudioAttributes());
setLedPreference(messages, TextSecurePreferences.getNotificationLedColor(context));
calls.setShowBadge(false);
backups.setShowBadge(false);
lockedStatus.setShowBadge(false);
other.setShowBadge(false);
notificationManager.createNotificationChannels(Arrays.asList(messages, calls, failures, backups, lockedStatus, other));
if (BuildConfig.PLAY_STORE_DISABLED) {
NotificationChannel appUpdates = new NotificationChannel(APP_UPDATES, context.getString(R.string.NotificationChannel_app_updates), NotificationManager.IMPORTANCE_HIGH);
notificationManager.createNotificationChannel(appUpdates);
} else {
notificationManager.deleteNotificationChannel(APP_UPDATES);
}
}
@TargetApi(26)
private static void onUpgrade(@NonNull NotificationManager notificationManager, int oldVersion, int newVersion) {
Log.i(TAG, "Upgrading channels from " + oldVersion + " to " + newVersion);
if (oldVersion < Version.MESSAGES_CATEGORY) {
notificationManager.deleteNotificationChannel("messages");
notificationManager.deleteNotificationChannel("calls");
notificationManager.deleteNotificationChannel("locked_status");
notificationManager.deleteNotificationChannel("backups");
notificationManager.deleteNotificationChannel("other");
}
if (oldVersion < Version.CALLS_PRIORITY_BUMP) {
notificationManager.deleteNotificationChannel("calls_v2");
}
}
@TargetApi(26)
private static void setLedPreference(@NonNull NotificationChannel channel, @NonNull String ledColor) {
if ("none".equals(ledColor)) {
channel.enableLights(false);
} else {
channel.enableLights(true);
channel.setLightColor(Color.parseColor(ledColor));
}
}
private static @NonNull String generateChannelIdFor(@NonNull Recipient recipient) {
return CONTACT_PREFIX + recipient.getId().serialize() + "_" + System.currentTimeMillis();
}
@TargetApi(26)
private static @NonNull NotificationChannel copyChannel(@NonNull NotificationChannel original, @NonNull String id) {
NotificationChannel copy = new NotificationChannel(id, original.getName(), original.getImportance());
copy.setGroup(original.getGroup());
copy.setSound(original.getSound(), original.getAudioAttributes());
copy.setBypassDnd(original.canBypassDnd());
copy.enableVibration(original.shouldVibrate());
copy.setVibrationPattern(original.getVibrationPattern());
copy.setLockscreenVisibility(original.getLockscreenVisibility());
copy.setShowBadge(original.canShowBadge());
copy.setLightColor(original.getLightColor());
copy.enableLights(original.shouldShowLights());
return copy;
}
private static String getMessagesChannelId(int version) {
return MESSAGES_PREFIX + version;
}
@WorkerThread
@TargetApi(26)
private static void updateAllRecipientChannelLedColors(@NonNull Context context, @NonNull NotificationManager notificationManager, @NonNull String color) {
RecipientDatabase database = DatabaseFactory.getRecipientDatabase(context);
try (RecipientDatabase.RecipientReader recipients = database.getRecipientsWithNotificationChannels()) {
Recipient recipient;
while ((recipient = recipients.getNext()) != null) {
assert recipient.getNotificationChannel() != null;
String newChannelId = generateChannelIdFor(recipient);
boolean success = updateExistingChannel(notificationManager, recipient.getNotificationChannel(), newChannelId, channel -> setLedPreference(channel, color));
database.setNotificationChannel(recipient.getId(), success ? newChannelId : null);
}
}
ensureCustomChannelConsistency(context);
}
@TargetApi(26)
private static void updateMessageChannel(@NonNull Context context, @NonNull ChannelUpdater updater) {
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
int existingVersion = TextSecurePreferences.getNotificationMessagesChannelVersion(context);
int newVersion = existingVersion + 1;
Log.i(TAG, "Updating message channel from version " + existingVersion + " to " + newVersion);
if (updateExistingChannel(notificationManager, getMessagesChannelId(existingVersion), getMessagesChannelId(newVersion), updater)) {
TextSecurePreferences.setNotificationMessagesChannelVersion(context, newVersion);
} else {
onCreate(context, notificationManager);
}
}
@TargetApi(26)
private static boolean updateExistingChannel(@NonNull NotificationManager notificationManager,
@NonNull String channelId,
@NonNull String newChannelId,
@NonNull ChannelUpdater updater)
{
NotificationChannel existingChannel = notificationManager.getNotificationChannel(channelId);
if (existingChannel == null) {
Log.w(TAG, "Tried to update a channel, but it didn't exist.");
return false;
}
notificationManager.deleteNotificationChannel(existingChannel.getId());
NotificationChannel newChannel = copyChannel(existingChannel, newChannelId);
updater.update(newChannel);
notificationManager.createNotificationChannel(newChannel);
return true;
}
@TargetApi(21)
private static AudioAttributes getRingtoneAudioAttributes() {
return new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN)
.setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT)
.build();
}
@TargetApi(26)
private static boolean channelExists(@Nullable NotificationChannel channel) {
return channel != null && !NotificationChannel.DEFAULT_CHANNEL_ID.equals(channel.getId());
}
private interface ChannelUpdater {
@TargetApi(26)
void update(@NonNull NotificationChannel channel);
}
}

View File

@@ -0,0 +1,89 @@
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.ConversationActivity;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.recipients.Recipient;
public class NotificationItem {
private final long id;
private final boolean mms;
private final @NonNull Recipient conversationRecipient;
private final @NonNull Recipient individualRecipient;
private final @Nullable Recipient threadRecipient;
private final long threadId;
private final @Nullable CharSequence text;
private final long timestamp;
private final @Nullable SlideDeck slideDeck;
public NotificationItem(long id, boolean mms,
@NonNull Recipient individualRecipient,
@NonNull Recipient conversationRecipient,
@Nullable Recipient threadRecipient,
long threadId, @Nullable CharSequence text, long timestamp,
@Nullable SlideDeck slideDeck)
{
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.slideDeck = slideDeck;
}
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) {
Intent intent = new Intent(context, ConversationActivity.class);
Recipient notifyRecipients = threadRecipient != null ? threadRecipient : conversationRecipient;
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, notifyRecipients.getId());
intent.putExtra("thread_id", threadId);
intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
return TaskStackBuilder.create(context)
.addNextIntentWithParentStack(intent)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
}
public long getId() {
return id;
}
public boolean isMms() {
return mms;
}
}

View File

@@ -0,0 +1,199 @@
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.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.conversation.ConversationPopupActivity;
import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState;
import org.thoughtcrime.securesms.logging.Log;
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 = NotificationState.class.getSimpleName();
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 getMarkAsReadIntent(Context context, int notificationId) {
long[] threadArray = new long[threads.size()];
int index = 0;
for (long thread : threads) {
Log.i(TAG, "Added thread: " + thread);
threadArray[index++] = thread;
}
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 = new Intent(context, ConversationPopupActivity.class);
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId());
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, (long)threads.toArray()[0]);
intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
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);
}
}

View File

@@ -0,0 +1,41 @@
package org.thoughtcrime.securesms.notifications;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import androidx.core.app.NotificationCompat;
import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
public class PendingMessageNotificationBuilder extends AbstractNotificationBuilder {
public PendingMessageNotificationBuilder(Context context, NotificationPrivacyPreference privacy) {
super(context, privacy);
// TODO [greyson] Navigation
Intent intent = new Intent(context, MainActivity.class);
setSmallIcon(R.drawable.icon_notification);
setColor(context.getResources().getColor(R.color.textsecure_primary));
setCategory(NotificationCompat.CATEGORY_MESSAGE);
setContentTitle(context.getString(R.string.MessageNotifier_you_may_have_new_messages));
setContentText(context.getString(R.string.MessageNotifier_open_signal_to_check_for_recent_notifications));
setTicker(context.getString(R.string.MessageNotifier_open_signal_to_check_for_recent_notifications));
setContentIntent(PendingIntent.getActivity(context, 0, intent, 0));
setAutoCancel(true);
setAlarms(null, RecipientDatabase.VibrateState.DEFAULT);
setOnlyAlertOnce(true);
if (!NotificationChannels.supported()) {
setPriority(TextSecurePreferences.getNotificationPriority(context));
}
}
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright (C) 2016 Open 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.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/**
* Get the response text from the Wearable Device and sends an message as a reply
*/
public class RemoteReplyReceiver extends BroadcastReceiver {
public static final String TAG = RemoteReplyReceiver.class.getSimpleName();
public static final String REPLY_ACTION = "org.thoughtcrime.securesms.notifications.WEAR_REPLY";
public static final String RECIPIENT_EXTRA = "recipient_extra";
public static final String REPLY_METHOD = "reply_method";
@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 RecipientId recipientId = intent.getParcelableExtra(RECIPIENT_EXTRA);
final ReplyMethod replyMethod = (ReplyMethod) intent.getSerializableExtra(REPLY_METHOD);
final CharSequence responseText = remoteInput.getCharSequence(MessageNotifier.EXTRA_REMOTE_REPLY);
if (recipientId == null) throw new AssertionError("No recipientId specified");
if (replyMethod == null) throw new AssertionError("No reply method specified");
if (responseText != null) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
long threadId;
Recipient recipient = Recipient.resolved(recipientId);
int subscriptionId = recipient.getDefaultSubscriptionId().or(-1);
long expiresIn = recipient.getExpireMessages() * 1000L;
switch (replyMethod) {
case GroupMessage: {
OutgoingMediaMessage reply = new OutgoingMediaMessage(recipient, responseText.toString(), new LinkedList<>(), System.currentTimeMillis(), subscriptionId, expiresIn, false, 0, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
threadId = MessageSender.send(context, reply, -1, false, null);
break;
}
case SecureMessage: {
OutgoingEncryptedMessage reply = new OutgoingEncryptedMessage(recipient, responseText.toString(), expiresIn);
threadId = MessageSender.send(context, reply, -1, false, null);
break;
}
case UnsecuredSmsMessage: {
OutgoingTextMessage reply = new OutgoingTextMessage(recipient, responseText.toString(), expiresIn, subscriptionId);
threadId = MessageSender.send(context, reply, -1, true, null);
break;
}
default:
throw new AssertionError("Unknown Reply method");
}
List<MarkedMessageInfo> messageIds = DatabaseFactory.getThreadDatabase(context).setRead(threadId, true);
MessageNotifier.updateNotification(context);
MarkReadReceiver.process(context, messageIds);
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
}

View File

@@ -0,0 +1,25 @@
package org.thoughtcrime.securesms.notifications;
import android.content.Context;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
public enum ReplyMethod {
GroupMessage,
SecureMessage,
UnsecuredSmsMessage;
public static @NonNull ReplyMethod forRecipient(Context context, Recipient recipient) {
if (recipient.isGroup()) {
return ReplyMethod.GroupMessage;
} else if (TextSecurePreferences.isPushRegistered(context) && recipient.getRegistered() == RecipientDatabase.RegisteredState.REGISTERED && !recipient.isForceSmsSelection()) {
return ReplyMethod.SecureMessage;
} else {
return ReplyMethod.UnsecuredSmsMessage;
}
}
}

View File

@@ -0,0 +1,387 @@
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 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 android.text.SpannableStringBuilder;
import com.annimon.stream.Stream;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
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.logging.Log;
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.BitmapUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutionException;
public class SingleRecipientNotificationBuilder extends AbstractNotificationBuilder {
private static final String TAG = SingleRecipientNotificationBuilder.class.getSimpleName();
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;
public SingleRecipientNotificationBuilder(@NonNull Context context, @NonNull NotificationPrivacyPreference privacy)
{
super(new ContextThemeWrapper(context, R.style.TextSecure_LightTheme), privacy);
setSmallIcon(R.drawable.icon_notification);
setColor(context.getResources().getColor(R.color.textsecure_primary));
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.toShortString(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, ContactColors.UNKNOWN_COLOR.toConversationColor(context)));
}
}
private Drawable getContactDrawable(@NonNull Recipient recipient) {
ContactPhoto contactPhoto = recipient.getContactPhoto();
FallbackContactPhoto fallbackContactPhoto = recipient.getFallbackContactPhoto();
if (contactPhoto != null) {
try {
return GlideApp.with(context.getApplicationContext())
.load(contactPhoto)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.circleCrop()
.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.getColor().toConversationColor(context));
}
} else {
return fallbackContactPhoto.asDrawable(context, recipient.getColor().toConversationColor(context));
}
}
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.toShortString(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 addActions(@NonNull PendingIntent markReadIntent,
@NonNull PendingIntent quickReplyIntent,
@NonNull PendingIntent wearableReplyIntent,
@NonNull ReplyMethod replyMethod)
{
Action markAsReadAction = new Action(R.drawable.check,
context.getString(R.string.MessageNotifier_mark_read),
markReadIntent);
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(MessageNotifier.EXTRA_REMOTE_REPLY)
.setLabel(label).build())
.build();
}
Action wearableReplyAction = new Action.Builder(R.drawable.ic_reply,
actionName,
wearableReplyIntent)
.addRemoteInput(new RemoteInput.Builder(MessageNotifier.EXTRA_REMOTE_REPLY)
.setLabel(label).build())
.build();
addAction(markAsReadAction);
addAction(replyAction);
extend(new NotificationCompat.WearableExtender().addAction(markAsReadAction)
.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)
{
SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
Person.Builder personBuilder = new Person.Builder()
.setKey(individualRecipient.getId().serialize())
.setBot(false);
this.threadRecipient = threadRecipient;
if (privacy.isDisplayContact()) {
personBuilder.setName(individualRecipient.getDisplayName(context));
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));
}
messages.add(new NotificationCompat.MessagingStyle.Message(text, timestamp, personBuilder.build()));
}
@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()) {
setStyle(new NotificationCompat.BigPictureStyle()
.bigPicture(getNotificationPicture(bigPictureUri.get(), BIG_PICTURE_DIMEN))
.setSummaryText(getBigText()));
} else {
if (Build.VERSION.SDK_INT >= 24) {
applyMessageStyle();
} else {
applyLegacy();
}
}
}
return super.build();
}
private void applyMessageStyle() {
NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(
new Person.Builder()
.setBot(false)
.setName(Recipient.self().getDisplayName(context))
.setKey(Recipient.self().getId().serialize())
.build());
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 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.getThumbnailUri() != null) {
return Optional.of(slide.getThumbnailUri());
} 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()));
}
}