Merge pull request #1368 from deltachat/rework-notifications

Rework notifications
This commit is contained in:
bjoern
2020-05-25 14:44:43 +02:00
committed by GitHub
23 changed files with 612 additions and 1598 deletions
-12
View File
@@ -117,18 +117,6 @@
<item>none</item>
</string-array>
<string-array name="pref_led_blink_pattern_entries">
<item>@string/fast</item>
<item>@string/normal</item>
<item>@string/slow</item>
</string-array>
<string-array name="pref_led_blink_pattern_values" translatable="false">
<item>300,300</item>
<item>500,2000</item>
<item>3000,3000</item>
</string-array>
<string-array name="default_or_custom_values" translatable="false">
<item>default</item>
<item>custom</item>
+6 -14
View File
@@ -35,20 +35,6 @@
android:entries="@array/pref_led_color_entries"
android:entryValues="@array/pref_led_color_values" />
<ListPreference
android:key="pref_led_blink"
android:defaultValue="500,2000"
android:title="@string/pref_led_pattern"
android:dependency="pref_led_color"
android:entries="@array/pref_led_blink_pattern_entries"
android:entryValues="@array/pref_led_blink_pattern_values" />
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
android:key="pref_key_inthread_notifications"
android:title="@string/pref_in_chat_sounds"
android:dependency="pref_key_enable_notifications"
android:defaultValue="true" />
<ListPreference
android:key="pref_notification_privacy"
android:title="@string/pref_notifications_show"
@@ -65,4 +51,10 @@
android:entries="@array/pref_notification_priority_entries"
android:entryValues="@array/pref_notification_priority_values"/>
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
android:key="pref_key_inthread_notifications"
android:dependency="pref_key_enable_notifications"
android:title="@string/pref_in_chat_sounds"
android:defaultValue="true" />
</PreferenceScreen>
@@ -27,7 +27,7 @@ import org.thoughtcrime.securesms.connect.NetworkStateReceiver;
import org.thoughtcrime.securesms.crypto.PRNGFixes;
import org.thoughtcrime.securesms.geolocation.DcLocationManager;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.notifications.MessageNotifierCompat;
import org.thoughtcrime.securesms.notifications.InChatSounds;
import org.thoughtcrime.securesms.util.AndroidSignalProtocolLogger;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.ScreenLockUtil;
@@ -72,7 +72,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
initializeLogging();
initializeJobManager();
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
MessageNotifierCompat.init(this);
InChatSounds.getInstance(this);
dcLocationManager = new DcLocationManager(this);
try {
@@ -93,7 +93,6 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.notifications.MessageNotifierCompat;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -302,14 +301,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
titleView.setTitle(glideRequests, dcChat);
MessageNotifierCompat.updateVisibleChat(chatId);
dcContext.notificationCenter.updateVisibleChat(chatId);
}
@Override
protected void onPause() {
super.onPause();
processComposeControls(ACTION_SAVE_DRAFT);
MessageNotifierCompat.updateVisibleChat(MessageNotifierCompat.NO_VISIBLE_CHAT_ID);
dcContext.notificationCenter.updateVisibleChat(0);
if (isFinishing()) overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_right);
quickAttachmentDrawer.onPause();
inputPanel.onPause();
@@ -1224,7 +1223,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (refreshFragment) {
fragment.reload(recipient, chatId);
MessageNotifierCompat.updateVisibleChat(chatId);
dcContext.notificationCenter.updateVisibleChat(chatId);
}
fragment.scrollToBottom();
@@ -1443,7 +1442,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
else {
processComposeControls(ACTION_SEND_OUT);
MessageNotifierCompat.playSendSound();
dcContext.notificationCenter.maybePlaySendSound(dcChat);
}
}
@@ -62,7 +62,6 @@ import org.thoughtcrime.securesms.connect.ApplicationDcContext;
import org.thoughtcrime.securesms.connect.DcChatlistLoader;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.notifications.MessageNotifierCompat;
import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.guava.Optional;
@@ -294,9 +293,9 @@ public class ConversationListFragment extends Fragment
@SuppressLint("StaticFieldLeak")
private void handleDeleteAllSelected() {
final DcContext dcContext = DcHelper.getContext(getActivity());
int conversationsCount = getListAdapter().getBatchSelections().size();
AlertDialog.Builder alert = new AlertDialog.Builder(getActivity());
final ApplicationDcContext dcContext = DcHelper.getContext(getActivity());
int conversationsCount = getListAdapter().getBatchSelections().size();
AlertDialog.Builder alert = new AlertDialog.Builder(getActivity());
alert.setMessage(getActivity().getResources().getQuantityString(R.plurals.ask_delete_chat,
conversationsCount, conversationsCount));
alert.setCancelable(true);
@@ -324,7 +323,7 @@ public class ConversationListFragment extends Fragment
dcContext.marknoticedContact(getListAdapter().getDeaddropContactId());
}
else {
MessageNotifierCompat.removeNotifications((int) chatId);
dcContext.notificationCenter.removeNotifications((int) chatId);
dcContext.deleteChat((int) chatId);
}
}
@@ -30,7 +30,7 @@ import com.b44t.messenger.DcMsg;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.notifications.MessageNotifierCompat;
import org.thoughtcrime.securesms.notifications.NotificationCenter;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.Util;
@@ -53,6 +53,7 @@ public class ApplicationDcContext extends DcContext {
public static final int RECIPIENT_TYPE_CONTACT = 1;
public Context context;
public NotificationCenter notificationCenter;
public ApplicationDcContext(Context context) {
super("Android "+BuildConfig.VERSION_NAME);
@@ -121,6 +122,7 @@ public class ApplicationDcContext extends DcContext {
Log.e(TAG, "Cannot create wakeLocks");
}
notificationCenter = new NotificationCenter(this);
startThreads(0);
}
@@ -444,6 +446,7 @@ public class ApplicationDcContext extends DcContext {
}
public void stopThreads() {
notificationCenter.removeAllNotifiations();
run = false;
synchronized (threadsCritical) {
while (true) {
@@ -578,13 +581,12 @@ public class ApplicationDcContext extends DcContext {
break;
case DC_EVENT_INCOMING_MSG:
MessageNotifierCompat.updateNotification((int) data1, (int) data2); // updateNotification() makes sure to run in the correct thread
notificationCenter.addNotification((int) data1, (int) data2);
if (eventCenter != null) {
eventCenter.sendToObservers(event, data1, data2); // Other parts of the code are also interested in this event
}
break;
default: {
final Object data1obj = data1IsString(event) ? dataToString(data1) : data1;
final Object data2obj = data2IsString(event) ? dataToString(data2) : data2;
@@ -15,6 +15,7 @@ import android.util.Log;
import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.notifications.NotificationCenter;
import org.thoughtcrime.securesms.util.Prefs;
public class KeepAliveService extends Service {
@@ -57,7 +58,7 @@ public class KeepAliveService extends Service {
// set self as foreground
try {
stopForeground(true);
startForeground(FG_NOTIFICATION_ID, createNotification());
startForeground(NotificationCenter.ID_PERMANTENT, createNotification());
}
catch (Exception e) {
e.printStackTrace();
@@ -93,7 +94,6 @@ public class KeepAliveService extends Service {
* Delta Chat won't get new messages reliable
**********************************************************************************************/
public static final int FG_NOTIFICATION_ID = 4142;
private Notification createNotification()
{
Intent intent = new Intent(this, ConversationListActivity.class);
@@ -112,18 +112,17 @@ public class KeepAliveService extends Service {
builder.setSmallIcon(R.drawable.notification_permanent);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O) {
createFgNotificationChannel(this);
builder.setChannelId(FG_CHANNEL_ID);
builder.setChannelId(NotificationCenter.CH_PERMANENT);
}
return builder.build();
}
private static boolean ch_created = false;
private static final String FG_CHANNEL_ID = "dc_foreground_notification_ch";
@TargetApi(Build.VERSION_CODES.O)
static private void createFgNotificationChannel(Context context) {
if(!ch_created) {
ch_created = true;
NotificationChannel channel = new NotificationChannel(FG_CHANNEL_ID,
NotificationChannel channel = new NotificationChannel(NotificationCenter.CH_PERMANENT,
"Receive messages in background.", NotificationManager.IMPORTANCE_MIN); // IMPORTANCE_DEFAULT will play a sound
channel.setDescription("Ensure reliable message receiving.");
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
@@ -1,212 +0,0 @@
package org.thoughtcrime.securesms.notifications;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.graphics.Color;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
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.preferences.widgets.NotificationPrivacyPreference;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.Util;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.util.List;
abstract class AbstractNotificationBuilder extends NotificationCompat.Builder {
@SuppressWarnings("unused")
private static final String TAG = AbstractNotificationBuilder.class.getSimpleName();
protected Context context;
protected NotificationPrivacyPreference privacy;
private int notificationId;
private Uri ringtone;
private boolean vibrate;
AbstractNotificationBuilder(Context context, NotificationPrivacyPreference privacy) {
super(context, createMsgNotificationChannel(context));
this.context = context;
this.privacy = privacy;
setLed();
}
CharSequence getStyledMessage(@NonNull Recipient recipient, @Nullable CharSequence message) {
SpannableStringBuilder builder = new SpannableStringBuilder();
builder.append(Util.getBoldedString(recipient.toShortString()));
builder.append(": ");
builder.append(message == null ? "" : message);
return builder;
}
// Alarms are not set in the notification or the notification channel but handled separately
// by the MessageNotifier. It allows us to dynamically turn on and off the sounds and as well as
// to change vibration and sounds during runtime
void setAlarms(int systemRingerMode, @Nullable Uri ringtone, Prefs.VibrateState vibrate) {
Uri appDefaultRingtone = Prefs.getNotificationRingtone(context);
boolean appDefaultVibrate = Prefs.isNotificationVibrateEnabled(context);
if (systemRingerMode == AudioManager.RINGER_MODE_NORMAL) {
if (ringtone == null && !TextUtils.isEmpty(appDefaultRingtone.toString())) {
this.ringtone = appDefaultRingtone;
} else if (ringtone != null && !ringtone.toString().isEmpty()) {
this.ringtone = ringtone;
}
}
this.vibrate = (systemRingerMode != AudioManager.RINGER_MODE_SILENT) &&
(vibrate == Prefs.VibrateState.ENABLED ||
(vibrate == Prefs.VibrateState.DEFAULT && appDefaultVibrate));
}
private void setLed() {
String ledColor = Prefs.getNotificationLedColor(context);
String ledBlinkPattern = Prefs.getNotificationLedPattern(context);
String ledBlinkPatternCustom = Prefs.getNotificationLedPatternCustom(context);
if (!ledColor.equals("none")) {
String[] blinkPatternArray = parseBlinkPattern(ledBlinkPattern, ledBlinkPatternCustom);
int argb;
try {
argb = Color.parseColor(ledColor);
}
catch (Exception e) {
argb = Color.rgb(0xFF, 0xFF, 0xFF);
}
setLights(argb,
Integer.parseInt(blinkPatternArray[0]),
Integer.parseInt(blinkPatternArray[1]));
}
}
void setTicker(@NonNull Recipient recipient, @Nullable CharSequence message) {
if (privacy.isDisplayMessage()) {
setTicker(getStyledMessage(recipient, message));
} else if (privacy.isDisplayContact()) {
setTicker(getStyledMessage(recipient, context.getString(R.string.notify_new_message)));
} else {
setTicker(context.getString(R.string.notify_new_message));
}
}
private String[] parseBlinkPattern(String blinkPattern, String blinkPatternCustom) {
if (blinkPattern.equals("custom"))
blinkPattern = blinkPatternCustom;
return blinkPattern.split(",");
}
// handle NotificationChannels:
// - since oreo, a NotificationChannel is a MUST
// - NotificationChannels have default values that have a higher precedence as the Notification.Builder setting
// - once created, NotificationChannels cannot be modified programmatically
// - NotificationChannels can be deleted, however, on re-creation it becomes un-deleted with the old settings
// - the idea is that sound and vibrate are handled outside of the scope of the notification channel
private static String createMsgNotificationChannel(Context context) {
String chBase = "ch_msg3_";
String chId = chBase + "unsupported";
if(notificationChannelsSupported()) {
try {
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
// get all values we'll use as settings for the NotificationChannel
String ledColor = Prefs.getNotificationLedColor(context);
// compute hash from these settings
String hash = "";
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(ledColor.getBytes());
hash = String.format("%X", new BigInteger(1, md.digest())).substring(0, 16);
// get channel name
chId = chBase + hash;
String oldChId = Prefs.getStringPreference(context, "ch_curr_" + chBase, "");
if (!oldChId.equals(chId)) {
try {
notificationManager.deleteNotificationChannel(oldChId);
}
catch (Exception e) {
// channel not created before
}
Prefs.setStringPreference(context, "ch_curr_" + chBase, chId);
}
// check if there is already a channel with the given name
List<NotificationChannel> channels = notificationManager.getNotificationChannels();
boolean channelExists = false;
for (int i = 0; i < channels.size(); i++) {
if (chId.equals(channels.get(i).getId())) {
channelExists = true;
}
}
// create a channel with the given settings;
// we cannot change the settings, however, this is handled by using different values for chId
if(!channelExists) {
NotificationChannel channel = new NotificationChannel(chId,
"New messages", NotificationManager.IMPORTANCE_HIGH);
channel.setDescription("Informs about new messages.");
if (!ledColor.equals("none")) {
channel.enableLights(true);
int argb;
try {
argb = Color.parseColor(ledColor);
}
catch (Exception e) {
argb = Color.rgb(0xFF, 0xFF, 0xFF);
}
channel.setLightColor(argb);
} else {
channel.enableLights(false);
}
channel.setSound(null, null);
channel.enableVibration(false);
notificationManager.createNotificationChannel(channel);
}
}
catch(Exception e) {
e.printStackTrace();
}
}
return chId;
}
private static boolean notificationChannelsSupported() {
return Build.VERSION.SDK_INT >= 26;
}
public void setNotificationId(int notificationId) {
this.notificationId = notificationId;
}
public int getNotificationId() {
return this.notificationId;
}
public Uri getRingtone() {
return this.ringtone;
}
public boolean getVibrate() {
return this.vibrate;
}
}
@@ -0,0 +1,57 @@
package org.thoughtcrime.securesms.notifications;
import android.content.Context;
import android.media.AudioManager;
import android.media.SoundPool;
import android.util.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.Prefs;
public class InChatSounds {
private static final String TAG = InChatSounds.class.getSimpleName();
private static volatile InChatSounds instance;
private Context appContext = null;
private SoundPool soundPool = null;
private int soundIn = 0;
private int soundOut = 0;
static public InChatSounds getInstance(Context context) {
if (instance == null) {
synchronized (InChatSounds.class) {
if (instance == null) {
instance = new InChatSounds(context);
}
}
}
return instance;
}
private InChatSounds(Context context) {
try {
appContext = context.getApplicationContext();
soundPool = new SoundPool(3, AudioManager.STREAM_SYSTEM, 0);
soundIn = soundPool.load(context, R.raw.sound_in, 1);
soundOut = soundPool.load(context, R.raw.sound_out, 1);
} catch(Exception e) {
Log.e(TAG, "cannot initialize sounds", e);
}
}
public void playSendSound() {
try {
soundPool.play(soundOut, 1.0f, 1.0f, 1, 0, 1.0f);
} catch(Exception e) {
Log.e(TAG, "cannot play send sound", e);
}
}
public void playIncomingSound() {
try {
soundPool.play(soundIn, 1.0f, 1.0f, 1, 0, 1.0f);
} catch(Exception e) {
Log.e(TAG, "cannot play incoming sound", e);
}
}
}
@@ -1,40 +0,0 @@
package org.thoughtcrime.securesms.notifications;
import android.os.AsyncTask;
import org.thoughtcrime.securesms.connect.ApplicationDcContext;
/**
* This marks the messages the user has acknowledged as noticed.
* @author Angelo Fuchs
*/
public class MarkAsNoticedAsyncTask extends AsyncTask<Void, Void, Void> {
private final int[] ids;
private final ApplicationDcContext dcContext;
private final boolean isChat;
/**
* goes through the given messages or chats and marks them as notified.
* @param ids chat or messages ids to be marked.
* @param dcContext the applications context
* @param isChat true if the ids are chat ids, false to signify message ids.
*/
MarkAsNoticedAsyncTask(int[] ids, ApplicationDcContext dcContext, boolean isChat) {
this.ids = ids;
this.dcContext = dcContext;
this.isChat = isChat;
}
@Override
protected Void doInBackground(Void... params) {
for (int id : ids) {
if (isChat)
dcContext.marknoticedChat(id);
else
dcContext.marknoticedChat(dcContext.getMsg(id).getChatId());
}
return null;
}
}
@@ -5,26 +5,32 @@ 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.connect.ApplicationDcContext;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.util.Util;
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 CHAT_IDS_EXTRA = "chat_ids";
public static final String NOTIFICATION_ID_EXTRA = "notification_id";
public static final String CHAT_ID_EXTRA = "chat_id";
@Override
public void onReceive(final Context context, Intent intent) {
if (!CLEAR_ACTION.equals(intent.getAction()))
if (!CLEAR_ACTION.equals(intent.getAction())) {
return;
}
final int[] chatIds = intent.getIntArrayExtra(CHAT_IDS_EXTRA);
MessageNotifierCompat.removeNotifications(chatIds);
ApplicationDcContext dcContext = DcHelper.getContext(context);
new MarkAsNoticedAsyncTask(chatIds, dcContext, true).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
final int chatId = intent.getIntExtra(CHAT_ID_EXTRA, 0);
if (chatId==0) {
return;
}
final ApplicationDcContext dcContext = DcHelper.getContext(context);
Util.runOnAnyBackgroundThread(() -> {
dcContext.notificationCenter.removeNotifications(chatId);
dcContext.marknoticedChat(chatId);
});
}
}
@@ -1,412 +0,0 @@
package org.thoughtcrime.securesms.notifications;
import android.app.NotificationManager;
import android.content.Context;
import android.media.AudioManager;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.media.SoundPool;
import android.net.Uri;
import android.os.Vibrator;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationManagerCompat;
import android.text.TextUtils;
import android.util.Log;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcMsg;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.connect.ApplicationDcContext;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.SpanUtil;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.concurrent.TimeUnit;
import static org.thoughtcrime.securesms.notifications.MessageNotifierCompat.NO_VISIBLE_CHAT_ID;
import static org.thoughtcrime.securesms.notifications.MessageNotifierCompat.SUMMARY_NOTIFICATION_ID;
abstract class MessageNotifier {
static final String TAG = org.thoughtcrime.securesms.notifications.MessageNotifierApi23.class.getSimpleName();
private static final String NOTIFICATION_GROUP = "messages";
private static final long MIN_AUDIBLE_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(20);
private static final long STARTUP_SILENCE_DELTA = TimeUnit.MINUTES.toMillis(1);
private static final long INITIAL_STARTUP = System.currentTimeMillis();
static volatile int visibleChatId = NO_VISIBLE_CHAT_ID;
static volatile long lastAudibleNotification = -1;
final NotificationState notificationState;
final Context appContext;
final Object lock;
private final SoundPool soundPool;
private final AudioManager audioManager;
private final int soundIn;
private final int soundOut;
private boolean soundInLoaded;
private boolean soundOutLoaded;
MessageNotifier(Context context) {
appContext = context.getApplicationContext();
soundPool = new SoundPool(3, AudioManager.STREAM_SYSTEM, 0);
audioManager = ServiceUtil.getAudioManager(appContext);
soundIn = soundPool.load(context, R.raw.sound_in, 1);
soundOut = soundPool.load(context, R.raw.sound_out, 1);
notificationState = new NotificationState();
lock = new Object();
soundPool.setOnLoadCompleteListener((soundPool, sampleId, status) -> {
if (status == 0) {
if (sampleId == soundIn) {
soundInLoaded = true;
} else if (sampleId == soundOut) {
soundOutLoaded = true;
}
}
});
}
public void playSendSound() {
if (Prefs.isInChatNotifications(appContext) && soundOutLoaded) {
soundPool.play(soundOut, 1.0f, 1.0f, 1, 0, 1.0f);
}
}
public void updateVisibleChat(int chatId) {
visibleChatId = chatId;
if (visibleChatId != NO_VISIBLE_CHAT_ID) {
removeNotifications(visibleChatId);
}
}
void updateNotification(int chatId, int messageId) {
updateNotification(DcHelper.getContext(appContext).getChat(chatId), messageId);
}
private void updateNotification(DcChat chat, int messageId) {
boolean isVisible = visibleChatId == chat.getId();
if (!Prefs.isNotificationsEnabled(appContext) ||
Prefs.isChatMuted(chat))
{
return;
}
if (isVisible) {
sendInChatNotification(chat);
} else if (visibleChatId != NO_VISIBLE_CHAT_ID) {
//different chat is on top
sendNotifications(chat, messageId, false);
} else {
//app is in background or different Activity is on top
sendNotifications(chat, messageId, true);
}
}
/**
* On notification privacy preference changed,
* the notification state needs to be updated.
*/
public void onNotificationPrivacyChanged() {
if (!Prefs.isNotificationsEnabled(appContext)) {
return;
}
clearNotifications();
ApplicationDcContext dcContext = DcHelper.getContext(appContext);
int[] freshMessages = dcContext.getFreshMsgs();
for (int message : freshMessages) {
DcMsg record = dcContext.getMsg(message);
updateNotification(dcContext.getChat(record.getChatId()), record.getId());
}
}
public void removeNotifications(int[] chatIds) {
List<NotificationItem> removedItems = new LinkedList<>();
synchronized (lock) {
for (int id : chatIds) {
removedItems.addAll(notificationState.removeNotificationsForChat(id));
}
}
cancelNotifications(removedItems);
recreateSummaryNotification();
}
public void removeNotifications(int chatId) {
List<NotificationItem> removedItems;
synchronized (lock) {
removedItems = notificationState.removeNotificationsForChat(chatId);
}
cancelNotifications(removedItems);
recreateSummaryNotification();
}
void cancelNotifications(List<NotificationItem> removedItems) {
NotificationManager notifications = ServiceUtil.getNotificationManager(appContext);
for (NotificationItem item : removedItems) {
notifications.cancel(item.getId());
}
}
private void recreateSummaryNotification() {
NotificationManager notifications = ServiceUtil.getNotificationManager(appContext);
notifications.cancel(SUMMARY_NOTIFICATION_ID);
synchronized (lock) {
if (notificationState.hasMultipleChats()) {
for (Integer id : notificationState.getChats()) {
sendSingleChatNotification(appContext, new NotificationState(notificationState.getNotificationsForChat(id)), false, true);
}
sendMultipleChatNotification(appContext, notificationState, false);
} else {
sendSingleChatNotification(appContext, notificationState, false, false);
}
}
}
void cancelActiveNotifications() {
NotificationManager notifications = ServiceUtil.getNotificationManager(appContext);
notifications.cancel(SUMMARY_NOTIFICATION_ID);
}
void sendNotifications(DcChat chat, int messageId, boolean signal) {
ApplicationDcContext dcContext = DcHelper.getContext(appContext);
if (signal = isSignalAllowed(signal)) {
lastAudibleNotification = System.currentTimeMillis();
}
if (chat.isDeviceTalk()) {
// currently, we just never notify on device chat.
// esp. on first start, this is annoying.
return;
}
synchronized (lock) {
addMessageToNotificationState(dcContext, chat, messageId);
if (notificationState.hasMultipleChats()) {
for (int id : notificationState.getChats()) {
sendSingleChatNotification(appContext, new NotificationState(notificationState.getNotificationsForChat(id)), false, true);
}
sendMultipleChatNotification(appContext, notificationState, signal);
} else {
sendSingleChatNotification(appContext, notificationState, signal, false);
}
}
}
boolean isSignalAllowed(boolean signalRequested) {
long now = System.currentTimeMillis();
return signalRequested &&
(now - INITIAL_STARTUP) > STARTUP_SILENCE_DELTA &&
(now - lastAudibleNotification) > MIN_AUDIBLE_PERIOD_MILLIS;
}
private void clearNotifications() {
synchronized (lock) {
notificationState.reset();
}
cancelActiveNotifications();
}
void sendSingleChatNotification(@NonNull Context context,
@NonNull NotificationState notificationState,
boolean signal,
boolean bundled)
{
AbstractNotificationBuilder notificationBuilder = createSingleChatNotification(context, notificationState, signal, bundled);
if (notificationBuilder != null)
notify(context, notificationBuilder.getNotificationId(), notificationBuilder, signal);
}
void sendMultipleChatNotification(@NonNull Context context,
@NonNull NotificationState notificationState,
boolean signal)
{
AbstractNotificationBuilder notificationBuilder = createMultipleChatNotification(context, notificationState, signal);
if (notificationBuilder != null)
notify(context, SUMMARY_NOTIFICATION_ID, notificationBuilder, signal);
}
protected AbstractNotificationBuilder createSingleChatNotification(@NonNull Context context,
@NonNull NotificationState notificationState,
boolean signal,
boolean bundled) {
if (notificationState.getNotifications().isEmpty()) {
if (!bundled) cancelActiveNotifications();
return null;
}
SingleRecipientNotificationBuilder builder = new SingleRecipientNotificationBuilder(context, Prefs.getNotificationPrivacy(context));
List<NotificationItem> notifications = notificationState.getNotifications();
NotificationItem firstItem = notifications.get(0);
Recipient recipient = firstItem.getRecipient();
int chatId = firstItem.getChatId();
int notificationId = (SUMMARY_NOTIFICATION_ID + (bundled ? chatId : 0));
builder.setNotificationId(notificationId);
builder.setChat(firstItem.getRecipient());
builder.setMessageCount(notificationState.getMessageCount());
builder.setPrimaryMessageBody(recipient, firstItem.getIndividualRecipient(),
firstItem.getText(""), firstItem.getSlideDeck());
builder.setContentIntent(firstItem.getPendingIntent(context));
builder.setGroup(NOTIFICATION_GROUP);
builder.setDeleteIntent(notificationState.getMarkAsReadIntent(context, chatId, notificationId));
long timestamp = firstItem.getTimestamp();
if (timestamp != 0) builder.setWhen(timestamp);
builder.addActions(notificationState.getMarkAsReadIntent(context, chatId, notificationId),
notificationState.getRemoteReplyIntent(context, recipient));
ListIterator<NotificationItem> iterator = notifications.listIterator(notifications.size());
while(iterator.hasPrevious()) {
NotificationItem item = iterator.previous();
builder.addMessageBody(item.getRecipient(), item.getIndividualRecipient(), item.getText());
}
if (signal) {
builder.setAlarms(audioManager.getRingerMode(),
notificationState.getRingtone(context),
notificationState.getVibrate(context));
builder.setTicker(firstItem.getIndividualRecipient(),
firstItem.getText());
}
if (!bundled) {
builder.setGroupSummary(true);
}
return builder;
}
private void playNotificationSound(Uri uri, boolean vibrate) {
if(uri != null) {
Ringtone ringtone = RingtoneManager.getRingtone(appContext, uri);
if (ringtone != null) {
ringtone.play();
}
} // else we selected "no sound"
if (vibrate) {
Vibrator v = (Vibrator) appContext.getSystemService(Context.VIBRATOR_SERVICE);
if (v!=null) {
v.vibrate(100);
v.vibrate(200);
}
}
}
protected AbstractNotificationBuilder createMultipleChatNotification(@NonNull Context context,
@NonNull NotificationState notificationState,
boolean signal) {
MultipleRecipientNotificationBuilder builder = new MultipleRecipientNotificationBuilder(context, Prefs.getNotificationPrivacy(context));
List<NotificationItem> notifications = notificationState.getNotifications();
NotificationItem firstItem = notifications.get(0);
builder.setMessageCount(notificationState.getMessageCount(), notificationState.getChatCount());
builder.setMostRecentSender(firstItem.getIndividualRecipient());
builder.setGroup(NOTIFICATION_GROUP);
builder.setDeleteIntent(notificationState.getMarkAsReadIntent(context, 0, SUMMARY_NOTIFICATION_ID));
long timestamp = firstItem.getTimestamp();
if (timestamp != 0) builder.setWhen(timestamp);
builder.addActions(notificationState.getMarkAsReadIntent(context, 0, SUMMARY_NOTIFICATION_ID));
ListIterator<NotificationItem> iterator = notifications.listIterator(notifications.size());
while(iterator.hasPrevious()) {
NotificationItem item = iterator.previous();
builder.addMessageBody(item.getRecipient(), item.getIndividualRecipient(), item.getText());
}
if (signal) {
builder.setAlarms(audioManager.getRingerMode(),
notificationState.getRingtone(context),
notificationState.getVibrate(context));
builder.setTicker(firstItem.getIndividualRecipient(),
firstItem.getText());
}
return builder;
}
private void notify(Context context, int notificationId, AbstractNotificationBuilder notificationBuilder, boolean signal) {
if (signal) {
playNotificationSound(notificationBuilder.getRingtone(), notificationBuilder.getVibrate());
}
NotificationManagerCompat.from(context).notify(notificationId, notificationBuilder.build());
}
private void sendInChatNotification(DcChat chat) {
if (!Prefs.isInChatNotifications(appContext) ||
audioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL)
{
return;
}
if(Prefs.isChatMuted(chat)) {
Log.d(TAG, "chat muted");
return;
}
if (soundInLoaded) {
soundPool.play(soundIn, 1.0f, 1.0f, 1, 0, 1.0f);
}
}
void addMessageToNotificationState(ApplicationDcContext dcContext, DcChat chat, int msgId) {
if (Prefs.isChatMuted(chat)) {
return;
}
DcMsg record = dcContext.getMsg(msgId);
if (record.isInfo()) {
return;
}
int id = record.getId();
CharSequence body = record.getDisplayBody();
DcMsg dcMsg = dcContext.getMsg(msgId);
Recipient chatRecipient = new Recipient(appContext, dcContext.getChat(dcMsg.getChatId()), null);
Recipient individualRecipient = new Recipient(appContext, null, dcContext.getContact(dcMsg.getFromId()));
SlideDeck slideDeck = new SlideDeck(dcContext.context, record);
long timestamp = record.getTimestamp();
if(slideDeck.getSlides().isEmpty())
slideDeck = null;
// TODO: if message content should be hidden on screen lock, do it here.
if (record.hasFile() && TextUtils.isEmpty(body)) {
String summaryText = record.getSummarytext(100);
if (summaryText.isEmpty()) {
body = SpanUtil.italic(appContext.getString(R.string.notify_media_message));
} else {
body = SpanUtil.italic(summaryText);
}
} else if (record.hasFile() && !record.isMediaPending()) {
String message = appContext.getString(R.string.notify_media_message_with_text, body);
int italicLength = message.length() - body.length();
body = SpanUtil.italic(message, italicLength);
}
synchronized (lock) {
notificationState.addNotification(new NotificationItem(id, chatRecipient, individualRecipient, chat.getId(), body, timestamp, slideDeck));
}
}
}
@@ -1,56 +0,0 @@
/*
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.notifications;
import android.annotation.TargetApi;
import android.app.NotificationManager;
import android.content.Context;
import android.service.notification.StatusBarNotification;
import android.util.Log;
import org.thoughtcrime.securesms.util.ServiceUtil;
/**
* Handles posting system notifications for new messages.
*
*/
@TargetApi(23)
class MessageNotifierApi23 extends MessageNotifier {
MessageNotifierApi23(Context context) {
super(context);
}
void cancelActiveNotifications() {
super.cancelActiveNotifications();
NotificationManager notifications = ServiceUtil.getNotificationManager(appContext);
try {
StatusBarNotification[] activeNotifications = notifications.getActiveNotifications();
for (StatusBarNotification activeNotification : activeNotifications) {
notifications.cancel(activeNotification.getId());
}
} catch (Throwable e) {
// XXX Appears to be a ROM bug, see #6043
Log.w(TAG, e);
notifications.cancelAll();
}
}
}
@@ -1,54 +0,0 @@
package org.thoughtcrime.securesms.notifications;
import android.content.Context;
import android.os.Build;
import org.thoughtcrime.securesms.util.Util;
public class MessageNotifierCompat {
public static final int NO_VISIBLE_CHAT_ID = -1;
static final int SUMMARY_NOTIFICATION_ID = 1339;
static final String EXTRA_REMOTE_REPLY = "extra_remote_reply";
private static MessageNotifier instance;
public static void init(Context context) {
if (instance != null) {
return;
}
if (Build.VERSION.SDK_INT < 23) {
instance = new MessageNotifierPreApi23(context);
} else {
instance = new MessageNotifierApi23(context);
}
}
public static void playSendSound() {
instance.playSendSound();
}
public static void updateNotification(int chatId, int messageId) {
Util.runOnAnyBackgroundThread(() -> instance.updateNotification(chatId, messageId));
}
public static void updateVisibleChat(int chatId) {
Util.runOnAnyBackgroundThread(() -> instance.updateVisibleChat(chatId));
}
public static void onNotificationPrivacyChanged() {
Util.runOnAnyBackgroundThread(() -> instance.onNotificationPrivacyChanged());
}
static void removeNotifications(int[] chatIds) {
Util.runOnAnyBackgroundThread(() -> instance.removeNotifications(chatIds));
}
public static void removeNotifications(int chatId) {
Util.runOnAnyBackgroundThread(() -> instance.removeNotifications(chatId));
}
}
@@ -1,82 +0,0 @@
package org.thoughtcrime.securesms.notifications;
import android.content.Context;
import androidx.core.app.NotificationManagerCompat;
import com.b44t.messenger.DcChat;
import org.thoughtcrime.securesms.connect.ApplicationDcContext;
import org.thoughtcrime.securesms.connect.DcHelper;
import java.util.List;
import static org.thoughtcrime.securesms.notifications.MessageNotifierCompat.SUMMARY_NOTIFICATION_ID;
class MessageNotifierPreApi23 extends MessageNotifier {
MessageNotifierPreApi23(Context context) {
super(context);
}
@Override
public void removeNotifications(int chatId) {
synchronized (lock) {
notificationState.removeNotificationsForChat(chatId);
}
recreateSummaryNotification();
}
@Override
public void removeNotifications(int[] chatIds) {
synchronized (lock) {
for (int id : chatIds) {
notificationState.removeNotificationsForChat(id);
}
}
recreateSummaryNotification();
}
@Override
void cancelNotifications(List<NotificationItem> removedItems) {
cancelNotifications();
}
@Override
void sendNotifications(DcChat chat, int messageId, boolean signal) {
ApplicationDcContext dcContext = DcHelper.getContext(appContext);
if (signal = isSignalAllowed(signal)) {
lastAudibleNotification = System.currentTimeMillis();
}
if (chat.isDeviceTalk()) {
// currently, we just never notify on device chat.
// esp. on first start, this is annoying.
return;
}
addMessageToNotificationState(dcContext, chat, messageId);
synchronized (lock) {
if (notificationState.hasMultipleChats()) {
sendMultipleChatNotification(appContext, notificationState, signal);
} else {
sendSingleChatNotification(appContext, notificationState, signal, false);
}
}
}
private void cancelNotifications() {
NotificationManagerCompat notificationManagerCompat = NotificationManagerCompat.from(appContext);
notificationManagerCompat.cancel(SUMMARY_NOTIFICATION_ID);
}
private void recreateSummaryNotification() {
cancelNotifications();
synchronized (lock) {
if (notificationState.hasMultipleChats()) {
sendMultipleChatNotification(appContext, notificationState, false);
} else {
sendSingleChatNotification(appContext, notificationState, false, false);
}
}
}
}
@@ -1,147 +0,0 @@
package org.thoughtcrime.securesms.notifications;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.Util;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
public class MultipleRecipientNotificationBuilder extends AbstractNotificationBuilder {
private final LinkedList<MessageBody> messageBodies = new LinkedList<>();
private static class MessageBody {
final @Nullable Recipient group;
final @NonNull Recipient sender;
final @NonNull CharSequence message;
MessageBody(@Nullable Recipient group, @NonNull Recipient sender, @NonNull CharSequence message) {
this.group = group;
this.sender = sender;
this.message = message;
}
}
MultipleRecipientNotificationBuilder(Context context, NotificationPrivacyPreference privacy) {
super(context, privacy);
setColor(context.getResources().getColor(R.color.delta_primary));
setSmallIcon(R.drawable.icon_notification);
setContentTitle(context.getString(R.string.app_name));
setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, ConversationListActivity.class), 0));
setCategory(NotificationCompat.CATEGORY_MESSAGE);
setPriority(Prefs.getNotificationPriority(context));
setGroupSummary(true);
}
void setMessageCount(int messageCount, int chatCount) {
setSubText(context.getString(R.string.notify_n_messages_in_m_chats,
messageCount, chatCount));
setContentInfo(String.valueOf(messageCount));
setNumber(messageCount); // this also sets badges on android8 and newer
}
void setMostRecentSender(Recipient recipient) {
if (privacy.isDisplayContact()) {
setContentText(context.getString(R.string.notify_most_recent_from,
recipient.toShortString()));
}
}
void addActions(PendingIntent markAsReadIntent) {
NotificationCompat.Action markAllAsReadAction = new NotificationCompat.Action(R.drawable.check,
context.getString(R.string.notify_mark_all_read),
markAsReadIntent);
addAction(markAllAsReadAction);
extend(new NotificationCompat.WearableExtender().addAction(markAllAsReadAction));
}
void addMessageBody(@Nullable Recipient group, @NonNull Recipient sender, @Nullable CharSequence body) {
messageBodies.add(new MessageBody(group, sender, body));
if (privacy.isDisplayContact() && sender.getContactUri() != null) {
addPerson(sender.getContactUri().toString());
} else if (privacy.isDisplayContact() && group != null && group.getContactUri() != null) {
addPerson(group.getContactUri().toString());
}
}
@Override
public Notification build() {
if (privacy.isDisplayMessage() || privacy.isDisplayContact()) {
NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
Map<Recipient, List<MessageBody>> byGroup = new LinkedHashMap<>();
for (MessageBody body : messageBodies) {
Recipient key = body.group == null ? body.sender : body.group;
if(byGroup.containsKey(key)) {
LinkedList<MessageBody> messagebodies = (LinkedList<MessageBody>) byGroup.remove(key);
messagebodies.addFirst(body);
byGroup.put(key, messagebodies);
} else {
byGroup.put(key, new LinkedList<>());
byGroup.get(key).add(body);
}
}
if (privacy.isDisplayMessage()) {
LinkedList<Recipient> list = new LinkedList<>(byGroup.keySet());
Iterator<Recipient> iterator = list.descendingIterator();
while(iterator.hasNext()) {
Recipient nextGroup = iterator.next();
String groupName = nextGroup.getName();
List<MessageBody> messages = byGroup.get(nextGroup);
String firstMessageSender = messages.get(0).sender.getName();
if(groupName != null && groupName.equals(firstMessageSender)) { // individual
for (MessageBody body : messages) {
style.addLine(getStyledMessage(body.sender, body.message));
}
} else { // group chat
style.addLine(Util.getBoldedString(groupName));
for (MessageBody body : messages) {
style.addLine("- " + getStyledMessage(body.sender, body.message));
}
}
}
} else if (privacy.isDisplayContact()) {
LinkedList<Recipient> list = new LinkedList<>(byGroup.keySet());
Iterator<Recipient> iterator = list.descendingIterator();
while(iterator.hasNext()) {
Recipient nextGroup = iterator.next();
String groupName = nextGroup.getName();
List<MessageBody> messages = byGroup.get(nextGroup);
String firstMessageSender = messages.get(0).sender.getName();
if(groupName != null && groupName.equals(firstMessageSender)) { // individual
for (MessageBody body : messages) {
style.addLine(body.sender.getName());
}
} else { // group chat
style.addLine(Util.getBoldedString(groupName));
for (MessageBody body : messages) {
style.addLine("- " + body.sender.getName());
}
}
}
}
setStyle(style);
}
return super.build();
}
}
@@ -0,0 +1,505 @@
package org.thoughtcrime.securesms.notifications;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.media.AudioAttributes;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.app.RemoteInput;
import androidx.core.app.TaskStackBuilder;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcMsg;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.thoughtcrime.securesms.ConversationActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.connect.ApplicationDcContext;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.Util;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class NotificationCenter {
private static final String TAG = NotificationCenter.class.getSimpleName();
@NonNull private ApplicationDcContext dcContext;
@NonNull private Context context;
private volatile int visibleChatId = 0;
private volatile long lastAudibleNotification = 0;
private static final long MIN_AUDIBLE_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(2);
private final HashMap<Integer, ArrayList<String>> inboxes = new HashMap<>(); // contains the last lines of each chat
public NotificationCenter(ApplicationDcContext dcContext) {
this.dcContext = dcContext;
this.context = dcContext.context.getApplicationContext();
}
private @Nullable Uri effectiveSound(int chatId) { // chatId=0: return app-global setting
@Nullable Uri chatRingtone = Prefs.getChatRingtone(context, chatId);
if (chatRingtone!=null) {
return chatRingtone;
} else {
@NonNull Uri appDefaultRingtone = Prefs.getNotificationRingtone(context);
if (!TextUtils.isEmpty(appDefaultRingtone.toString())) {
return appDefaultRingtone;
}
}
return null;
}
private boolean effectiveVibrate(int chatId) { // chatId=0: return app-global setting
Prefs.VibrateState vibrate = Prefs.getChatVibrate(context, chatId);
if (vibrate == Prefs.VibrateState.ENABLED) {
return true;
} else if (vibrate == Prefs.VibrateState.DISABLED) {
return false;
}
return Prefs.isNotificationVibrateEnabled(context);
}
private boolean requiresIndependentChannel(int chatId) {
return Prefs.getChatRingtone(context, chatId) != null
|| Prefs.getChatVibrate(context, chatId) != Prefs.VibrateState.DEFAULT;
}
private int getLedArgb(String ledColor) {
int argb;
try {
argb = Color.parseColor(ledColor);
}
catch (Exception e) {
argb = Color.rgb(0xFF, 0xFF, 0xFF);
}
return argb;
}
private PendingIntent getOpenChatIntent(int chatId) {
Intent intent = new Intent(context, ConversationActivity.class);
intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, chatId);
intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
return TaskStackBuilder.create(context)
.addNextIntentWithParentStack(intent)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
}
private PendingIntent getRemoteReplyIntent(int chatId) {
Intent intent = new Intent(RemoteReplyReceiver.REPLY_ACTION);
intent.setClass(context, RemoteReplyReceiver.class);
intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
intent.putExtra(RemoteReplyReceiver.CHAT_ID_EXTRA, chatId);
intent.setPackage(context.getPackageName());
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
private PendingIntent getMarkAsReadIntent(int chatId) {
Intent intent = new Intent(MarkReadReceiver.CLEAR_ACTION);
intent.setClass(context, MarkReadReceiver.class);
intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
intent.putExtra(MarkReadReceiver.CHAT_ID_EXTRA, chatId);
intent.setPackage(context.getPackageName());
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
// Groups and Notification channel groups
// --------------------------------------------------------------------------------------------
public static final String GRP_MSG = "chgrp_msg";
// Notification IDs
// --------------------------------------------------------------------------------------------
public static final int ID_PERMANTENT = 1;
public static final int ID_MSG_OFFSET = 0; // msgId is added - as msgId start at 10, there are no conflicts with lower numbers
// Notification channels
// --------------------------------------------------------------------------------------------
// Overview:
// - since SDK 26 (Oreo), a NotificationChannel is a MUST for notifications
// - NotificationChannels are defined by a channelId
// and its user-editable settings have a higher precedence as the Notification.Builder setting
// - once created, NotificationChannels cannot be modified programmatically
// - NotificationChannels can be deleted, however, on re-creation with the same id,
// it becomes un-deleted with the old user-defined settings
//
// How we use Notification channel:
// - We include the delta-chat-notifications settings into the name of the channelId
// - The chatId is included only, if there are separate sound- or vibration-settings for a chat
// - This way, we have stable and few channelIds and the user
// can edit the notifications in Delta Chat as well as in the system
// channelIds: CH_MSG_* are used here, the other ones from outside (defined here to have some overview)
public static final String CH_MSG_PREFIX = "ch_msg";
public static final String CH_MSG_VERSION = "4";
public static final String CH_PERMANENT = "dc_foreground_notification_ch";
private boolean notificationChannelsSupported() {
return Build.VERSION.SDK_INT >= 26;
}
// full name is "ch_msgV_HASH" or "ch_msgV_HASH.CHATID"
private String computeChannelId(String ledColor, boolean vibrate, @Nullable Uri ringtone, int chatId) {
String channelId = CH_MSG_PREFIX;
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(ledColor.getBytes());
md.update(vibrate ? (byte) 1 : (byte) 0);
md.update((ringtone != null ? ringtone.toString() : "").getBytes());
if (chatId!=0) {
// for multi-account, force different channelIds for maybe the same chatIds in multiple accounts
md.update(dcContext.getConfig("addr").getBytes());
}
String hash = String.format("%X", new BigInteger(1, md.digest())).substring(0, 16);
channelId = CH_MSG_PREFIX + CH_MSG_VERSION + "_" + hash;
if (chatId!=0) {
channelId += String.format(".%d", chatId);
}
} catch(Exception e) {
Log.e(TAG, e.toString());
}
return channelId;
}
// return chatId from "ch_msgV_HASH.CHATID" or 0
private int parseNotificationChannelChatId(String channelId) {
try {
int point = channelId.lastIndexOf(".");
if (point>0) {
return Integer.parseInt(channelId.substring(point + 1));
}
} catch(Exception e) { }
return 0;
}
private String getNotificationChannelGroup(NotificationManagerCompat notificationManager) {
if (notificationChannelsSupported() && notificationManager.getNotificationChannelGroup(GRP_MSG) == null) {
NotificationChannelGroup chGrp = new NotificationChannelGroup(GRP_MSG, context.getString(R.string.pref_chats));
notificationManager.createNotificationChannelGroup(chGrp);
}
return GRP_MSG;
}
private String getNotificationChannel(NotificationManagerCompat notificationManager, DcChat dcChat) {
int chatId = dcChat.getId();
String channelId = CH_MSG_PREFIX;
if (notificationChannelsSupported()) {
try {
// get all values we'll use as settings for the NotificationChannel
String ledColor = Prefs.getNotificationLedColor(context);
boolean defaultVibrate = effectiveVibrate(chatId);
@Nullable Uri ringtone = effectiveSound(chatId);
boolean isIndependent = requiresIndependentChannel(chatId);
// get channel id from these settings
channelId = computeChannelId(ledColor, defaultVibrate, ringtone, isIndependent? chatId : 0);
// user-visible name of the channel -
// we just use the name of the chat or "Default"
// (the name is shown in the context of the group "Chats" - that should be enough context)
String name = context.getString(R.string.def);
if (isIndependent) {
name = dcChat.getName();
}
// check if there is already a channel with the given name
List<NotificationChannel> channels = notificationManager.getNotificationChannels();
boolean channelExists = false;
for (int i = 0; i < channels.size(); i++) {
String currChannelId = channels.get(i).getId();
if (currChannelId.startsWith(CH_MSG_PREFIX)) {
// this is one of the message channels handled here ...
if (currChannelId.equals(channelId)) {
// ... this is the actually required channel, fine :)
// update the name to reflect localize changes and chat renames
channelExists = true;
channels.get(i).setName(name);
} else {
// ... another message channel, delete if it is not in use.
int currChatId = parseNotificationChannelChatId(currChannelId);
if (!currChannelId.equals(computeChannelId(ledColor, effectiveVibrate(currChatId), effectiveSound(currChatId), currChatId))) {
notificationManager.deleteNotificationChannel(currChannelId);
}
}
}
}
// create a channel with the given settings;
// we cannot change the settings, however, this is handled by using different values for chId
if(!channelExists) {
NotificationChannel channel = new NotificationChannel(channelId, name, NotificationManager.IMPORTANCE_HIGH);
channel.setDescription("Informs about new messages.");
channel.setGroup(getNotificationChannelGroup(notificationManager));
channel.enableVibration(defaultVibrate);
channel.setShowBadge(true);
if (!ledColor.equals("none")) {
channel.enableLights(true);
channel.setLightColor(getLedArgb(ledColor));
} else {
channel.enableLights(false);
}
if (ringtone != null && !TextUtils.isEmpty(ringtone.toString())) {
channel.setSound(ringtone,
new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN)
.setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT)
.build());
}
notificationManager.createNotificationChannel(channel);
}
}
catch(Exception e) {
e.printStackTrace();
}
}
return channelId;
}
// add notifications & co.
// --------------------------------------------------------------------------------------------
public void addNotification(int chatId, int msgId) {
Util.runOnAnyBackgroundThread(() -> {
DcChat dcChat = dcContext.getChat(chatId);
if (!Prefs.isNotificationsEnabled(context) || dcChat.isMuted()) {
return;
}
if (chatId == visibleChatId) {
if (Prefs.isInChatNotifications(context)) {
InChatSounds.getInstance(context).playIncomingSound();
}
return;
}
if (dcChat.isDeviceTalk()) {
// currently, we just never notify on device chat.
// esp. on first start, this is annoying.
return;
}
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
// get notification text as a single line
NotificationPrivacyPreference privacy = Prefs.getNotificationPrivacy(context);
DcMsg dcMsg = dcContext.getMsg(msgId);
String line = privacy.isDisplayMessage()? dcMsg.getSummarytext(100) : context.getString(R.string.notify_new_message);
if (dcChat.isGroup() && privacy.isDisplayContact()) {
line = dcContext.getContact(dcMsg.getFromId()).getFirstName() + ": " + line;
}
// play signal?
long now = System.currentTimeMillis();
boolean signal = (now - lastAudibleNotification) > MIN_AUDIBLE_PERIOD_MILLIS;
if (signal) {
lastAudibleNotification = now;
}
// create a basic notification
// even without a name or message displayed,
// it makes sense to use separate notification channels and to open the respective chat directly -
// the user may eg. have chosen a different sound
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, getNotificationChannel(notificationManager, dcChat))
.setSmallIcon(R.drawable.icon_notification)
.setColor(context.getResources().getColor(R.color.delta_primary))
.setPriority(Prefs.getNotificationPriority(context))
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
.setGroup(GRP_MSG)
.setOnlyAlertOnce(!signal)
.setContentText(line)
.setContentIntent(getOpenChatIntent(chatId));
if (privacy.isDisplayContact()) {
builder.setContentTitle(dcChat.getName());
}
// if privacy allows, for better accessibility,
// prepend the sender in the ticker also for one-to-one chats (for group-chats, this is already done)
String tickerLine = line;
if (!dcChat.isGroup() && privacy.isDisplayContact()) {
line = dcContext.getContact(dcMsg.getFromId()).getFirstName() + ": " + line;
}
builder.setTicker(tickerLine);
// set sound, vibrate, led for systems that do not have notification channels
if (!notificationChannelsSupported()) {
if (signal) {
Uri sound = effectiveSound(chatId);
if (sound != null) {
builder.setSound(sound);
}
boolean vibrate = effectiveVibrate(chatId);
if (vibrate) {
builder.setDefaults(Notification.DEFAULT_VIBRATE);
}
}
String ledColor = Prefs.getNotificationLedColor(context);
if (!ledColor.equals("none")) {
builder.setLights(getLedArgb(ledColor),500, 2000);
}
}
// set avatar
Recipient recipient = new Recipient(context, dcChat, null);
if (privacy.isDisplayContact()) {
try {
Drawable drawable;
ContactPhoto contactPhoto = recipient.getContactPhoto(context);
if (contactPhoto != null) {
drawable = GlideApp.with(context.getApplicationContext())
.load(contactPhoto)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.circleCrop()
.submit(context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width),
context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_height))
.get();
} else {
drawable = recipient.getFallbackContactPhoto().asDrawable(context, recipient.getFallbackAvatarColor(context));
}
if (drawable != null) {
int wh = context.getResources().getDimensionPixelSize(R.dimen.contact_photo_target_size);
Bitmap bitmap = BitmapUtil.createFromDrawable(drawable, wh, wh);
if (bitmap != null) {
builder.setLargeIcon(bitmap);
}
}
} catch (Exception e) { Log.w(TAG, e); }
}
// add buttons that allow some actions without opening Delta Chat.
// if privacy options are enabled, the buttons are not added.
if (privacy.isDisplayContact() && privacy.isDisplayMessage()
&& !Prefs.isScreenLockEnabled(context)) {
try {
PendingIntent inNotificationReplyIntent = getRemoteReplyIntent(chatId);
PendingIntent markReadIntent = getMarkAsReadIntent(chatId);
NotificationCompat.Action markAsReadAction = new NotificationCompat.Action(R.drawable.check,
context.getString(R.string.notify_mark_read),
markReadIntent);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder(R.drawable.ic_reply_white_36dp,
context.getString(R.string.notify_reply_button),
inNotificationReplyIntent)
.addRemoteInput(new RemoteInput.Builder(RemoteReplyReceiver.EXTRA_REMOTE_REPLY)
.setLabel(context.getString(R.string.notify_reply_button)).build())
.build();
builder.addAction(replyAction);
}
NotificationCompat.Action wearableReplyAction = new NotificationCompat.Action.Builder(R.drawable.ic_reply,
context.getString(R.string.notify_reply_button),
inNotificationReplyIntent)
.addRemoteInput(new RemoteInput.Builder(RemoteReplyReceiver.EXTRA_REMOTE_REPLY)
.setLabel(context.getString(R.string.notify_reply_button)).build())
.build();
builder.addAction(markAsReadAction);
builder.extend(new NotificationCompat.WearableExtender().addAction(markAsReadAction).addAction(wearableReplyAction));
} catch(Exception e) { Log.w(TAG, e); }
}
// create a tiny inbox (gets visible if the notification is expanded)
if (privacy.isDisplayContact() && privacy.isDisplayMessage()) {
try {
NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle();
synchronized (inboxes) {
ArrayList<String> lines = inboxes.get(chatId);
if (lines == null) {
lines = new ArrayList<>();
inboxes.put(chatId, lines);
}
lines.add(line);
for (int l = lines.size() - 1; l >= 0; l--) {
inboxStyle.addLine(lines.get(l));
}
}
builder.setStyle(inboxStyle);
} catch(Exception e) { Log.w(TAG, e); }
}
// messages count, some os make some use of that
// - do not use setSubText() as this is displayed together with setContentInfo() eg. on Lollipop
// - setNumber() may overwrite setContentInfo(), should be called last
// weird stuff.
int cnt = dcContext.getFreshMsgCount(chatId);
builder.setContentInfo(String.valueOf(cnt));
builder.setNumber(cnt);
// add notification, we use one notification per chat,
// esp. older android are not that great at grouping
notificationManager.notify(ID_MSG_OFFSET + chatId, builder.build());
});
}
public void removeNotifications(int chatId) {
synchronized (inboxes) {
inboxes.remove(chatId);
}
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
notificationManager.cancel(ID_MSG_OFFSET + chatId);
}
public void removeAllNotifiations() {
synchronized (inboxes) {
inboxes.clear();
}
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
notificationManager.cancelAll();
}
public void updateVisibleChat(int chatId) {
Util.runOnAnyBackgroundThread(() -> {
visibleChatId = chatId;
if (chatId!=0) {
removeNotifications(chatId);
}
});
}
public void maybePlaySendSound(DcChat dcChat) {
if (Prefs.isNotificationsEnabled(context) && !dcChat.isMuted()) {
InChatSounds.getInstance(context).playSendSound();
}
}
}
@@ -1,87 +0,0 @@
package org.thoughtcrime.securesms.notifications;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.TaskStackBuilder;
import org.thoughtcrime.securesms.ConversationActivity;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.recipients.Recipient;
public class NotificationItem {
private final int id;
private final @NonNull Recipient threadRecipient;
private final @NonNull Recipient individualRecipient;
private final int chatId;
private final @Nullable CharSequence text;
private final long timestamp;
private final @Nullable SlideDeck slideDeck;
NotificationItem(int id,
@NonNull Recipient threadRecipient,
@NonNull Recipient individualRecipient,
int chatId, @Nullable CharSequence text, long timestamp,
@Nullable SlideDeck slideDeck)
{
this.id = id;
this.threadRecipient = threadRecipient;
this.individualRecipient = individualRecipient;
this.text = text;
this.chatId = chatId;
this.timestamp = timestamp;
this.slideDeck = slideDeck;
}
public @NonNull Recipient getRecipient() {
return threadRecipient;
}
public @Nullable CharSequence getText() {
return text;
}
public @NonNull CharSequence getText(@NonNull CharSequence defaul) {
return (text == null ? defaul : text);
}
public long getTimestamp() {
return timestamp;
}
public int getChatId() {
return chatId;
}
@NonNull Recipient getIndividualRecipient() {
return individualRecipient;
}
@Deprecated
public int getThreadId() {
return chatId;
}
@Nullable SlideDeck getSlideDeck() {
return slideDeck;
}
@NonNull PendingIntent getPendingIntent(Context context) {
Intent intent = new Intent(context, ConversationActivity.class);
intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, chatId);
intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
return TaskStackBuilder.create(context)
.addNextIntentWithParentStack(intent)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
}
public int getId() {
return id;
}
}
@@ -1,156 +0,0 @@
package org.thoughtcrime.securesms.notifications;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.util.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.Prefs.VibrateState;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import static org.thoughtcrime.securesms.notifications.MessageNotifierCompat.SUMMARY_NOTIFICATION_ID;
public class NotificationState {
private final LinkedList<NotificationItem> notifications = new LinkedList<>();
private final LinkedHashSet<Integer> chats = new LinkedHashSet<>();
private int notificationCount = 0;
NotificationState() {}
NotificationState(@NonNull List<NotificationItem> items) {
for (NotificationItem item : items) {
addNotification(item);
}
}
public void reset() {
notificationCount = 0;
notifications.clear();
chats.clear();
}
void addNotification(NotificationItem item) {
notifications.addFirst(item);
if (chats.contains(item.getChatId())) {
chats.remove(item.getChatId());
}
chats.add(item.getChatId());
notificationCount++;
}
@Nullable Uri getRingtone(Context context) {
if (!notifications.isEmpty()) {
Recipient recipient = notifications.iterator().next().getRecipient();
if (recipient.getAddress().isDcChat()) {
return Prefs.getChatRingtone(context, recipient.getAddress().getDcChatId());
}
}
return null;
}
VibrateState getVibrate(Context context) {
if (!notifications.isEmpty()) {
Recipient recipient = notifications.iterator().next().getRecipient();
if (recipient.getAddress().isDcChat()) {
return Prefs.getChatVibrate(context, recipient.getAddress().getDcChatId());
}
}
return VibrateState.DEFAULT;
}
boolean hasMultipleChats() {
return chats.size() > 1;
}
public LinkedHashSet<Integer> getChats() {
return chats;
}
int getChatCount() {
return chats.size();
}
int getMessageCount() {
return notificationCount;
}
public List<NotificationItem> getNotifications() {
return notifications;
}
List<NotificationItem> getNotificationsForChat(int chatId) {
LinkedList<NotificationItem> list = new LinkedList<>();
for (NotificationItem item : notifications) {
if (item.getChatId() == chatId) list.addFirst(item);
}
return list;
}
List<NotificationItem> removeNotificationsForChat(int chatId) {
LinkedList<NotificationItem> removedItems = new LinkedList<>();
chats.remove(chatId);
for (Iterator<NotificationItem> it = notifications.iterator(); it.hasNext();) {
NotificationItem item = it.next();
if (item.getChatId() == chatId) {
removedItems.add(item);
it.remove();
}
}
notificationCount -= removedItems.size();
return removedItems;
}
PendingIntent getMarkAsReadIntent(Context context, int chatId, int notificationId) {
int index = 0;
int[] chatArray;
if (notificationId == SUMMARY_NOTIFICATION_ID) {
chatArray = new int[chats.size()];
for (int chat : chats) {
Log.w("NotificationState", "Added chat: " + chat);
chatArray[index++] = chat;
}
} else {
chatArray = new int[]{chatId};
}
Intent intent = new Intent(MarkReadReceiver.CLEAR_ACTION);
intent.setClass(context, MarkReadReceiver.class);
intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
intent.putExtra(MarkReadReceiver.CHAT_IDS_EXTRA, chatArray);
intent.putExtra(MarkReadReceiver.NOTIFICATION_ID_EXTRA, notificationId);
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
PendingIntent getRemoteReplyIntent(Context context, Recipient recipient) {
if (chats.size() != 1) throw new AssertionError("We only support replies to single chat notifications!");
Intent intent = new Intent(RemoteReplyReceiver.REPLY_ACTION);
intent.setClass(context, RemoteReplyReceiver.class);
intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
intent.putExtra(RemoteReplyReceiver.ADDRESS_EXTRA, recipient.getAddress());
intent.setPackage(context.getPackageName());
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
}
@@ -27,7 +27,6 @@ import androidx.core.app.RemoteInput;
import org.thoughtcrime.securesms.connect.ApplicationDcContext;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.database.Address;
/**
@@ -37,30 +36,27 @@ 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 ADDRESS_EXTRA = "address";
public static final String CHAT_ID_EXTRA = "chat_id";
public static final String EXTRA_REMOTE_REPLY = "extra_remote_reply";
@SuppressLint("StaticFieldLeak")
@Override
public void onReceive(final Context context, Intent intent) {
if (!REPLY_ACTION.equals(intent.getAction())) return;
Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
final int chatId = intent.getIntExtra(CHAT_ID_EXTRA, 0);
if (remoteInput == null) return;
if (remoteInput == null || chatId == 0) return;
final Address address = intent.getParcelableExtra(ADDRESS_EXTRA);
final CharSequence responseText = remoteInput.getCharSequence(MessageNotifierCompat.EXTRA_REMOTE_REPLY);
final CharSequence responseText = remoteInput.getCharSequence(EXTRA_REMOTE_REPLY);
if (responseText != null) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
if(address.isDcChat()) {
ApplicationDcContext dcContext = DcHelper.getContext(context);
dcContext.sendTextMsg(address.getDcChatId(), responseText.toString());
MessageNotifierCompat.removeNotifications(address.getDcChatId());
}
ApplicationDcContext dcContext = DcHelper.getContext(context);
dcContext.sendTextMsg(chatId, responseText.toString());
dcContext.notificationCenter.removeNotifications(chatId);
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
@@ -1,250 +0,0 @@
package org.thoughtcrime.securesms.notifications;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationCompat.Action;
import androidx.core.app.RemoteInput;
import android.text.SpannableStringBuilder;
import android.util.Log;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
import org.thoughtcrime.securesms.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.Prefs;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util;
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 final LinkedList<CharSequence> messageBodies = new LinkedList<>();
private SlideDeck slideDeck;
private CharSequence contentTitle;
private CharSequence contentText;
SingleRecipientNotificationBuilder(@NonNull Context context, @NonNull NotificationPrivacyPreference privacy)
{
super(context, privacy);
setSmallIcon(R.drawable.icon_notification);
setColor(context.getResources().getColor(R.color.delta_primary));
setPriority(Prefs.getNotificationPriority(context));
setCategory(NotificationCompat.CATEGORY_MESSAGE);
}
public void setChat(@NonNull Recipient recipient) {
if (privacy.isDisplayContact()) {
setContentTitle(recipient.toShortString());
if (recipient.getContactUri() != null) {
addPerson(recipient.getContactUri().toString());
}
ContactPhoto contactPhoto = recipient.getContactPhoto(context);
FallbackContactPhoto fallbackContactPhoto = recipient.getFallbackContactPhoto();
if (contactPhoto != null) {
try {
setLargeIcon(GlideApp.with(context.getApplicationContext())
.load(contactPhoto)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.circleCrop()
.submit(context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width),
context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_height))
.get());
} catch (Exception e) {
Log.w(TAG, e);
setLargeIcon(fallbackContactPhoto.asDrawable(context, recipient.getFallbackAvatarColor(context)));
}
} else {
setLargeIcon(fallbackContactPhoto.asDrawable(context, recipient.getFallbackAvatarColor(context)));
}
} else {
setContentTitle(context.getString(R.string.app_name));
setLargeIcon(new GeneratedContactPhoto("Unknown").asDrawable(context, ThemeUtil.getDummyContactColor(context)));
}
}
public void setMessageCount(int messageCount) {
setContentInfo(String.valueOf(messageCount));
setNumber(messageCount); // this also sets badges on android8 and newer
}
public void setPrimaryMessageBody(@NonNull Recipient chatRecipients,
@NonNull Recipient individualRecipient,
@NonNull CharSequence message,
@Nullable SlideDeck slideDeck)
{
SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
if (privacy.isDisplayContact() && chatRecipients.isGroupRecipient()) {
stringBuilder.append(Util.getBoldedString(individualRecipient.toShortString() + ": "));
}
if (privacy.isDisplayMessage()) {
setContentText(stringBuilder.append(message));
this.slideDeck = slideDeck;
} else {
setContentText(stringBuilder.append(context.getString(R.string.notify_new_message)));
}
}
void addActions(@NonNull PendingIntent markReadIntent,
@NonNull PendingIntent inNotificationReplyIntent)
{
Action markAsReadAction = new Action(R.drawable.check,
context.getString(R.string.notify_mark_read),
markReadIntent);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Action replyAction = new Action.Builder(R.drawable.ic_reply_white_36dp,
context.getString(R.string.notify_reply_button),
inNotificationReplyIntent)
.addRemoteInput(new RemoteInput.Builder(MessageNotifierCompat.EXTRA_REMOTE_REPLY)
.setLabel(context.getString(R.string.notify_reply_button)).build())
.build();
addAction(replyAction);
}
Action wearableReplyAction = new Action.Builder(R.drawable.ic_reply,
context.getString(R.string.notify_reply_button),
inNotificationReplyIntent)
.addRemoteInput(new RemoteInput.Builder(MessageNotifierCompat.EXTRA_REMOTE_REPLY)
.setLabel(context.getString(R.string.notify_reply_button)).build())
.build();
addAction(markAsReadAction);
extend(new NotificationCompat.WearableExtender().addAction(markAsReadAction)
.addAction(wearableReplyAction));
}
void addMessageBody(@NonNull Recipient chatRecipient,
@NonNull Recipient individualRecipient,
@Nullable CharSequence messageBody)
{
SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
if (privacy.isDisplayContact() && chatRecipient.isGroupRecipient()) {
stringBuilder.append(Util.getBoldedString(individualRecipient.toShortString() + ": "));
}
if (privacy.isDisplayMessage()) {
messageBodies.addFirst(stringBuilder.append(messageBody == null ? "" : messageBody));
} else {
messageBodies.addFirst(stringBuilder.append(context.getString(R.string.notify_new_message)));
}
}
@Override
public Notification build() {
// the filtering whether or not to display messages and contacts is done in addMessageBody
// and setPrimaryMessageBody, no need to do it here again.
NotificationCompat.Style style;
if (Build.VERSION.SDK_INT < 23) {
style = new NotificationCompat.InboxStyle();
for (CharSequence messageBody : messageBodies) {
((NotificationCompat.InboxStyle) style).addLine(messageBody);
}
} else if (messageBodies.size() == 1 && hasBigPictureSlide(slideDeck)) {
style = new NotificationCompat.BigPictureStyle()
.bigPicture(getBigPicture(slideDeck))
.setSummaryText(getBigText(messageBodies));
} else {
style = new NotificationCompat.BigTextStyle().bigText(getBigText(messageBodies));
}
setStyle(style);
return super.build();
}
private void setLargeIcon(@Nullable Drawable drawable) {
if (drawable != null) {
int largeIconTargetSize = context.getResources().getDimensionPixelSize(R.dimen.contact_photo_target_size);
Bitmap recipientPhotoBitmap = BitmapUtil.createFromDrawable(drawable, largeIconTargetSize, largeIconTargetSize);
if (recipientPhotoBitmap != null) {
setLargeIcon(recipientPhotoBitmap);
}
}
}
private boolean hasBigPictureSlide(@Nullable SlideDeck slideDeck) {
if (slideDeck == null || Build.VERSION.SDK_INT < 16) {
return false;
}
Slide thumbnailSlide = slideDeck.getThumbnailSlide();
return thumbnailSlide != null &&
thumbnailSlide.hasImage() &&
thumbnailSlide.getThumbnailUri() != null;
}
private Bitmap getBigPicture(@NonNull SlideDeck slideDeck)
{
try {
@SuppressWarnings("ConstantConditions")
Uri uri = slideDeck.getThumbnailSlide().getThumbnailUri();
return GlideApp.with(context.getApplicationContext())
.asBitmap()
.load(new DecryptableStreamUriLoader.DecryptableUri(uri))
.diskCacheStrategy(DiskCacheStrategy.NONE)
.submit(500, 500)
.get();
} catch (InterruptedException | ExecutionException e) {
Log.w(TAG, e);
return Bitmap.createBitmap(500, 500, 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 = contentText;
return super.setContentText(contentText);
}
private CharSequence getBigText(List<CharSequence> messageBodies) {
SpannableStringBuilder content = new SpannableStringBuilder();
for (CharSequence message : messageBodies) {
content.append(message);
content.append('\n');
}
return content;
}
}
@@ -1,12 +1,10 @@
package org.thoughtcrime.securesms.preferences;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.provider.Settings;
import androidx.annotation.Nullable;
@@ -18,7 +16,6 @@ import android.text.TextUtils;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.connect.KeepAliveService;
import org.thoughtcrime.securesms.notifications.MessageNotifierCompat;
import org.thoughtcrime.securesms.util.Prefs;
import static android.app.Activity.RESULT_OK;
@@ -35,12 +32,10 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme
this.findPreference(Prefs.LED_COLOR_PREF)
.setOnPreferenceChangeListener(new ListSummaryListener());
this.findPreference(Prefs.LED_BLINK_PREF)
.setOnPreferenceChangeListener(new ListSummaryListener());
this.findPreference(Prefs.RINGTONE_PREF)
.setOnPreferenceChangeListener(new RingtoneSummaryListener());
this.findPreference(Prefs.NOTIFICATION_PRIVACY_PREF)
.setOnPreferenceChangeListener(new NotificationPrivacyListener());
.setOnPreferenceChangeListener(new ListSummaryListener());
this.findPreference(Prefs.NOTIFICATION_PRIORITY_PREF)
.setOnPreferenceChangeListener(new ListSummaryListener());
@@ -61,7 +56,6 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme
});
initializeListSummary((ListPreference) findPreference(Prefs.LED_COLOR_PREF));
initializeListSummary((ListPreference) findPreference(Prefs.LED_BLINK_PREF));
initializeListSummary((ListPreference) findPreference(Prefs.NOTIFICATION_PRIVACY_PREF));
initializeListSummary((ListPreference) findPreference(Prefs.NOTIFICATION_PRIORITY_PREF));
@@ -151,21 +145,4 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme
String ret = context.getString(notificationsEnabled ? R.string.on : R.string.off);
return ret;
}
private class NotificationPrivacyListener extends ListSummaryListener {
@SuppressLint("StaticFieldLeak")
@Override
public boolean onPreferenceChange(Preference preference, Object value) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
MessageNotifierCompat.onNotificationPrivacyChanged();
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
return super.onPreferenceChange(preference, value);
}
}
}
+1 -11
View File
@@ -42,9 +42,7 @@ public class Prefs {
private static final String CHAT_VIBRATE = "pref_chat_vibrate_"; // followed by chat-id
private static final String NOTIFICATION_PREF = "pref_key_enable_notifications";
public static final String LED_COLOR_PREF = "pref_led_color";
public static final String LED_BLINK_PREF = "pref_led_blink";
private static final String LED_BLINK_PREF_CUSTOM = "pref_led_blink_custom";
public static final String CHAT_MUTED_UNTIL = "pref_chat_muted_until_"; // followed by chat-id
public static final String CHAT_MUTED_UNTIL = "pref_chat_muted_until_"; // followed by chat-id
private static final String CHAT_RINGTONE = "pref_chat_ringtone_"; // followed by chat-id
public static final String SCREEN_LOCK_TIMEOUT_INTERVAL_PREF = "pref_timeout_interval";
public static final String SCREEN_LOCK_TIMEOUT_PREF = "pref_timeout_passphrase";
@@ -269,14 +267,6 @@ public class Prefs {
return getStringPreference(context, LED_COLOR_PREF, "blue");
}
public static String getNotificationLedPattern(Context context) {
return getStringPreference(context, LED_BLINK_PREF, "500,2000");
}
public static String getNotificationLedPatternCustom(Context context) {
return getStringPreference(context, LED_BLINK_PREF_CUSTOM, "500,2000");
}
// mute
public static void setChatMuteDuration(DcContext context, int chatId, long duration) {