diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 7762493325..6dc4d31bc8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -52,7 +52,6 @@ import org.thoughtcrime.securesms.logging.PersistentLogger; import org.thoughtcrime.securesms.logging.SignalUncaughtExceptionHandler; import org.thoughtcrime.securesms.messages.InitialMessageRetriever; import org.thoughtcrime.securesms.migrations.ApplicationMigrations; -import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; @@ -162,7 +161,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi isAppVisible = false; Log.i(TAG, "App is no longer visible."); KeyCachingService.onAppBackgrounded(this); - MessageNotifier.setVisibleThread(-1); + ApplicationDependencies.getMessageNotifier().clearVisibleThread(); ApplicationDependencies.getFrameRateTracker().end(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsActivity.java index 8b796297f4..594d2bf05b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MessageDetailsActivity.java @@ -32,6 +32,7 @@ import androidx.loader.content.Loader; import org.thoughtcrime.securesms.conversation.ConversationItem; import org.thoughtcrime.securesms.database.GroupDatabase; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.logging.Log; import android.view.LayoutInflater; import android.view.MenuItem; @@ -52,7 +53,6 @@ import org.thoughtcrime.securesms.database.loaders.MessageDetailsLoader; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideRequests; -import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.recipients.LiveRecipient; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.sms.MessageSender; @@ -133,13 +133,13 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity assert getSupportActionBar() != null; getSupportActionBar().setTitle(R.string.AndroidManifest__message_details); - MessageNotifier.setVisibleThread(threadId); + ApplicationDependencies.getMessageNotifier().setVisibleThread(threadId); } @Override protected void onPause() { super.onPause(); - MessageNotifier.setVisibleThread(-1L); + ApplicationDependencies.getMessageNotifier().clearVisibleThread(); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelperV1.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelperV1.java index dae0ad7d4f..fdf442b98d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelperV1.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelperV1.java @@ -30,7 +30,6 @@ import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob; import org.thoughtcrime.securesms.logging.Log; -import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter; @@ -178,9 +177,9 @@ class DirectoryHelperV1 { if (insertResult.isPresent()) { int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY); if (hour >= 9 && hour < 23) { - MessageNotifier.updateNotification(context, insertResult.get().getThreadId(), true); + ApplicationDependencies.getMessageNotifier().updateNotification(context, insertResult.get().getThreadId(), true); } else { - MessageNotifier.updateNotification(context, insertResult.get().getThreadId(), false); + ApplicationDependencies.getMessageNotifier().updateNotification(context, insertResult.get().getThreadId(), false); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 4a3d8b5ca5..c9069d15f6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -193,7 +193,6 @@ import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.mms.StickerSlide; import org.thoughtcrime.securesms.mms.VideoSlide; import org.thoughtcrime.securesms.notifications.MarkReadReceiver; -import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.profiles.GroupShareProfileView; @@ -498,14 +497,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity ApplicationDependencies.getJobManager().add(new RequestGroupV2InfoJob(recipientSnapshot.getGroupId().get().requireV2())); } - MessageNotifier.setVisibleThread(threadId); + ApplicationDependencies.getMessageNotifier().setVisibleThread(threadId); markThreadAsRead(); } @Override protected void onPause() { super.onPause(); - MessageNotifier.setVisibleThread(-1L); + ApplicationDependencies.getMessageNotifier().clearVisibleThread(); if (isFinishing()) overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_end); inputPanel.onPause(); @@ -2229,7 +2228,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity Context context = ConversationActivity.this; List messageIds = DatabaseFactory.getThreadDatabase(context).setRead(params[0], false); - MessageNotifier.updateNotification(context); + ApplicationDependencies.getMessageNotifier().updateNotification(context); MarkReadReceiver.process(context, messageIds); return null; @@ -2259,7 +2258,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity if (refreshFragment) { fragment.reload(recipient.get(), threadId); - MessageNotifier.setVisibleThread(threadId); + ApplicationDependencies.getMessageNotifier().setVisibleThread(threadId); } fragment.scrollToBottom(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index 3d40f0bc20..e898aa1644 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -90,7 +90,6 @@ import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder; import org.thoughtcrime.securesms.components.reminder.ShareReminder; import org.thoughtcrime.securesms.components.reminder.SystemSmsImportReminder; import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder; -import org.thoughtcrime.securesms.conversation.ConversationFragment; import org.thoughtcrime.securesms.conversationlist.ConversationListAdapter.ItemClickListener; import org.thoughtcrime.securesms.conversationlist.model.MessageResult; import org.thoughtcrime.securesms.conversationlist.model.SearchResult; @@ -113,13 +112,11 @@ import org.thoughtcrime.securesms.megaphone.MegaphoneViewBuilder; import org.thoughtcrime.securesms.megaphone.Megaphones; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.notifications.MarkReadReceiver; -import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.util.AvatarUtil; -import org.thoughtcrime.securesms.util.CachedInflater; import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.StickyHeaderDecoration; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -591,7 +588,7 @@ public class ConversationListFragment extends MainFragment implements LoaderMana SignalExecutors.BOUNDED.execute(() -> { List messageIds = DatabaseFactory.getThreadDatabase(context).setAllThreadsRead(); - MessageNotifier.updateNotification(context); + ApplicationDependencies.getMessageNotifier().updateNotification(context); MarkReadReceiver.process(context, messageIds); }); } @@ -672,7 +669,7 @@ public class ConversationListFragment extends MainFragment implements LoaderMana @Override protected Void doInBackground(Void... params) { DatabaseFactory.getThreadDatabase(getActivity()).deleteConversations(selectedConversations); - MessageNotifier.updateNotification(getActivity()); + ApplicationDependencies.getMessageNotifier().updateNotification(getActivity()); return null; } @@ -873,7 +870,7 @@ public class ConversationListFragment extends MainFragment implements LoaderMana if (unreadCount > 0) { List messageIds = DatabaseFactory.getThreadDatabase(getActivity()).setRead(threadId, false); - MessageNotifier.updateNotification(getActivity()); + ApplicationDependencies.getMessageNotifier().updateNotification(getActivity()); MarkReadReceiver.process(getActivity(), messageIds); } } @@ -884,7 +881,7 @@ public class ConversationListFragment extends MainFragment implements LoaderMana if (unreadCount > 0) { DatabaseFactory.getThreadDatabase(getActivity()).incrementUnread(threadId, unreadCount); - MessageNotifier.updateNotification(getActivity()); + ApplicationDependencies.getMessageNotifier().updateNotification(getActivity()); } } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, threadId); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/ClassicOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/ClassicOpenHelper.java index a802828659..63588eb40a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/ClassicOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/ClassicOpenHelper.java @@ -35,10 +35,10 @@ import org.thoughtcrime.securesms.database.PushDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.migrations.LegacyMigrationJob; -import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.phonenumbers.NumberUtil; import org.thoughtcrime.securesms.util.Base64; @@ -434,7 +434,7 @@ public class ClassicOpenHelper extends SQLiteOpenHelper { db.endTransaction(); // DecryptingQueue.schedulePendingDecrypts(context, masterSecret); - MessageNotifier.updateNotification(context); + ApplicationDependencies.getMessageNotifier().updateNotification(context); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java index d1398d0f75..1296c3cd21 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java @@ -14,6 +14,8 @@ import org.thoughtcrime.securesms.keyvalue.KeyValueStore; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.megaphone.MegaphoneRepository; import org.thoughtcrime.securesms.messages.InitialMessageRetriever; +import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier; +import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; import org.thoughtcrime.securesms.recipients.LiveRecipientCache; import org.thoughtcrime.securesms.messages.IncomingMessageObserver; @@ -57,6 +59,7 @@ public class ApplicationDependencies { private static GroupsV2Operations groupsV2Operations; private static EarlyMessageCache earlyMessageCache; private static InitialMessageRetriever initialMessageRetriever; + private static MessageNotifier messageNotifier; public static synchronized void init(@NonNull Application application, @NonNull Provider provider) { if (ApplicationDependencies.application != null || ApplicationDependencies.provider != null) { @@ -246,6 +249,16 @@ public class ApplicationDependencies { return initialMessageRetriever; } + public static synchronized @NonNull MessageNotifier getMessageNotifier() { + assertInitialization(); + + if (messageNotifier == null) { + messageNotifier = provider.provideMessageNotifier(); + } + + return messageNotifier; + } + private static void assertInitialization() { if (application == null || provider == null) { throw new UninitializedException(); @@ -267,6 +280,7 @@ public class ApplicationDependencies { @NonNull MegaphoneRepository provideMegaphoneRepository(); @NonNull EarlyMessageCache provideEarlyMessageCache(); @NonNull InitialMessageRetriever provideInitialMessageRetriever(); + @NonNull MessageNotifier provideMessageNotifier(); } private static class UninitializedException extends IllegalStateException { diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java index 7237dcfab0..83c3791fe5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java @@ -21,6 +21,8 @@ import org.thoughtcrime.securesms.keyvalue.KeyValueStore; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.megaphone.MegaphoneRepository; import org.thoughtcrime.securesms.messages.InitialMessageRetriever; +import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier; +import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.push.SecurityEventListener; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; import org.thoughtcrime.securesms.recipients.LiveRecipientCache; @@ -158,6 +160,11 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr return new InitialMessageRetriever(); } + @Override + public @NonNull MessageNotifier provideMessageNotifier() { + return new DefaultMessageNotifier(); + } + private static class DynamicCredentialsProvider implements CredentialsProvider { private final Context context; diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java index 64308c74d4..5e87c921d3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java @@ -20,7 +20,6 @@ import org.thoughtcrime.securesms.jobs.PushGroupUpdateJob; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.OutgoingGroupUpdateMessage; -import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; @@ -252,7 +251,7 @@ public final class GroupV1MessageProcessor { Optional insertResult = smsDatabase.insertMessageInbox(groupMessage); if (insertResult.isPresent()) { - MessageNotifier.updateNotification(context, insertResult.get().getThreadId()); + ApplicationDependencies.getMessageNotifier().updateNotification(context, insertResult.get().getThreadId()); return insertResult.get().getThreadId(); } else { return null; diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java index 529f2dcdff..304e7b2330 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java @@ -20,7 +20,6 @@ import org.thoughtcrime.securesms.jobmanager.JobLogger; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.MmsException; -import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.util.AttachmentUtil; import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.Hex; @@ -109,7 +108,7 @@ public class AttachmentDownloadJob extends BaseJob { @Override public void onRun() throws IOException { doWork(); - MessageNotifier.updateNotification(context, 0); + ApplicationDependencies.getMessageNotifier().updateNotification(context, 0); } public void doWork() throws IOException { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java index 734e10a952..c089a66851 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java @@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult; import org.thoughtcrime.securesms.database.MmsDatabase; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; @@ -27,7 +28,6 @@ import org.thoughtcrime.securesms.mms.IncomingMediaMessage; import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.MmsRadioException; import org.thoughtcrime.securesms.mms.PartParser; -import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; @@ -94,7 +94,7 @@ public class MmsDownloadJob extends BaseJob { public void onAdded() { if (automatic && KeyCachingService.isLocked(context)) { DatabaseFactory.getMmsDatabase(context).markIncomingNotificationReceived(threadId); - MessageNotifier.updateNotification(context); + ApplicationDependencies.getMessageNotifier().updateNotification(context); } } @@ -169,7 +169,7 @@ public class MmsDownloadJob extends BaseJob { if (automatic) { database.markIncomingNotificationReceived(threadId); - MessageNotifier.updateNotification(context, threadId); + ApplicationDependencies.getMessageNotifier().updateNotification(context, threadId); } } @@ -244,7 +244,7 @@ public class MmsDownloadJob extends BaseJob { if (insertResult.isPresent()) { database.delete(messageId); - MessageNotifier.updateNotification(context, insertResult.get().getThreadId()); + ApplicationDependencies.getMessageNotifier().updateNotification(context, insertResult.get().getThreadId()); } } @@ -256,7 +256,7 @@ public class MmsDownloadJob extends BaseJob { if (automatic) { db.markIncomingNotificationReceived(threadId); - MessageNotifier.updateNotification(context, threadId); + ApplicationDependencies.getMessageNotifier().updateNotification(context, threadId); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsSendJob.java index 66979aed49..4bcf5a1853 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MmsSendJob.java @@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.ThreadDatabase; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.JobLogger; @@ -41,7 +42,6 @@ import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.MmsSendResult; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; import org.thoughtcrime.securesms.mms.PartAuthority; -import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException; import org.thoughtcrime.securesms.transport.UndeliverableMessageException; @@ -333,7 +333,7 @@ public final class MmsSendJob extends SendJob { Recipient recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId); if (recipient != null) { - MessageNotifier.notifyMessageDeliveryFailed(context, recipient, threadId); + ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, recipient, threadId); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java index 3d62d9cf53..09a8fbcab0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java @@ -69,6 +69,7 @@ import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage; import org.thoughtcrime.securesms.mms.QuoteModel; import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.mms.StickerSlide; +import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; @@ -623,7 +624,7 @@ public final class PushProcessMessageJob extends BaseJob { sessionStore.deleteAllSessions(content.getSender().getIdentifier()); SecurityEvent.broadcastSecurityUpdateEvent(context); - MessageNotifier.updateNotification(context, threadId); + ApplicationDependencies.getMessageNotifier().updateNotification(context, threadId); } } @@ -748,11 +749,11 @@ public final class PushProcessMessageJob extends BaseJob { if (reaction.isRemove()) { db.deleteReaction(targetMessage.getId(), reactionAuthor.getId()); - MessageNotifier.updateNotification(context); + ApplicationDependencies.getMessageNotifier().updateNotification(context); } else { ReactionRecord reactionRecord = new ReactionRecord(reaction.getEmoji(), reactionAuthor.getId(), message.getTimestamp(), System.currentTimeMillis()); db.addReaction(targetMessage.getId(), reactionRecord); - MessageNotifier.updateNotification(context, targetMessage.getThreadId(), false); + ApplicationDependencies.getMessageNotifier().updateNotification(context, targetMessage.getThreadId(), false); } } else if (targetMessage != null) { Log.w(TAG, "[handleReaction] Found a matching message, but it's flagged as remotely deleted. timestamp: " + reaction.getTargetSentTimestamp() + " author: " + targetAuthor.getId()); @@ -771,7 +772,7 @@ public final class PushProcessMessageJob extends BaseJob { if (targetMessage != null && RemoteDeleteUtil.isValidReceive(targetMessage, sender, content.getServerTimestamp())) { MessagingDatabase db = targetMessage.isMms() ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context); db.markAsRemoteDelete(targetMessage.getId()); - MessageNotifier.updateNotification(context, targetMessage.getThreadId(), false); + ApplicationDependencies.getMessageNotifier().updateNotification(context, targetMessage.getThreadId(), false); } else if (targetMessage == null) { Log.w(TAG, "[handleRemoteDelete] Could not find matching message! timestamp: " + delete.getTargetSentTimestamp() + " author: " + sender.getId()); ApplicationDependencies.getEarlyMessageCache().store(sender.getId(), delete.getTargetSentTimestamp(), content); @@ -930,10 +931,10 @@ public final class PushProcessMessageJob extends BaseJob { if (threadId != null) { DatabaseFactory.getThreadDatabase(context).setRead(threadId, true); - MessageNotifier.updateNotification(context); + ApplicationDependencies.getMessageNotifier().updateNotification(context); } - MessageNotifier.setLastDesktopActivityTimestamp(message.getTimestamp()); + ApplicationDependencies.getMessageNotifier().setLastDesktopActivityTimestamp(message.getTimestamp()); } catch (MmsException e) { throw new StorageFailedException(e, content.getSender().getIdentifier(), content.getSenderDevice()); } @@ -985,9 +986,10 @@ public final class PushProcessMessageJob extends BaseJob { } } - MessageNotifier.setLastDesktopActivityTimestamp(envelopeTimestamp); - MessageNotifier.cancelDelayedNotifications(); - MessageNotifier.updateNotification(context); + MessageNotifier messageNotifier = ApplicationDependencies.getMessageNotifier(); + messageNotifier.setLastDesktopActivityTimestamp(envelopeTimestamp); + messageNotifier.cancelDelayedNotifications(); + messageNotifier.updateNotification(context); } private void handleSynchronizeViewOnceOpenMessage(@NonNull ViewOnceOpenMessage openMessage, long envelopeTimestamp) { @@ -999,9 +1001,10 @@ public final class PushProcessMessageJob extends BaseJob { DatabaseFactory.getAttachmentDatabase(context).deleteAttachmentFilesForViewOnceMessage(record.getId()); } - MessageNotifier.setLastDesktopActivityTimestamp(envelopeTimestamp); - MessageNotifier.cancelDelayedNotifications(); - MessageNotifier.updateNotification(context); + MessageNotifier messageNotifier = ApplicationDependencies.getMessageNotifier(); + messageNotifier.setLastDesktopActivityTimestamp(envelopeTimestamp); + messageNotifier.cancelDelayedNotifications(); + messageNotifier.updateNotification(context); } private void handleMediaMessage(@NonNull SignalServiceContent content, @@ -1063,7 +1066,7 @@ public final class PushProcessMessageJob extends BaseJob { } if (insertResult.isPresent()) { - MessageNotifier.updateNotification(context, insertResult.get().getThreadId()); + ApplicationDependencies.getMessageNotifier().updateNotification(context, insertResult.get().getThreadId()); if (message.isViewOnce()) { ApplicationContext.getInstance(context).getViewOnceMessageManager().scheduleIfNecessary(); @@ -1258,7 +1261,7 @@ public final class PushProcessMessageJob extends BaseJob { } if (threadId != null) { - MessageNotifier.updateNotification(context, threadId); + ApplicationDependencies.getMessageNotifier().updateNotification(context, threadId); } } @@ -1323,7 +1326,7 @@ public final class PushProcessMessageJob extends BaseJob { if (insertResult.isPresent()) { smsDatabase.markAsInvalidVersionKeyExchange(insertResult.get().getMessageId()); - MessageNotifier.updateNotification(context, insertResult.get().getThreadId()); + ApplicationDependencies.getMessageNotifier().updateNotification(context, insertResult.get().getThreadId()); } } else { smsDatabase.markAsInvalidVersionKeyExchange(smsMessageId.get()); @@ -1340,7 +1343,7 @@ public final class PushProcessMessageJob extends BaseJob { if (insertResult.isPresent()) { smsDatabase.markAsDecryptFailed(insertResult.get().getMessageId()); - MessageNotifier.updateNotification(context, insertResult.get().getThreadId()); + ApplicationDependencies.getMessageNotifier().updateNotification(context, insertResult.get().getThreadId()); } } else { smsDatabase.markAsDecryptFailed(smsMessageId.get()); @@ -1357,7 +1360,7 @@ public final class PushProcessMessageJob extends BaseJob { if (insertResult.isPresent()) { smsDatabase.markAsNoSession(insertResult.get().getMessageId()); - MessageNotifier.updateNotification(context, insertResult.get().getThreadId()); + ApplicationDependencies.getMessageNotifier().updateNotification(context, insertResult.get().getThreadId()); } } else { smsDatabase.markAsNoSession(smsMessageId.get()); @@ -1377,7 +1380,7 @@ public final class PushProcessMessageJob extends BaseJob { if (insertResult.isPresent()) { smsDatabase.markAsUnsupportedProtocolVersion(insertResult.get().getMessageId()); - MessageNotifier.updateNotification(context, insertResult.get().getThreadId()); + ApplicationDependencies.getMessageNotifier().updateNotification(context, insertResult.get().getThreadId()); } } else { smsDatabase.markAsNoSession(smsMessageId.get()); @@ -1397,7 +1400,7 @@ public final class PushProcessMessageJob extends BaseJob { if (insertResult.isPresent()) { smsDatabase.markAsInvalidMessage(insertResult.get().getMessageId()); - MessageNotifier.updateNotification(context, insertResult.get().getThreadId()); + ApplicationDependencies.getMessageNotifier().updateNotification(context, insertResult.get().getThreadId()); } } else { smsDatabase.markAsNoSession(smsMessageId.get()); @@ -1414,7 +1417,7 @@ public final class PushProcessMessageJob extends BaseJob { if (insertResult.isPresent()) { smsDatabase.markAsLegacyVersion(insertResult.get().getMessageId()); - MessageNotifier.updateNotification(context, insertResult.get().getThreadId()); + ApplicationDependencies.getMessageNotifier().updateNotification(context, insertResult.get().getThreadId()); } } else { smsDatabase.markAsLegacyVersion(smsMessageId.get()); @@ -1431,7 +1434,7 @@ public final class PushProcessMessageJob extends BaseJob { // if (smsMessageId <= 0) { // Pair messageAndThreadId = insertPlaceholder(masterSecret, envelope); // smsDatabase.markAsDecryptDuplicate(messageAndThreadId.first); -// MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second); +// ApplicationDependencies.getMessageNotifier().updateNotification(context, masterSecret, messageAndThreadId.second); // } else { // smsDatabase.markAsDecryptDuplicate(smsMessageId); // } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java index fb33783964..7cfcfdd2a4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java @@ -30,7 +30,6 @@ import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; import org.thoughtcrime.securesms.mms.PartAuthority; -import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; @@ -221,7 +220,7 @@ public abstract class PushSendJob extends SendJob { Recipient recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId); if (threadId != -1 && recipient != null) { - MessageNotifier.notifyMessageDeliveryFailed(context, recipient, threadId); + ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, recipient, threadId); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java index 6cdcf66d0c..dcc8a52b70 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java @@ -14,7 +14,6 @@ import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; -import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.service.ExpiringMessageManager; @@ -26,15 +25,12 @@ import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; -import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; -import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import java.io.IOException; -import java.util.List; public class PushTextSendJob extends PushSendJob { @@ -122,7 +118,7 @@ public class PushTextSendJob extends PushSendJob { } catch (InsecureFallbackApprovalException e) { warn(TAG, "Failure", e); database.markAsPendingInsecureSmsFallback(record.getId()); - MessageNotifier.notifyMessageDeliveryFailed(context, record.getRecipient(), record.getThreadId()); + ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, record.getRecipient(), record.getThreadId()); ApplicationDependencies.getJobManager().add(new DirectoryRefreshJob(false)); } catch (UntrustedIdentityException e) { warn(TAG, "Failure", e); @@ -147,7 +143,7 @@ public class PushTextSendJob extends PushSendJob { Recipient recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId); if (threadId != -1 && recipient != null) { - MessageNotifier.notifyMessageDeliveryFailed(context, recipient, threadId); + ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, recipient, threadId); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsReceiveJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsReceiveJob.java index 08c9803db2..d90b5823cf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsReceiveJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsReceiveJob.java @@ -4,6 +4,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import android.telephony.SmsMessage; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint; @@ -12,7 +13,6 @@ import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult; import org.thoughtcrime.securesms.database.SmsDatabase; -import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.sms.IncomingTextMessage; import org.thoughtcrime.securesms.util.Base64; @@ -81,7 +81,7 @@ public class SmsReceiveJob extends BaseJob { Optional insertResult = storeMessage(message.get()); if (insertResult.isPresent()) { - MessageNotifier.updateNotification(context, insertResult.get().getThreadId()); + ApplicationDependencies.getMessageNotifier().updateNotification(context, insertResult.get().getThreadId()); } } else if (message.isPresent()) { Log.w(TAG, "*** Received blocked SMS, ignoring..."); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsSendJob.java index 5f35d5c0ad..4b91a482ef 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsSendJob.java @@ -9,6 +9,7 @@ import androidx.annotation.NonNull; import android.telephony.PhoneNumberUtils; import android.telephony.SmsManager; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint; @@ -17,7 +18,6 @@ import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.model.SmsMessageRecord; -import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.service.SmsDeliveryListener; import org.thoughtcrime.securesms.transport.UndeliverableMessageException; @@ -87,7 +87,7 @@ public class SmsSendJob extends SendJob { } catch (UndeliverableMessageException ude) { warn(TAG, ude); DatabaseFactory.getSmsDatabase(context).markAsSentFailed(record.getId()); - MessageNotifier.notifyMessageDeliveryFailed(context, record.getRecipient(), record.getThreadId()); + ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, record.getRecipient(), record.getThreadId()); } } @@ -105,7 +105,7 @@ public class SmsSendJob extends SendJob { DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId); if (threadId != -1 && recipient != null) { - MessageNotifier.notifyMessageDeliveryFailed(context, recipient, threadId); + ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, recipient, threadId); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsSentJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsSentJob.java index a5f4558681..270d1a3afd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsSentJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsSentJob.java @@ -9,12 +9,10 @@ import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.logging.Log; -import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.model.SmsMessageRecord; -import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.service.SmsDeliveryListener; public class SmsSentJob extends BaseJob { @@ -107,7 +105,7 @@ public class SmsSentJob extends BaseJob { break; default: database.markAsSentFailed(messageId); - MessageNotifier.notifyMessageDeliveryFailed(context, record.getRecipient(), record.getThreadId()); + ApplicationDependencies.getMessageNotifier().notifyMessageDeliveryFailed(context, record.getRecipient(), record.getThreadId()); } } catch (NoSuchMessageException e) { Log.w(TAG, e); diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java index dfcc3b0fc4..98c909472e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java @@ -22,7 +22,6 @@ import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason; import org.thoughtcrime.securesms.jobs.MultiDeviceMessageRequestResponseJob; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.notifications.MarkReadReceiver; -import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.recipients.LiveRecipient; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; @@ -119,7 +118,7 @@ final class MessageRequestRepository { List messageIds = DatabaseFactory.getThreadDatabase(context) .setEntireThreadRead(threadId); - MessageNotifier.updateNotification(context); + ApplicationDependencies.getMessageNotifier().updateNotification(context); MarkReadReceiver.process(context, messageIds); if (TextSecurePreferences.isMultiDevice(context)) { @@ -189,7 +188,7 @@ final class MessageRequestRepository { List messageIds = DatabaseFactory.getThreadDatabase(context) .setEntireThreadRead(threadId); - MessageNotifier.updateNotification(context); + ApplicationDependencies.getMessageNotifier().updateNotification(context); MarkReadReceiver.process(context, messageIds); if (TextSecurePreferences.isMultiDevice(context)) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoHeardReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoHeardReceiver.java index 615887c3fc..975908f2b7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoHeardReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoHeardReceiver.java @@ -26,6 +26,7 @@ import androidx.core.app.NotificationManagerCompat; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.logging.Log; import java.util.LinkedList; @@ -66,7 +67,7 @@ public class AndroidAutoHeardReceiver extends BroadcastReceiver { messageIdsCollection.addAll(messageIds); } - MessageNotifier.updateNotification(context); + ApplicationDependencies.getMessageNotifier().updateNotification(context); MarkReadReceiver.process(context, messageIdsCollection); return null; diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java index c2eac046a0..8bcb4cfb05 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/AndroidAutoReplyReceiver.java @@ -27,6 +27,7 @@ import androidx.core.app.RemoteInput; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; import org.thoughtcrime.securesms.recipients.Recipient; @@ -84,7 +85,7 @@ public class AndroidAutoReplyReceiver extends BroadcastReceiver { List messageIds = DatabaseFactory.getThreadDatabase(context).setRead(replyThreadId, true); - MessageNotifier.updateNotification(context); + ApplicationDependencies.getMessageNotifier().updateNotification(context); MarkReadReceiver.process(context, messageIds); return null; diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java new file mode 100644 index 0000000000..173d8f83b2 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java @@ -0,0 +1,737 @@ +/* + * 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 . + */ +package org.thoughtcrime.securesms.notifications; + +import android.annotation.SuppressLint; +import android.app.AlarmManager; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.media.AudioAttributes; +import android.media.AudioManager; +import android.media.Ringtone; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.Build; +import android.service.notification.StatusBarNotification; +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; + +import android.text.SpannableStringBuilder; +import android.text.TextUtils; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.contactshare.Contact; +import org.thoughtcrime.securesms.contactshare.ContactUtil; +import org.thoughtcrime.securesms.conversation.ConversationActivity; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo; +import org.thoughtcrime.securesms.database.MmsSmsColumns; +import org.thoughtcrime.securesms.database.MmsSmsDatabase; +import org.thoughtcrime.securesms.database.ThreadDatabase; +import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; +import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.database.model.MmsMessageRecord; +import org.thoughtcrime.securesms.database.model.ReactionRecord; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.mms.Slide; +import org.thoughtcrime.securesms.mms.SlideDeck; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientUtil; +import org.thoughtcrime.securesms.messages.IncomingMessageObserver; +import org.thoughtcrime.securesms.service.KeyCachingService; +import org.thoughtcrime.securesms.util.MediaUtil; +import org.thoughtcrime.securesms.util.MessageRecordUtil; +import org.thoughtcrime.securesms.util.ServiceUtil; +import org.thoughtcrime.securesms.util.SpanUtil; +import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; +import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder; +import org.whispersystems.signalservice.internal.util.Util; + +import java.util.HashSet; +import java.util.List; +import java.util.ListIterator; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import me.leolin.shortcutbadger.ShortcutBadger; + + +/** + * Handles posting system notifications for new messages. + * + * + * @author Moxie Marlinspike + */ +public class DefaultMessageNotifier implements MessageNotifier { + + private static final String TAG = DefaultMessageNotifier.class.getSimpleName(); + + public static final String EXTRA_REMOTE_REPLY = "extra_remote_reply"; + + private static final String EMOJI_REPLACEMENT_STRING = "__EMOJI__"; + private static final String NOTIFICATION_GROUP = "messages"; + private static final long MIN_AUDIBLE_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(2); + private static final long DESKTOP_ACTIVITY_PERIOD = TimeUnit.MINUTES.toMillis(1); + + private volatile long visibleThread = -1; + private volatile long lastDesktopActivityTimestamp = -1; + private volatile long lastAudibleNotification = -1; + private final CancelableExecutor executor = new CancelableExecutor(); + + @Override + public void setVisibleThread(long threadId) { + visibleThread = threadId; + } + + @Override + public void clearVisibleThread() { + setVisibleThread(-1); + } + + @Override + public void setLastDesktopActivityTimestamp(long timestamp) { + lastDesktopActivityTimestamp = timestamp; + } + + @Override + public void notifyMessageDeliveryFailed(Context context, Recipient recipient, long threadId) { + if (visibleThread == threadId) { + sendInThreadNotification(context, recipient); + } else { + Intent intent = new Intent(context, ConversationActivity.class); + intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId()); + intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId); + intent.setData((Uri.parse("custom://" + System.currentTimeMillis()))); + + FailedNotificationBuilder builder = new FailedNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context), intent); + ((NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE)) + .notify((int)threadId, builder.build()); + } + } + + @Override + public void cancelDelayedNotifications() { + executor.cancel(); + } + + private static void cancelActiveNotifications(@NonNull Context context) { + NotificationManager notifications = ServiceUtil.getNotificationManager(context); + notifications.cancel(NotificationIds.MESSAGE_SUMMARY); + + if (Build.VERSION.SDK_INT >= 23) { + try { + StatusBarNotification[] activeNotifications = notifications.getActiveNotifications(); + + for (StatusBarNotification activeNotification : activeNotifications) { + if (!CallNotificationBuilder.isWebRtcNotification(activeNotification.getId())) { + notifications.cancel(activeNotification.getId()); + } + } + } catch (Throwable e) { + // XXX Appears to be a ROM bug, see #6043 + Log.w(TAG, e); + notifications.cancelAll(); + } + } + } + + private static boolean isDisplayingSummaryNotification(@NonNull Context context) { + if (Build.VERSION.SDK_INT >= 23) { + try { + NotificationManager notificationManager = ServiceUtil.getNotificationManager(context); + StatusBarNotification[] activeNotifications = notificationManager.getActiveNotifications(); + + for (StatusBarNotification activeNotification : activeNotifications) { + if (activeNotification.getId() == NotificationIds.MESSAGE_SUMMARY) { + return true; + } + } + + return false; + + } catch (Throwable e) { + // XXX Android ROM Bug, see #6043 + Log.w(TAG, e); + return false; + } + } else { + return false; + } + } + + private static void cancelOrphanedNotifications(@NonNull Context context, NotificationState notificationState) { + if (Build.VERSION.SDK_INT >= 23) { + try { + NotificationManager notifications = ServiceUtil.getNotificationManager(context); + StatusBarNotification[] activeNotifications = notifications.getActiveNotifications(); + + for (StatusBarNotification notification : activeNotifications) { + boolean validNotification = false; + + if (notification.getId() != NotificationIds.MESSAGE_SUMMARY && + notification.getId() != KeyCachingService.SERVICE_RUNNING_ID && + notification.getId() != IncomingMessageObserver.FOREGROUND_ID && + notification.getId() != NotificationIds.PENDING_MESSAGES && + !CallNotificationBuilder.isWebRtcNotification(notification.getId())) + { + for (NotificationItem item : notificationState.getNotifications()) { + if (notification.getId() == NotificationIds.getNotificationIdForThread(item.getThreadId())) { + validNotification = true; + break; + } + } + + if (!validNotification) { + notifications.cancel(notification.getId()); + } + } + } + } catch (Throwable e) { + // XXX Android ROM Bug, see #6043 + Log.w(TAG, e); + } + } + } + + @Override + public void updateNotification(@NonNull Context context) { + if (!TextSecurePreferences.isNotificationsEnabled(context)) { + return; + } + + updateNotification(context, -1, false, 0); + } + + @Override + public void updateNotification(@NonNull Context context, long threadId) + { + if (System.currentTimeMillis() - lastDesktopActivityTimestamp < DESKTOP_ACTIVITY_PERIOD) { + Log.i(TAG, "Scheduling delayed notification..."); + executor.execute(new DelayedNotification(context, threadId)); + } else { + updateNotification(context, threadId, true); + } + } + + @Override + public void updateNotification(@NonNull Context context, + long threadId, + boolean signal) + { + boolean isVisible = visibleThread == threadId; + + ThreadDatabase threads = DatabaseFactory.getThreadDatabase(context); + Recipient recipients = DatabaseFactory.getThreadDatabase(context) + .getRecipientForThreadId(threadId); + + if (isVisible) { + List messageIds = threads.setRead(threadId, false); + MarkReadReceiver.process(context, messageIds); + } + + if (!TextSecurePreferences.isNotificationsEnabled(context) || + (recipients != null && recipients.isMuted())) + { + return; + } + + if (isVisible) { + sendInThreadNotification(context, threads.getRecipientForThreadId(threadId)); + } else { + updateNotification(context, threadId, signal, 0); + } + } + + @Override + public void updateNotification(@NonNull Context context, + long targetThread, + boolean signal, + int reminderCount) + { + Cursor telcoCursor = null; + Cursor pushCursor = null; + + try { + telcoCursor = DatabaseFactory.getMmsSmsDatabase(context).getUnread(); + pushCursor = DatabaseFactory.getPushDatabase(context).getPending(); + + if ((telcoCursor == null || telcoCursor.isAfterLast()) && + (pushCursor == null || pushCursor.isAfterLast())) + { + cancelActiveNotifications(context); + updateBadge(context, 0); + clearReminder(context); + return; + } + + NotificationState notificationState = constructNotificationState(context, telcoCursor); + + if (signal && (System.currentTimeMillis() - lastAudibleNotification) < MIN_AUDIBLE_PERIOD_MILLIS) { + signal = false; + } else if (signal) { + lastAudibleNotification = System.currentTimeMillis(); + } + + if (notificationState.hasMultipleThreads()) { + if (Build.VERSION.SDK_INT >= 23) { + for (long threadId : notificationState.getThreads()) { + if (targetThread < 1 || targetThread == threadId) { + sendSingleThreadNotification(context, + new NotificationState(notificationState.getNotificationsForThread(threadId)), + signal && (threadId == targetThread), + true); + } + } + } + + sendMultipleThreadNotification(context, notificationState, signal && (Build.VERSION.SDK_INT < 23)); + } else { + sendSingleThreadNotification(context, notificationState, signal, false); + + if (isDisplayingSummaryNotification(context)) { + sendMultipleThreadNotification(context, notificationState, false); + } + } + + cancelOrphanedNotifications(context, notificationState); + updateBadge(context, notificationState.getMessageCount()); + + if (signal) { + scheduleReminder(context, reminderCount); + } + } finally { + if (telcoCursor != null) telcoCursor.close(); + if (pushCursor != null) pushCursor.close(); + } + } + + private static void sendSingleThreadNotification(@NonNull Context context, + @NonNull NotificationState notificationState, + boolean signal, + boolean bundled) + { + Log.i(TAG, "sendSingleThreadNotification() signal: " + signal + " bundled: " + bundled); + + if (notificationState.getNotifications().isEmpty()) { + if (!bundled) cancelActiveNotifications(context); + Log.i(TAG, "[sendSingleThreadNotification] Empty notification state. Skipping."); + return; + } + + SingleRecipientNotificationBuilder builder = new SingleRecipientNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context)); + List notifications = notificationState.getNotifications(); + Recipient recipient = notifications.get(0).getRecipient(); + int notificationId; + + if (Build.VERSION.SDK_INT >= 23) { + notificationId = NotificationIds.getNotificationIdForThread(notifications.get(0).getThreadId()); + } else { + notificationId = NotificationIds.MESSAGE_SUMMARY; + } + + builder.setThread(notifications.get(0).getRecipient()); + builder.setMessageCount(notificationState.getMessageCount()); + builder.setPrimaryMessageBody(recipient, notifications.get(0).getIndividualRecipient(), + notifications.get(0).getText(), notifications.get(0).getSlideDeck()); + builder.setContentIntent(notifications.get(0).getPendingIntent(context)); + builder.setDeleteIntent(notificationState.getDeleteIntent(context)); + builder.setOnlyAlertOnce(!signal); + builder.setSortKey(String.valueOf(Long.MAX_VALUE - notifications.get(0).getTimestamp())); + + long timestamp = notifications.get(0).getTimestamp(); + if (timestamp != 0) builder.setWhen(timestamp); + + if (!KeyCachingService.isLocked(context) && RecipientUtil.isMessageRequestAccepted(context, recipient.resolve())) { + ReplyMethod replyMethod = ReplyMethod.forRecipient(context, recipient); + + builder.addActions(notificationState.getMarkAsReadIntent(context, notificationId), + notificationState.getQuickReplyIntent(context, notifications.get(0).getRecipient()), + notificationState.getRemoteReplyIntent(context, notifications.get(0).getRecipient(), replyMethod), + replyMethod); + + builder.addAndroidAutoAction(notificationState.getAndroidAutoReplyIntent(context, notifications.get(0).getRecipient()), + notificationState.getAndroidAutoHeardIntent(context, notificationId), notifications.get(0).getTimestamp()); + } + + ListIterator iterator = notifications.listIterator(notifications.size()); + + while(iterator.hasPrevious()) { + NotificationItem item = iterator.previous(); + builder.addMessageBody(item.getRecipient(), item.getIndividualRecipient(), item.getText(), item.getTimestamp()); + } + + if (signal) { + builder.setAlarms(notificationState.getRingtone(context), notificationState.getVibrate()); + builder.setTicker(notifications.get(0).getIndividualRecipient(), + notifications.get(0).getText()); + } + + if (Build.VERSION.SDK_INT >= 23) { + builder.setGroup(NOTIFICATION_GROUP); + builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN); + } + + Notification notification = builder.build(); + NotificationManagerCompat.from(context).notify(notificationId, notification); + Log.i(TAG, "Posted notification. " + notification.toString()); + } + + private static void sendMultipleThreadNotification(@NonNull Context context, + @NonNull NotificationState notificationState, + boolean signal) + { + Log.i(TAG, "sendMultiThreadNotification() signal: " + signal); + + if (notificationState.getNotifications().isEmpty()) { + Log.i(TAG, "[sendMultiThreadNotification] Empty notification state. Skipping."); + return; + } + + MultipleRecipientNotificationBuilder builder = new MultipleRecipientNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context)); + List notifications = notificationState.getNotifications(); + + builder.setMessageCount(notificationState.getMessageCount(), notificationState.getThreadCount()); + builder.setMostRecentSender(notifications.get(0).getIndividualRecipient()); + builder.setDeleteIntent(notificationState.getDeleteIntent(context)); + builder.setOnlyAlertOnce(!signal); + + if (Build.VERSION.SDK_INT >= 23) { + builder.setGroup(NOTIFICATION_GROUP); + builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN); + } + + long timestamp = notifications.get(0).getTimestamp(); + if (timestamp != 0) builder.setWhen(timestamp); + + builder.addActions(notificationState.getMarkAsReadIntent(context, NotificationIds.MESSAGE_SUMMARY)); + + ListIterator iterator = notifications.listIterator(notifications.size()); + + while(iterator.hasPrevious()) { + NotificationItem item = iterator.previous(); + builder.addMessageBody(item.getIndividualRecipient(), item.getText()); + } + + if (signal) { + builder.setAlarms(notificationState.getRingtone(context), notificationState.getVibrate()); + builder.setTicker(notifications.get(0).getIndividualRecipient(), + notifications.get(0).getText()); + } + + Notification notification = builder.build(); + NotificationManagerCompat.from(context).notify(NotificationIds.MESSAGE_SUMMARY, builder.build()); + Log.i(TAG, "Posted notification. " + notification.toString()); + } + + private static void sendInThreadNotification(Context context, Recipient recipient) { + if (!TextSecurePreferences.isInThreadNotifications(context) || + ServiceUtil.getAudioManager(context).getRingerMode() != AudioManager.RINGER_MODE_NORMAL) + { + return; + } + + Uri uri = null; + if (recipient != null) { + uri = NotificationChannels.supported() ? NotificationChannels.getMessageRingtone(context, recipient) : recipient.getMessageRingtone(); + } + + if (uri == null) { + uri = NotificationChannels.supported() ? NotificationChannels.getMessageRingtone(context) : TextSecurePreferences.getNotificationRingtone(context); + } + + if (uri.toString().isEmpty()) { + Log.d(TAG, "ringtone uri is empty"); + return; + } + + Ringtone ringtone = RingtoneManager.getRingtone(context, uri); + + if (ringtone == null) { + Log.w(TAG, "ringtone is null"); + return; + } + + if (Build.VERSION.SDK_INT >= 21) { + ringtone.setAudioAttributes(new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN) + .setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT) + .build()); + } else { + ringtone.setStreamType(AudioManager.STREAM_NOTIFICATION); + } + + ringtone.play(); + } + + private static NotificationState constructNotificationState(@NonNull Context context, + @NonNull Cursor cursor) + { + NotificationState notificationState = new NotificationState(); + MmsSmsDatabase.Reader reader = DatabaseFactory.getMmsSmsDatabase(context).readerFor(cursor); + + MessageRecord record; + + while ((record = reader.getNext()) != null) { + long id = record.getId(); + boolean mms = record.isMms() || record.isMmsNotification(); + Recipient recipient = record.getIndividualRecipient().resolve(); + Recipient conversationRecipient = record.getRecipient().resolve(); + long threadId = record.getThreadId(); + CharSequence body = record.getDisplayBody(context); + Recipient threadRecipients = null; + SlideDeck slideDeck = null; + long timestamp = record.getTimestamp(); + long receivedTimestamp = record.getDateReceived(); + boolean isUnreadMessage = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.READ)) == 0; + boolean hasUnreadReactions = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.REACTIONS_UNREAD)) == 1; + long lastReactionRead = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.REACTIONS_LAST_SEEN)); + + if (threadId != -1) { + threadRecipients = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId); + } + + if (isUnreadMessage) { + if (KeyCachingService.isLocked(context)) { + body = SpanUtil.italic(context.getString(R.string.MessageNotifier_locked_message)); + } else if (record.isMms() && !((MmsMessageRecord) record).getSharedContacts().isEmpty()) { + Contact contact = ((MmsMessageRecord) record).getSharedContacts().get(0); + body = ContactUtil.getStringSummary(context, contact); + } else if (record.isMms() && ((MmsMessageRecord) record).getSlideDeck().getStickerSlide() != null) { + body = SpanUtil.italic(context.getString(R.string.MessageNotifier_sticker)); + slideDeck = ((MmsMessageRecord) record).getSlideDeck(); + } else if (record.isMms() && ((MmsMessageRecord) record).isViewOnce()) { + body = SpanUtil.italic(context.getString(getViewOnceDescription((MmsMessageRecord) record))); + } else if (record.isRemoteDelete()) { + body = SpanUtil.italic(context.getString(R.string.MessageNotifier_this_message_was_deleted));; + } else if (record.isMms() && TextUtils.isEmpty(body) && !((MmsMessageRecord) record).getSlideDeck().getSlides().isEmpty()) { + body = SpanUtil.italic(context.getString(R.string.MessageNotifier_media_message)); + slideDeck = ((MediaMmsMessageRecord) record).getSlideDeck(); + } else if (record.isMms() && !record.isMmsNotification() && !((MmsMessageRecord) record).getSlideDeck().getSlides().isEmpty()) { + String message = context.getString(R.string.MessageNotifier_media_message_with_text, body); + int italicLength = message.length() - body.length(); + body = SpanUtil.italic(message, italicLength); + slideDeck = ((MediaMmsMessageRecord) record).getSlideDeck(); + } + + if (threadRecipients == null || !threadRecipients.isMuted()) { + notificationState.addNotification(new NotificationItem(id, mms, recipient, conversationRecipient, threadRecipients, threadId, body, timestamp, receivedTimestamp, slideDeck, false)); + } + } + + if (hasUnreadReactions) { + for (ReactionRecord reaction : record.getReactions()) { + Recipient reactionSender = Recipient.resolved(reaction.getAuthor()); + if (reactionSender.equals(Recipient.self()) || !record.isOutgoing() || reaction.getDateReceived() <= lastReactionRead) { + continue; + } + + if (KeyCachingService.isLocked(context)) { + body = SpanUtil.italic(context.getString(R.string.MessageNotifier_locked_message)); + } else { + String text = SpanUtil.italic(getReactionMessageBody(context, record)).toString(); + String[] parts = text.split(EMOJI_REPLACEMENT_STRING); + + SpannableStringBuilder builder = new SpannableStringBuilder(); + for (int i = 0; i < parts.length; i++) { + builder.append(SpanUtil.italic(parts[i])); + + if (i != parts.length -1) { + builder.append(reaction.getEmoji()); + } + } + + if (text.endsWith(EMOJI_REPLACEMENT_STRING)) { + builder.append(reaction.getEmoji()); + } + + body = builder; + } + + if (threadRecipients == null || !threadRecipients.isMuted()) { + notificationState.addNotification(new NotificationItem(id, mms, reactionSender, conversationRecipient, threadRecipients, threadId, body, reaction.getDateReceived(), receivedTimestamp, null, true)); + } + } + } + } + + reader.close(); + return notificationState; + } + + private static CharSequence getReactionMessageBody(@NonNull Context context, @NonNull MessageRecord record) { + CharSequence body = record.getDisplayBody(context); + boolean bodyIsEmpty = TextUtils.isEmpty(body); + + if (MessageRecordUtil.hasSharedContact(record)) { + Contact contact = ((MmsMessageRecord) record).getSharedContacts().get(0); + CharSequence summary = ContactUtil.getStringSummary(context, contact); + + return context.getString(R.string.MessageNotifier_reacted_s_to_s, EMOJI_REPLACEMENT_STRING, summary); + } else if (MessageRecordUtil.hasSticker(record)) { + return context.getString(R.string.MessageNotifier_reacted_s_to_your_sticker, EMOJI_REPLACEMENT_STRING); + } else if (record.isMms() && record.isViewOnce() && MediaUtil.isVideoType(getMessageContentType((MmsMessageRecord) record))) { + return context.getString(R.string.MessageNotifier_reacted_s_to_your_view_once_video, EMOJI_REPLACEMENT_STRING); + } else if (record.isMms() && record.isViewOnce()){ + return context.getString(R.string.MessageNotifier_reacted_s_to_your_view_once_photo, EMOJI_REPLACEMENT_STRING); + } else if (!bodyIsEmpty) { + return context.getString(R.string.MessageNotifier_reacted_s_to_s, EMOJI_REPLACEMENT_STRING, body); + } else if (MessageRecordUtil.isMediaMessage(record) && MediaUtil.isVideoType(getMessageContentType((MmsMessageRecord) record))) { + return context.getString(R.string.MessageNotifier_reacted_s_to_your_video, EMOJI_REPLACEMENT_STRING); + } else if (MessageRecordUtil.isMediaMessage(record) && MediaUtil.isImageType(getMessageContentType((MmsMessageRecord) record))) { + return context.getString(R.string.MessageNotifier_reacted_s_to_your_image, EMOJI_REPLACEMENT_STRING); + } else if (MessageRecordUtil.isMediaMessage(record) && MediaUtil.isAudioType(getMessageContentType((MmsMessageRecord) record))) { + return context.getString(R.string.MessageNotifier_reacted_s_to_your_audio, EMOJI_REPLACEMENT_STRING); + } else if (MessageRecordUtil.isMediaMessage(record)) { + return context.getString(R.string.MessageNotifier_reacted_s_to_your_file, EMOJI_REPLACEMENT_STRING); + } else { + return context.getString(R.string.MessageNotifier_reacted_s_to_s, EMOJI_REPLACEMENT_STRING, body); + } + } + + private static @StringRes int getViewOnceDescription(@NonNull MmsMessageRecord messageRecord) { + final String contentType = getMessageContentType(messageRecord); + + if (MediaUtil.isImageType(contentType)) { + return R.string.MessageNotifier_view_once_photo; + } + return R.string.MessageNotifier_view_once_video; + } + + private static String getMessageContentType(@NonNull MmsMessageRecord messageRecord) { + Slide thumbnailSlide = messageRecord.getSlideDeck().getThumbnailSlide(); + if (thumbnailSlide == null) { + Log.w(TAG, "Could not distinguish view-once content type from message record, defaulting to JPEG"); + return MediaUtil.IMAGE_JPEG; + } + return thumbnailSlide.getContentType(); + } + + private static void updateBadge(Context context, int count) { + try { + if (count == 0) ShortcutBadger.removeCount(context); + else ShortcutBadger.applyCount(context, count); + } catch (Throwable t) { + // NOTE :: I don't totally trust this thing, so I'm catching + // everything. + Log.w(TAG, t); + } + } + + private static void scheduleReminder(Context context, int count) { + if (count >= TextSecurePreferences.getRepeatAlertsCount(context)) { + return; + } + + AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + Intent alarmIntent = new Intent(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); + } + + @Override + public void clearReminder(@NonNull 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); + } + + private static class DelayedNotification implements Runnable { + + private static final long DELAY = TimeUnit.SECONDS.toMillis(5); + + private final AtomicBoolean canceled = new AtomicBoolean(false); + + private final Context context; + private final long threadId; + private final long delayUntil; + + private DelayedNotification(Context context, long threadId) { + this.context = context; + this.threadId = threadId; + this.delayUntil = System.currentTimeMillis() + DELAY; + } + + @Override + public void run() { + long delayMillis = delayUntil - System.currentTimeMillis(); + Log.i(TAG, "Waiting to notify: " + delayMillis); + + if (delayMillis > 0) { + Util.sleep(delayMillis); + } + + if (!canceled.get()) { + Log.i(TAG, "Not canceled, notifying..."); + ApplicationDependencies.getMessageNotifier().updateNotification(context, threadId, true); + ApplicationDependencies.getMessageNotifier().cancelDelayedNotifications(); + } else { + Log.w(TAG, "Canceled, not notifying..."); + } + } + + public void cancel() { + canceled.set(true); + } + } + + private static class CancelableExecutor { + + private final Executor executor = Executors.newSingleThreadExecutor(); + private final Set tasks = new HashSet<>(); + + public void execute(final DelayedNotification runnable) { + synchronized (tasks) { + tasks.add(runnable); + } + + Runnable wrapper = () -> { + runnable.run(); + + synchronized (tasks) { + tasks.remove(runnable); + } + }; + + executor.execute(wrapper); + } + + public void cancel() { + synchronized (tasks) { + for (DelayedNotification task : tasks) { + task.cancel(); + } + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.java index be541f2add..7d961ea434 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.java @@ -7,6 +7,7 @@ import android.content.Intent; import android.os.AsyncTask; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; public class DeleteNotificationReceiver extends BroadcastReceiver { @@ -18,7 +19,7 @@ public class DeleteNotificationReceiver extends BroadcastReceiver { @Override public void onReceive(final Context context, Intent intent) { if (DELETE_NOTIFICATION_ACTION.equals(intent.getAction())) { - MessageNotifier.clearReminder(context); + ApplicationDependencies.getMessageNotifier().clearReminder(context); final long[] ids = intent.getLongArrayExtra(EXTRA_IDS); final boolean[] mms = intent.getBooleanArrayExtra(EXTRA_MMS); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java index 4503effff5..cbc4f6777a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java @@ -59,7 +59,7 @@ public class MarkReadReceiver extends BroadcastReceiver { process(context, messageIdsCollection); - MessageNotifier.updateNotification(context); + ApplicationDependencies.getMessageNotifier().updateNotification(context); return null; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/MessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/MessageNotifier.java index 93f1bfc4ef..16d59410a2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/MessageNotifier.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/MessageNotifier.java @@ -1,759 +1,40 @@ -/* - * 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 . - */ package org.thoughtcrime.securesms.notifications; import android.annotation.SuppressLint; -import android.app.AlarmManager; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.database.Cursor; -import android.media.AudioAttributes; -import android.media.AudioManager; -import android.media.Ringtone; -import android.media.RingtoneManager; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Build; -import android.service.notification.StatusBarNotification; + import androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; -import android.text.SpannableStringBuilder; -import android.text.TextUtils; - -import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.contactshare.Contact; -import org.thoughtcrime.securesms.contactshare.ContactUtil; -import org.thoughtcrime.securesms.conversation.ConversationActivity; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo; -import org.thoughtcrime.securesms.database.MmsSmsColumns; -import org.thoughtcrime.securesms.database.MmsSmsDatabase; -import org.thoughtcrime.securesms.database.ThreadDatabase; -import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; -import org.thoughtcrime.securesms.database.model.MessageRecord; -import org.thoughtcrime.securesms.database.model.MmsMessageRecord; -import org.thoughtcrime.securesms.database.model.ReactionRecord; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.logging.Log; -import org.thoughtcrime.securesms.mms.Slide; -import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.recipients.RecipientUtil; -import org.thoughtcrime.securesms.messages.IncomingMessageObserver; -import org.thoughtcrime.securesms.service.KeyCachingService; -import org.thoughtcrime.securesms.util.MediaUtil; -import org.thoughtcrime.securesms.util.MessageRecordUtil; -import org.thoughtcrime.securesms.util.ServiceUtil; -import org.thoughtcrime.securesms.util.SpanUtil; -import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder; -import org.whispersystems.signalservice.internal.util.Util; - -import java.util.HashSet; -import java.util.List; -import java.util.ListIterator; -import java.util.Set; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -import me.leolin.shortcutbadger.ShortcutBadger; - - -/** - * Handles posting system notifications for new messages. - * - * - * @author Moxie Marlinspike - */ - -public class MessageNotifier { - - private static final String TAG = MessageNotifier.class.getSimpleName(); - - public static final String EXTRA_REMOTE_REPLY = "extra_remote_reply"; - - private static final String EMOJI_REPLACEMENT_STRING = "__EMOJI__"; - private static final String NOTIFICATION_GROUP = "messages"; - private static final long MIN_AUDIBLE_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(2); - private static final long DESKTOP_ACTIVITY_PERIOD = TimeUnit.MINUTES.toMillis(1); - - private volatile static long visibleThread = -1; - private volatile static long lastDesktopActivityTimestamp = -1; - private volatile static long lastAudibleNotification = -1; - private static final CancelableExecutor executor = new CancelableExecutor(); - - public static void setVisibleThread(long threadId) { - visibleThread = threadId; - } - - public static void setLastDesktopActivityTimestamp(long timestamp) { - lastDesktopActivityTimestamp = timestamp; - } - - public static void notifyMessageDeliveryFailed(Context context, Recipient recipient, long threadId) { - if (visibleThread == threadId) { - sendInThreadNotification(context, recipient); - } else { - Intent intent = new Intent(context, ConversationActivity.class); - intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId()); - intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId); - intent.setData((Uri.parse("custom://" + System.currentTimeMillis()))); - - FailedNotificationBuilder builder = new FailedNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context), intent); - ((NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE)) - .notify((int)threadId, builder.build()); - } - } - - public static void notifyMessagesPending(Context context) { - if (!TextSecurePreferences.isNotificationsEnabled(context) || ApplicationContext.getInstance(context).isAppVisible()) { - return; - } - - PendingMessageNotificationBuilder builder = new PendingMessageNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context)); - ServiceUtil.getNotificationManager(context).notify(NotificationIds.PENDING_MESSAGES, builder.build()); - } - - public static void cancelMessagesPending(Context context) { - ServiceUtil.getNotificationManager(context).cancel(NotificationIds.PENDING_MESSAGES); - } - - public static void cancelDelayedNotifications() { - executor.cancel(); - } - - private static void cancelActiveNotifications(@NonNull Context context) { - NotificationManager notifications = ServiceUtil.getNotificationManager(context); - notifications.cancel(NotificationIds.MESSAGE_SUMMARY); - - if (Build.VERSION.SDK_INT >= 23) { - try { - StatusBarNotification[] activeNotifications = notifications.getActiveNotifications(); - - for (StatusBarNotification activeNotification : activeNotifications) { - if (!CallNotificationBuilder.isWebRtcNotification(activeNotification.getId())) { - notifications.cancel(activeNotification.getId()); - } - } - } catch (Throwable e) { - // XXX Appears to be a ROM bug, see #6043 - Log.w(TAG, e); - notifications.cancelAll(); - } - } - } - - private static boolean isDisplayingSummaryNotification(@NonNull Context context) { - if (Build.VERSION.SDK_INT >= 23) { - try { - NotificationManager notificationManager = ServiceUtil.getNotificationManager(context); - StatusBarNotification[] activeNotifications = notificationManager.getActiveNotifications(); - - for (StatusBarNotification activeNotification : activeNotifications) { - if (activeNotification.getId() == NotificationIds.MESSAGE_SUMMARY) { - return true; - } - } - - return false; - - } catch (Throwable e) { - // XXX Android ROM Bug, see #6043 - Log.w(TAG, e); - return false; - } - } else { - return false; - } - } - - private static void cancelOrphanedNotifications(@NonNull Context context, NotificationState notificationState) { - if (Build.VERSION.SDK_INT >= 23) { - try { - NotificationManager notifications = ServiceUtil.getNotificationManager(context); - StatusBarNotification[] activeNotifications = notifications.getActiveNotifications(); - - for (StatusBarNotification notification : activeNotifications) { - boolean validNotification = false; - - if (notification.getId() != NotificationIds.MESSAGE_SUMMARY && - notification.getId() != KeyCachingService.SERVICE_RUNNING_ID && - notification.getId() != IncomingMessageObserver.FOREGROUND_ID && - notification.getId() != NotificationIds.PENDING_MESSAGES && - !CallNotificationBuilder.isWebRtcNotification(notification.getId())) - { - for (NotificationItem item : notificationState.getNotifications()) { - if (notification.getId() == NotificationIds.getNotificationIdForThread(item.getThreadId())) { - validNotification = true; - break; - } - } - - if (!validNotification) { - notifications.cancel(notification.getId()); - } - } - } - } catch (Throwable e) { - // XXX Android ROM Bug, see #6043 - Log.w(TAG, e); - } - } - } - - public static void updateNotification(@NonNull Context context) { - if (!TextSecurePreferences.isNotificationsEnabled(context)) { - return; - } - - updateNotification(context, -1, false, 0); - } - - public static void updateNotification(@NonNull Context context, long threadId) - { - if (System.currentTimeMillis() - lastDesktopActivityTimestamp < DESKTOP_ACTIVITY_PERIOD) { - Log.i(TAG, "Scheduling delayed notification..."); - executor.execute(new DelayedNotification(context, threadId)); - } else { - updateNotification(context, threadId, true); - } - } - - public static void updateNotification(@NonNull Context context, - long threadId, - boolean signal) - { - boolean isVisible = visibleThread == threadId; - - ThreadDatabase threads = DatabaseFactory.getThreadDatabase(context); - Recipient recipients = DatabaseFactory.getThreadDatabase(context) - .getRecipientForThreadId(threadId); - - if (isVisible) { - List messageIds = threads.setRead(threadId, false); - MarkReadReceiver.process(context, messageIds); - } - - if (!TextSecurePreferences.isNotificationsEnabled(context) || - (recipients != null && recipients.isMuted())) - { - return; - } - - if (isVisible) { - sendInThreadNotification(context, threads.getRecipientForThreadId(threadId)); - } else { - updateNotification(context, threadId, signal, 0); - } - } - - private static void updateNotification(@NonNull Context context, - long targetThread, - boolean signal, - int reminderCount) - { - Cursor telcoCursor = null; - Cursor pushCursor = null; - - try { - telcoCursor = DatabaseFactory.getMmsSmsDatabase(context).getUnread(); - pushCursor = DatabaseFactory.getPushDatabase(context).getPending(); - - if ((telcoCursor == null || telcoCursor.isAfterLast()) && - (pushCursor == null || pushCursor.isAfterLast())) - { - cancelActiveNotifications(context); - updateBadge(context, 0); - clearReminder(context); - return; - } - - NotificationState notificationState = constructNotificationState(context, telcoCursor); - - if (signal && (System.currentTimeMillis() - lastAudibleNotification) < MIN_AUDIBLE_PERIOD_MILLIS) { - signal = false; - } else if (signal) { - lastAudibleNotification = System.currentTimeMillis(); - } - - if (notificationState.hasMultipleThreads()) { - if (Build.VERSION.SDK_INT >= 23) { - for (long threadId : notificationState.getThreads()) { - if (targetThread < 1 || targetThread == threadId) { - sendSingleThreadNotification(context, - new NotificationState(notificationState.getNotificationsForThread(threadId)), - signal && (threadId == targetThread), - true); - } - } - } - - sendMultipleThreadNotification(context, notificationState, signal && (Build.VERSION.SDK_INT < 23)); - } else { - sendSingleThreadNotification(context, notificationState, signal, false); - - if (isDisplayingSummaryNotification(context)) { - sendMultipleThreadNotification(context, notificationState, false); - } - } - - cancelOrphanedNotifications(context, notificationState); - updateBadge(context, notificationState.getMessageCount()); - - if (signal) { - scheduleReminder(context, reminderCount); - } - } finally { - if (telcoCursor != null) telcoCursor.close(); - if (pushCursor != null) pushCursor.close(); - } - } - - private static void sendSingleThreadNotification(@NonNull Context context, - @NonNull NotificationState notificationState, - boolean signal, - boolean bundled) - { - Log.i(TAG, "sendSingleThreadNotification() signal: " + signal + " bundled: " + bundled); - - if (notificationState.getNotifications().isEmpty()) { - if (!bundled) cancelActiveNotifications(context); - Log.i(TAG, "[sendSingleThreadNotification] Empty notification state. Skipping."); - return; - } - - SingleRecipientNotificationBuilder builder = new SingleRecipientNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context)); - List notifications = notificationState.getNotifications(); - Recipient recipient = notifications.get(0).getRecipient(); - int notificationId; - - if (Build.VERSION.SDK_INT >= 23) { - notificationId = NotificationIds.getNotificationIdForThread(notifications.get(0).getThreadId()); - } else { - notificationId = NotificationIds.MESSAGE_SUMMARY; - } - - builder.setThread(notifications.get(0).getRecipient()); - builder.setMessageCount(notificationState.getMessageCount()); - builder.setPrimaryMessageBody(recipient, notifications.get(0).getIndividualRecipient(), - notifications.get(0).getText(), notifications.get(0).getSlideDeck()); - builder.setContentIntent(notifications.get(0).getPendingIntent(context)); - builder.setDeleteIntent(notificationState.getDeleteIntent(context)); - builder.setOnlyAlertOnce(!signal); - builder.setSortKey(String.valueOf(Long.MAX_VALUE - notifications.get(0).getTimestamp())); - - long timestamp = notifications.get(0).getTimestamp(); - if (timestamp != 0) builder.setWhen(timestamp); - - if (!KeyCachingService.isLocked(context) && RecipientUtil.isMessageRequestAccepted(context, recipient.resolve())) { - ReplyMethod replyMethod = ReplyMethod.forRecipient(context, recipient); - - builder.addActions(notificationState.getMarkAsReadIntent(context, notificationId), - notificationState.getQuickReplyIntent(context, notifications.get(0).getRecipient()), - notificationState.getRemoteReplyIntent(context, notifications.get(0).getRecipient(), replyMethod), - replyMethod); - - builder.addAndroidAutoAction(notificationState.getAndroidAutoReplyIntent(context, notifications.get(0).getRecipient()), - notificationState.getAndroidAutoHeardIntent(context, notificationId), notifications.get(0).getTimestamp()); - } - - ListIterator iterator = notifications.listIterator(notifications.size()); - - while(iterator.hasPrevious()) { - NotificationItem item = iterator.previous(); - builder.addMessageBody(item.getRecipient(), item.getIndividualRecipient(), item.getText(), item.getTimestamp()); - } - - if (signal) { - builder.setAlarms(notificationState.getRingtone(context), notificationState.getVibrate()); - builder.setTicker(notifications.get(0).getIndividualRecipient(), - notifications.get(0).getText()); - } - - if (Build.VERSION.SDK_INT >= 23) { - builder.setGroup(NOTIFICATION_GROUP); - builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN); - } - - Notification notification = builder.build(); - NotificationManagerCompat.from(context).notify(notificationId, notification); - Log.i(TAG, "Posted notification. " + notification.toString()); - } - - private static void sendMultipleThreadNotification(@NonNull Context context, - @NonNull NotificationState notificationState, - boolean signal) - { - Log.i(TAG, "sendMultiThreadNotification() signal: " + signal); - - if (notificationState.getNotifications().isEmpty()) { - Log.i(TAG, "[sendMultiThreadNotification] Empty notification state. Skipping."); - return; - } - - MultipleRecipientNotificationBuilder builder = new MultipleRecipientNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context)); - List notifications = notificationState.getNotifications(); - - builder.setMessageCount(notificationState.getMessageCount(), notificationState.getThreadCount()); - builder.setMostRecentSender(notifications.get(0).getIndividualRecipient()); - builder.setDeleteIntent(notificationState.getDeleteIntent(context)); - builder.setOnlyAlertOnce(!signal); - - if (Build.VERSION.SDK_INT >= 23) { - builder.setGroup(NOTIFICATION_GROUP); - builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN); - } - - long timestamp = notifications.get(0).getTimestamp(); - if (timestamp != 0) builder.setWhen(timestamp); - - builder.addActions(notificationState.getMarkAsReadIntent(context, NotificationIds.MESSAGE_SUMMARY)); - - ListIterator iterator = notifications.listIterator(notifications.size()); - - while(iterator.hasPrevious()) { - NotificationItem item = iterator.previous(); - builder.addMessageBody(item.getIndividualRecipient(), item.getText()); - } - - if (signal) { - builder.setAlarms(notificationState.getRingtone(context), notificationState.getVibrate()); - builder.setTicker(notifications.get(0).getIndividualRecipient(), - notifications.get(0).getText()); - } - - Notification notification = builder.build(); - NotificationManagerCompat.from(context).notify(NotificationIds.MESSAGE_SUMMARY, builder.build()); - Log.i(TAG, "Posted notification. " + notification.toString()); - } - - private static void sendInThreadNotification(Context context, Recipient recipient) { - if (!TextSecurePreferences.isInThreadNotifications(context) || - ServiceUtil.getAudioManager(context).getRingerMode() != AudioManager.RINGER_MODE_NORMAL) - { - return; - } - - Uri uri = null; - if (recipient != null) { - uri = NotificationChannels.supported() ? NotificationChannels.getMessageRingtone(context, recipient) : recipient.getMessageRingtone(); - } - - if (uri == null) { - uri = NotificationChannels.supported() ? NotificationChannels.getMessageRingtone(context) : TextSecurePreferences.getNotificationRingtone(context); - } - - if (uri.toString().isEmpty()) { - Log.d(TAG, "ringtone uri is empty"); - return; - } - - Ringtone ringtone = RingtoneManager.getRingtone(context, uri); - - if (ringtone == null) { - Log.w(TAG, "ringtone is null"); - return; - } - - if (Build.VERSION.SDK_INT >= 21) { - ringtone.setAudioAttributes(new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN) - .setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT) - .build()); - } else { - ringtone.setStreamType(AudioManager.STREAM_NOTIFICATION); - } - - ringtone.play(); - } - - private static NotificationState constructNotificationState(@NonNull Context context, - @NonNull Cursor cursor) - { - NotificationState notificationState = new NotificationState(); - MmsSmsDatabase.Reader reader = DatabaseFactory.getMmsSmsDatabase(context).readerFor(cursor); - - MessageRecord record; - - while ((record = reader.getNext()) != null) { - long id = record.getId(); - boolean mms = record.isMms() || record.isMmsNotification(); - Recipient recipient = record.getIndividualRecipient().resolve(); - Recipient conversationRecipient = record.getRecipient().resolve(); - long threadId = record.getThreadId(); - CharSequence body = record.getDisplayBody(context); - Recipient threadRecipients = null; - SlideDeck slideDeck = null; - long timestamp = record.getTimestamp(); - long receivedTimestamp = record.getDateReceived(); - boolean isUnreadMessage = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.READ)) == 0; - boolean hasUnreadReactions = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.REACTIONS_UNREAD)) == 1; - long lastReactionRead = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.REACTIONS_LAST_SEEN)); - - if (threadId != -1) { - threadRecipients = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId); - } - - if (isUnreadMessage) { - if (KeyCachingService.isLocked(context)) { - body = SpanUtil.italic(context.getString(R.string.MessageNotifier_locked_message)); - } else if (record.isMms() && !((MmsMessageRecord) record).getSharedContacts().isEmpty()) { - Contact contact = ((MmsMessageRecord) record).getSharedContacts().get(0); - body = ContactUtil.getStringSummary(context, contact); - } else if (record.isMms() && ((MmsMessageRecord) record).getSlideDeck().getStickerSlide() != null) { - body = SpanUtil.italic(context.getString(R.string.MessageNotifier_sticker)); - slideDeck = ((MmsMessageRecord) record).getSlideDeck(); - } else if (record.isMms() && ((MmsMessageRecord) record).isViewOnce()) { - body = SpanUtil.italic(context.getString(getViewOnceDescription((MmsMessageRecord) record))); - } else if (record.isRemoteDelete()) { - body = SpanUtil.italic(context.getString(R.string.MessageNotifier_this_message_was_deleted));; - } else if (record.isMms() && TextUtils.isEmpty(body) && !((MmsMessageRecord) record).getSlideDeck().getSlides().isEmpty()) { - body = SpanUtil.italic(context.getString(R.string.MessageNotifier_media_message)); - slideDeck = ((MediaMmsMessageRecord) record).getSlideDeck(); - } else if (record.isMms() && !record.isMmsNotification() && !((MmsMessageRecord) record).getSlideDeck().getSlides().isEmpty()) { - String message = context.getString(R.string.MessageNotifier_media_message_with_text, body); - int italicLength = message.length() - body.length(); - body = SpanUtil.italic(message, italicLength); - slideDeck = ((MediaMmsMessageRecord) record).getSlideDeck(); - } - - if (threadRecipients == null || !threadRecipients.isMuted()) { - notificationState.addNotification(new NotificationItem(id, mms, recipient, conversationRecipient, threadRecipients, threadId, body, timestamp, receivedTimestamp, slideDeck, false)); - } - } - - if (hasUnreadReactions) { - for (ReactionRecord reaction : record.getReactions()) { - Recipient reactionSender = Recipient.resolved(reaction.getAuthor()); - if (reactionSender.equals(Recipient.self()) || !record.isOutgoing() || reaction.getDateReceived() <= lastReactionRead) { - continue; - } - - if (KeyCachingService.isLocked(context)) { - body = SpanUtil.italic(context.getString(R.string.MessageNotifier_locked_message)); - } else { - String text = SpanUtil.italic(getReactionMessageBody(context, record)).toString(); - String[] parts = text.split(EMOJI_REPLACEMENT_STRING); - - SpannableStringBuilder builder = new SpannableStringBuilder(); - for (int i = 0; i < parts.length; i++) { - builder.append(SpanUtil.italic(parts[i])); - - if (i != parts.length -1) { - builder.append(reaction.getEmoji()); - } - } - - if (text.endsWith(EMOJI_REPLACEMENT_STRING)) { - builder.append(reaction.getEmoji()); - } - - body = builder; - } - - if (threadRecipients == null || !threadRecipients.isMuted()) { - notificationState.addNotification(new NotificationItem(id, mms, reactionSender, conversationRecipient, threadRecipients, threadId, body, reaction.getDateReceived(), receivedTimestamp, null, true)); - } - } - } - } - - reader.close(); - return notificationState; - } - - private static CharSequence getReactionMessageBody(@NonNull Context context, @NonNull MessageRecord record) { - CharSequence body = record.getDisplayBody(context); - boolean bodyIsEmpty = TextUtils.isEmpty(body); - - if (MessageRecordUtil.hasSharedContact(record)) { - Contact contact = ((MmsMessageRecord) record).getSharedContacts().get(0); - CharSequence summary = ContactUtil.getStringSummary(context, contact); - - return context.getString(R.string.MessageNotifier_reacted_s_to_s, EMOJI_REPLACEMENT_STRING, summary); - } else if (MessageRecordUtil.hasSticker(record)) { - return context.getString(R.string.MessageNotifier_reacted_s_to_your_sticker, EMOJI_REPLACEMENT_STRING); - } else if (record.isMms() && record.isViewOnce() && MediaUtil.isVideoType(getMessageContentType((MmsMessageRecord) record))) { - return context.getString(R.string.MessageNotifier_reacted_s_to_your_view_once_video, EMOJI_REPLACEMENT_STRING); - } else if (record.isMms() && record.isViewOnce()){ - return context.getString(R.string.MessageNotifier_reacted_s_to_your_view_once_photo, EMOJI_REPLACEMENT_STRING); - } else if (!bodyIsEmpty) { - return context.getString(R.string.MessageNotifier_reacted_s_to_s, EMOJI_REPLACEMENT_STRING, body); - } else if (MessageRecordUtil.isMediaMessage(record) && MediaUtil.isVideoType(getMessageContentType((MmsMessageRecord) record))) { - return context.getString(R.string.MessageNotifier_reacted_s_to_your_video, EMOJI_REPLACEMENT_STRING); - } else if (MessageRecordUtil.isMediaMessage(record) && MediaUtil.isImageType(getMessageContentType((MmsMessageRecord) record))) { - return context.getString(R.string.MessageNotifier_reacted_s_to_your_image, EMOJI_REPLACEMENT_STRING); - } else if (MessageRecordUtil.isMediaMessage(record) && MediaUtil.isAudioType(getMessageContentType((MmsMessageRecord) record))) { - return context.getString(R.string.MessageNotifier_reacted_s_to_your_audio, EMOJI_REPLACEMENT_STRING); - } else if (MessageRecordUtil.isMediaMessage(record)) { - return context.getString(R.string.MessageNotifier_reacted_s_to_your_file, EMOJI_REPLACEMENT_STRING); - } else { - return context.getString(R.string.MessageNotifier_reacted_s_to_s, EMOJI_REPLACEMENT_STRING, body); - } - } - - private static @StringRes int getViewOnceDescription(@NonNull MmsMessageRecord messageRecord) { - final String contentType = getMessageContentType(messageRecord); - - if (MediaUtil.isImageType(contentType)) { - return R.string.MessageNotifier_view_once_photo; - } - return R.string.MessageNotifier_view_once_video; - } - - private static String getMessageContentType(@NonNull MmsMessageRecord messageRecord) { - Slide thumbnailSlide = messageRecord.getSlideDeck().getThumbnailSlide(); - if (thumbnailSlide == null) { - Log.w(TAG, "Could not distinguish view-once content type from message record, defaulting to JPEG"); - return MediaUtil.IMAGE_JPEG; - } - return thumbnailSlide.getContentType(); - } - - private static void updateBadge(Context context, int count) { - try { - if (count == 0) ShortcutBadger.removeCount(context); - else ShortcutBadger.applyCount(context, count); - } catch (Throwable t) { - // NOTE :: I don't totally trust this thing, so I'm catching - // everything. - Log.w(TAG, t); - } - } - - private static void scheduleReminder(Context context, int count) { - if (count >= TextSecurePreferences.getRepeatAlertsCount(context)) { - return; - } - - AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - Intent alarmIntent = new Intent(ReminderReceiver.REMINDER_ACTION); - alarmIntent.putExtra("reminder_count", count); - - PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, PendingIntent.FLAG_CANCEL_CURRENT); - long timeout = TimeUnit.MINUTES.toMillis(2); - - alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + timeout, pendingIntent); - } - - public static void clearReminder(Context context) { - Intent alarmIntent = new Intent(ReminderReceiver.REMINDER_ACTION); - PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, PendingIntent.FLAG_CANCEL_CURRENT); - AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - alarmManager.cancel(pendingIntent); - } - - public static class ReminderReceiver extends BroadcastReceiver { +import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; + +public interface MessageNotifier { + void setVisibleThread(long threadId); + void clearVisibleThread(); + void setLastDesktopActivityTimestamp(long timestamp); + void notifyMessageDeliveryFailed(Context context, Recipient recipient, long threadId); + void cancelDelayedNotifications(); + void updateNotification(@NonNull Context context); + void updateNotification(@NonNull Context context, long threadId); + void updateNotification(@NonNull Context context, long threadId, boolean signal); + void updateNotification(@NonNull Context context, long threadId, boolean signal, int reminderCount); + void clearReminder(@NonNull Context context); + + + class ReminderReceiver extends BroadcastReceiver { public static final String REMINDER_ACTION = "org.thoughtcrime.securesms.MessageNotifier.REMINDER_ACTION"; @SuppressLint("StaticFieldLeak") @Override public void onReceive(final Context context, final Intent intent) { - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - int reminderCount = intent.getIntExtra("reminder_count", 0); - MessageNotifier.updateNotification(context, -1, true, reminderCount + 1); - - return null; - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - } - - private static class DelayedNotification implements Runnable { - - private static final long DELAY = TimeUnit.SECONDS.toMillis(5); - - private final AtomicBoolean canceled = new AtomicBoolean(false); - - private final Context context; - private final long threadId; - private final long delayUntil; - - private DelayedNotification(Context context, long threadId) { - this.context = context; - this.threadId = threadId; - this.delayUntil = System.currentTimeMillis() + DELAY; - } - - @Override - public void run() { - long delayMillis = delayUntil - System.currentTimeMillis(); - Log.i(TAG, "Waiting to notify: " + delayMillis); - - if (delayMillis > 0) { - Util.sleep(delayMillis); - } - - if (!canceled.get()) { - Log.i(TAG, "Not canceled, notifying..."); - MessageNotifier.updateNotification(context, threadId, true); - MessageNotifier.cancelDelayedNotifications(); - } else { - Log.w(TAG, "Canceled, not notifying..."); - } - } - - public void cancel() { - canceled.set(true); - } - } - - private static class CancelableExecutor { - - private final Executor executor = Executors.newSingleThreadExecutor(); - private final Set tasks = new HashSet<>(); - - public void execute(final DelayedNotification runnable) { - synchronized (tasks) { - tasks.add(runnable); - } - - Runnable wrapper = new Runnable() { - @Override - public void run() { - runnable.run(); - - synchronized (tasks) { - tasks.remove(runnable); - } - } - }; - - executor.execute(wrapper); - } - - public void cancel() { - synchronized (tasks) { - for (DelayedNotification task : tasks) { - task.cancel(); - } - } + SignalExecutors.BOUNDED.execute(() -> { + int reminderCount = intent.getIntExtra("reminder_count", 0); + ApplicationDependencies.getMessageNotifier().updateNotification(context, -1, true, reminderCount + 1); + }); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java index c400f4090d..dc7cb25f35 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/RemoteReplyReceiver.java @@ -27,6 +27,7 @@ import androidx.core.app.RemoteInput; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; @@ -59,7 +60,7 @@ public class RemoteReplyReceiver extends BroadcastReceiver { final RecipientId recipientId = intent.getParcelableExtra(RECIPIENT_EXTRA); final ReplyMethod replyMethod = (ReplyMethod) intent.getSerializableExtra(REPLY_METHOD); - final CharSequence responseText = remoteInput.getCharSequence(MessageNotifier.EXTRA_REMOTE_REPLY); + final CharSequence responseText = remoteInput.getCharSequence(DefaultMessageNotifier.EXTRA_REMOTE_REPLY); if (recipientId == null) throw new AssertionError("No recipientId specified"); if (replyMethod == null) throw new AssertionError("No reply method specified"); @@ -96,7 +97,7 @@ public class RemoteReplyReceiver extends BroadcastReceiver { List messageIds = DatabaseFactory.getThreadDatabase(context).setRead(threadId, true); - MessageNotifier.updateNotification(context); + ApplicationDependencies.getMessageNotifier().updateNotification(context); MarkReadReceiver.process(context, messageIds); return null; diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java index 42f1bc16e1..2839ea2c21 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java @@ -176,7 +176,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil replyAction = new Action.Builder(R.drawable.ic_reply_white_36dp, actionName, wearableReplyIntent) - .addRemoteInput(new RemoteInput.Builder(MessageNotifier.EXTRA_REMOTE_REPLY) + .addRemoteInput(new RemoteInput.Builder(DefaultMessageNotifier.EXTRA_REMOTE_REPLY) .setLabel(label).build()) .build(); } @@ -184,7 +184,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil Action wearableReplyAction = new Action.Builder(R.drawable.ic_reply, actionName, wearableReplyIntent) - .addRemoteInput(new RemoteInput.Builder(MessageNotifier.EXTRA_REMOTE_REPLY) + .addRemoteInput(new RemoteInput.Builder(DefaultMessageNotifier.EXTRA_REMOTE_REPLY) .setLabel(label).build()) .build(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java index ededd69dfd..6980d126d3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/NotificationsPreferenceFragment.java @@ -17,7 +17,7 @@ import android.text.TextUtils; import org.thoughtcrime.securesms.ApplicationPreferencesActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.SwitchPreferenceCompat; -import org.thoughtcrime.securesms.notifications.MessageNotifier; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -211,7 +211,7 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme new AsyncTask() { @Override protected Void doInBackground(Void... params) { - MessageNotifier.updateNotification(getActivity()); + ApplicationDependencies.getMessageNotifier().updateNotification(getActivity()); return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/KeyCachingService.java b/app/src/main/java/org/thoughtcrime/securesms/service/KeyCachingService.java index 2645c765f0..c450efc04e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/KeyCachingService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/KeyCachingService.java @@ -33,6 +33,7 @@ import androidx.core.app.NotificationCompat; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.MainActivity; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.DummyActivity; @@ -41,7 +42,6 @@ import org.thoughtcrime.securesms.crypto.InvalidPassphraseException; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecretUtil; import org.thoughtcrime.securesms.migrations.ApplicationMigrations; -import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.ServiceUtil; @@ -115,7 +115,7 @@ public class KeyCachingService extends Service { @Override protected Void doInBackground(Void... params) { if (!ApplicationMigrations.isUpdate(KeyCachingService.this)) { - MessageNotifier.updateNotification(KeyCachingService.this); + ApplicationDependencies.getMessageNotifier().updateNotification(KeyCachingService.this); } return null; } @@ -188,7 +188,7 @@ public class KeyCachingService extends Service { new AsyncTask() { @Override protected Void doInBackground(Void... params) { - MessageNotifier.updateNotification(KeyCachingService.this); + ApplicationDependencies.getMessageNotifier().updateNotification(KeyCachingService.this); return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java index 85f98d3dd5..e4dfbe6227 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/WebRtcCallService.java @@ -5,7 +5,6 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.graphics.Bitmap; import android.media.AudioManager; import android.net.Uri; import android.os.Build; @@ -34,7 +33,6 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.events.WebRtcViewModel; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.notifications.DoNotDisturbUtil; -import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.ringrtc.CallState; @@ -58,10 +56,8 @@ import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger; import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager; import org.thoughtcrime.securesms.webrtc.locks.LockManager; import org.webrtc.EglBase; -import org.webrtc.EglRenderer; import org.webrtc.IceCandidate; import org.webrtc.PeerConnection; -import org.webrtc.SurfaceViewRenderer; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceMessageSender; @@ -455,7 +451,7 @@ public class WebRtcCallService extends Service implements CallManager.Observer, private void insertMissedCall(@NonNull RemotePeer remotePeer, boolean signal) { Pair messageAndThreadId = DatabaseFactory.getSmsDatabase(this).insertMissedCall(remotePeer.getId()); - MessageNotifier.updateNotification(this, messageAndThreadId.second, signal); + ApplicationDependencies.getMessageNotifier().updateNotification(this, messageAndThreadId.second, signal); } private void handleDenyCall(Intent intent) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/IdentityUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/IdentityUtil.java index 950295f6cb..f0831dbba5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/IdentityUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/IdentityUtil.java @@ -16,8 +16,8 @@ import org.thoughtcrime.securesms.database.IdentityDatabase; import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord; import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult; import org.thoughtcrime.securesms.database.SmsDatabase; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.logging.Log; -import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.sms.IncomingIdentityDefaultMessage; @@ -141,7 +141,7 @@ public class IdentityUtil { Optional insertResult = smsDatabase.insertMessageInbox(individualUpdate); if (insertResult.isPresent()) { - MessageNotifier.updateNotification(context, insertResult.get().getThreadId()); + ApplicationDependencies.getMessageNotifier().updateNotification(context, insertResult.get().getThreadId()); } }