mirror of
https://github.com/oxen-io/session-android.git
synced 2024-12-25 01:07:47 +00:00
f8bb065ffd
Closes #3859 Fixes #1858 // FREEBIE
436 lines
18 KiB
Java
436 lines
18 KiB
Java
/**
|
|
* Copyright (C) 2011 Whisper Systems
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
package org.thoughtcrime.securesms.notifications;
|
|
|
|
import android.app.AlarmManager;
|
|
import android.app.NotificationManager;
|
|
import android.app.PendingIntent;
|
|
import android.content.BroadcastReceiver;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.database.Cursor;
|
|
import android.media.AudioManager;
|
|
import android.media.MediaPlayer;
|
|
import android.net.Uri;
|
|
import android.support.annotation.NonNull;
|
|
import android.support.annotation.Nullable;
|
|
import android.text.Spannable;
|
|
import android.text.SpannableString;
|
|
import android.text.TextUtils;
|
|
import android.text.style.StyleSpan;
|
|
import android.util.Log;
|
|
|
|
import org.thoughtcrime.securesms.ConversationActivity;
|
|
import org.thoughtcrime.securesms.R;
|
|
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
|
import org.thoughtcrime.securesms.database.PushDatabase;
|
|
import org.thoughtcrime.securesms.database.SmsDatabase;
|
|
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
|
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
|
import org.thoughtcrime.securesms.mms.SlideDeck;
|
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
|
import org.thoughtcrime.securesms.recipients.RecipientFactory;
|
|
import org.thoughtcrime.securesms.recipients.Recipients;
|
|
import org.thoughtcrime.securesms.service.KeyCachingService;
|
|
import org.thoughtcrime.securesms.util.ListenableFutureTask;
|
|
import org.thoughtcrime.securesms.util.SpanUtil;
|
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
|
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
|
import org.whispersystems.textsecure.api.messages.TextSecureEnvelope;
|
|
|
|
import java.io.IOException;
|
|
import java.util.List;
|
|
import java.util.ListIterator;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
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 int NOTIFICATION_ID = 1338;
|
|
|
|
private volatile static long visibleThread = -1;
|
|
|
|
public static final String EXTRA_VOICE_REPLY = "extra_voice_reply";
|
|
|
|
public static void setVisibleThread(long threadId) {
|
|
visibleThread = threadId;
|
|
}
|
|
|
|
public static void notifyMessageDeliveryFailed(Context context, Recipients recipients, long threadId) {
|
|
if (visibleThread == threadId) {
|
|
sendInThreadNotification(context, recipients);
|
|
} else {
|
|
Intent intent = new Intent(context, ConversationActivity.class);
|
|
intent.putExtra(ConversationActivity.RECIPIENTS_EXTRA, recipients.getIds());
|
|
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 updateNotification(@NonNull Context context, @Nullable MasterSecret masterSecret) {
|
|
if (!TextSecurePreferences.isNotificationsEnabled(context)) {
|
|
return;
|
|
}
|
|
|
|
updateNotification(context, masterSecret, false, false, 0);
|
|
}
|
|
|
|
public static void updateNotification(@NonNull Context context,
|
|
@Nullable MasterSecret masterSecret,
|
|
long threadId)
|
|
{
|
|
updateNotification(context, masterSecret, false, threadId);
|
|
}
|
|
|
|
public static void updateNotification(@NonNull Context context,
|
|
@Nullable MasterSecret masterSecret,
|
|
boolean includePushDatabase,
|
|
long threadId)
|
|
{
|
|
Recipients recipients = DatabaseFactory.getThreadDatabase(context)
|
|
.getRecipientsForThreadId(threadId);
|
|
|
|
if (!TextSecurePreferences.isNotificationsEnabled(context) ||
|
|
(recipients != null && recipients.isMuted()))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (visibleThread == threadId) {
|
|
ThreadDatabase threads = DatabaseFactory.getThreadDatabase(context);
|
|
threads.setRead(threadId);
|
|
sendInThreadNotification(context, threads.getRecipientsForThreadId(threadId));
|
|
} else {
|
|
updateNotification(context, masterSecret, true, includePushDatabase, 0);
|
|
}
|
|
}
|
|
|
|
private static void updateNotification(@NonNull Context context,
|
|
@Nullable MasterSecret masterSecret,
|
|
boolean signal,
|
|
boolean includePushDatabase,
|
|
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()))
|
|
{
|
|
((NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE))
|
|
.cancel(NOTIFICATION_ID);
|
|
updateBadge(context, 0);
|
|
clearReminder(context);
|
|
return;
|
|
}
|
|
|
|
NotificationState notificationState = constructNotificationState(context, masterSecret, telcoCursor);
|
|
|
|
if (includePushDatabase) {
|
|
appendPushNotificationState(context, notificationState, pushCursor);
|
|
}
|
|
|
|
if (notificationState.hasMultipleThreads()) {
|
|
sendMultipleThreadNotification(context, notificationState, signal);
|
|
} else {
|
|
sendSingleThreadNotification(context, masterSecret, notificationState, signal);
|
|
}
|
|
|
|
updateBadge(context, notificationState.getMessageCount());
|
|
scheduleReminder(context, reminderCount);
|
|
} finally {
|
|
if (telcoCursor != null) telcoCursor.close();
|
|
if (pushCursor != null) pushCursor.close();
|
|
}
|
|
}
|
|
|
|
private static void sendSingleThreadNotification(@NonNull Context context,
|
|
@Nullable MasterSecret masterSecret,
|
|
@NonNull NotificationState notificationState,
|
|
boolean signal)
|
|
{
|
|
if (notificationState.getNotifications().isEmpty()) {
|
|
((NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE))
|
|
.cancel(NOTIFICATION_ID);
|
|
return;
|
|
}
|
|
|
|
SingleRecipientNotificationBuilder builder = new SingleRecipientNotificationBuilder(context, masterSecret, TextSecurePreferences.getNotificationPrivacy(context));
|
|
List<NotificationItem> notifications = notificationState.getNotifications();
|
|
|
|
builder.setSender(notifications.get(0).getIndividualRecipient());
|
|
builder.setMessageCount(notificationState.getMessageCount());
|
|
builder.setPrimaryMessageBody(notifications.get(0).getText(), notifications.get(0).getSlideDeck());
|
|
builder.setContentIntent(notifications.get(0).getPendingIntent(context));
|
|
|
|
long timestamp = notifications.get(0).getTimestamp();
|
|
if (timestamp != 0) builder.setWhen(timestamp);
|
|
|
|
builder.addActions(masterSecret,
|
|
notificationState.getMarkAsReadIntent(context),
|
|
notificationState.getQuickReplyIntent(context, notifications.get(0).getRecipients()),
|
|
notificationState.getWearableReplyIntent(context, notifications.get(0).getRecipients()));
|
|
|
|
ListIterator<NotificationItem> iterator = notifications.listIterator(notifications.size());
|
|
|
|
while(iterator.hasPrevious()) {
|
|
builder.addMessageBody(iterator.previous().getText());
|
|
|
|
}
|
|
|
|
if (signal) {
|
|
builder.setAlarms(notificationState.getRingtone(), notificationState.getVibrate());
|
|
builder.setTicker(notifications.get(0).getIndividualRecipient(),
|
|
notifications.get(0).getText());
|
|
}
|
|
|
|
((NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE))
|
|
.notify(NOTIFICATION_ID, builder.build());
|
|
}
|
|
|
|
private static void sendMultipleThreadNotification(@NonNull Context context,
|
|
@NonNull NotificationState notificationState,
|
|
boolean 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());
|
|
|
|
long timestamp = notifications.get(0).getTimestamp();
|
|
if (timestamp != 0) builder.setWhen(timestamp);
|
|
|
|
builder.addActions(notificationState.getMarkAsReadIntent(context));
|
|
|
|
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(), notificationState.getVibrate());
|
|
builder.setTicker(notifications.get(0).getText());
|
|
}
|
|
|
|
((NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE))
|
|
.notify(NOTIFICATION_ID, builder.build());
|
|
}
|
|
|
|
private static void sendInThreadNotification(Context context, Recipients recipients) {
|
|
try {
|
|
if (!TextSecurePreferences.isInThreadNotifications(context)) {
|
|
return;
|
|
}
|
|
|
|
Uri uri = recipients != null ? recipients.getRingtone() : null;
|
|
|
|
if (uri == null) {
|
|
String ringtone = TextSecurePreferences.getNotificationRingtone(context);
|
|
|
|
if (ringtone == null) {
|
|
Log.w(TAG, "ringtone preference was null.");
|
|
return;
|
|
} else {
|
|
uri = Uri.parse(ringtone);
|
|
}
|
|
}
|
|
|
|
if (uri == null) {
|
|
Log.w(TAG, "couldn't parse ringtone uri " + TextSecurePreferences.getNotificationRingtone(context));
|
|
return;
|
|
}
|
|
|
|
MediaPlayer player = new MediaPlayer();
|
|
player.setAudioStreamType(AudioManager.STREAM_NOTIFICATION);
|
|
player.setDataSource(context, uri);
|
|
player.setLooping(false);
|
|
player.setVolume(0.25f, 0.25f);
|
|
player.prepare();
|
|
|
|
final AudioManager audioManager = ((AudioManager)context.getSystemService(Context.AUDIO_SERVICE));
|
|
|
|
audioManager.requestAudioFocus(null, AudioManager.STREAM_NOTIFICATION,
|
|
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
|
|
|
|
player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
|
|
@Override
|
|
public void onCompletion(MediaPlayer mp) {
|
|
audioManager.abandonAudioFocus(null);
|
|
}
|
|
});
|
|
|
|
player.start();
|
|
} catch (IOException ioe) {
|
|
Log.w("MessageNotifier", ioe);
|
|
}
|
|
}
|
|
|
|
private static void appendPushNotificationState(@NonNull Context context,
|
|
@NonNull NotificationState notificationState,
|
|
@NonNull Cursor cursor)
|
|
{
|
|
PushDatabase.Reader reader = null;
|
|
TextSecureEnvelope envelope;
|
|
|
|
try {
|
|
reader = DatabaseFactory.getPushDatabase(context).readerFor(cursor);
|
|
|
|
while ((envelope = reader.getNext()) != null) {
|
|
Recipients recipients = RecipientFactory.getRecipientsFromString(context, envelope.getSource(), false);
|
|
Recipient recipient = recipients.getPrimaryRecipient();
|
|
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
|
|
SpannableString body = new SpannableString(context.getString(R.string.MessageNotifier_locked_message));
|
|
body.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
|
|
if (!recipients.isMuted()) {
|
|
notificationState.addNotification(new NotificationItem(recipient, recipients, null, threadId, body, 0, null));
|
|
}
|
|
}
|
|
} finally {
|
|
if (reader != null)
|
|
reader.close();
|
|
}
|
|
}
|
|
|
|
private static NotificationState constructNotificationState(@NonNull Context context,
|
|
@Nullable MasterSecret masterSecret,
|
|
@NonNull Cursor cursor)
|
|
{
|
|
NotificationState notificationState = new NotificationState();
|
|
MessageRecord record;
|
|
MmsSmsDatabase.Reader reader;
|
|
|
|
if (masterSecret == null) reader = DatabaseFactory.getMmsSmsDatabase(context).readerFor(cursor);
|
|
else reader = DatabaseFactory.getMmsSmsDatabase(context).readerFor(cursor, masterSecret);
|
|
|
|
while ((record = reader.getNext()) != null) {
|
|
Recipient recipient = record.getIndividualRecipient();
|
|
Recipients recipients = record.getRecipients();
|
|
long threadId = record.getThreadId();
|
|
CharSequence body = record.getDisplayBody();
|
|
Recipients threadRecipients = null;
|
|
ListenableFutureTask<SlideDeck> slideDeck = null;
|
|
long timestamp;
|
|
|
|
if (record.isPush()) timestamp = record.getDateSent();
|
|
else timestamp = record.getDateReceived();
|
|
|
|
if (threadId != -1) {
|
|
threadRecipients = DatabaseFactory.getThreadDatabase(context).getRecipientsForThreadId(threadId);
|
|
}
|
|
|
|
if (SmsDatabase.Types.isDecryptInProgressType(record.getType()) || !record.getBody().isPlaintext()) {
|
|
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_locked_message));
|
|
} else if (record.isMms() && TextUtils.isEmpty(body)) {
|
|
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_media_message));
|
|
slideDeck = ((MediaMmsMessageRecord)record).getSlideDeckFuture();
|
|
} else if (record.isMms() && !record.isMmsNotification()) {
|
|
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).getSlideDeckFuture();
|
|
}
|
|
|
|
if (threadRecipients == null || !threadRecipients.isMuted()) {
|
|
notificationState.addNotification(new NotificationItem(recipient, recipients, threadRecipients, threadId, body, timestamp, slideDeck));
|
|
}
|
|
}
|
|
|
|
reader.close();
|
|
return notificationState;
|
|
}
|
|
|
|
private static void updateBadge(Context context, int count) {
|
|
try {
|
|
ShortcutBadger.setBadge(context.getApplicationContext(), 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);
|
|
}
|
|
|
|
private 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";
|
|
|
|
@Override
|
|
public void onReceive(Context context, Intent intent) {
|
|
MasterSecret masterSecret = KeyCachingService.getMasterSecret(context);
|
|
int reminderCount = intent.getIntExtra("reminder_count", 0);
|
|
MessageNotifier.updateNotification(context, masterSecret, true, reminderCount + 1);
|
|
}
|
|
}
|
|
|
|
public static class DeleteReceiver extends BroadcastReceiver {
|
|
|
|
public static final String DELETE_REMINDER_ACTION = "org.thoughtcrime.securesms.MessageNotifier.DELETE_REMINDER_ACTION";
|
|
|
|
@Override
|
|
public void onReceive(Context context, Intent intent) {
|
|
clearReminder(context);
|
|
}
|
|
}
|
|
}
|