diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 45a7371eae..c7cf687241 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -430,7 +430,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab } } - String channelId = NotificationChannels.createChannelFor(context, "contact_" + address + "_" + System.currentTimeMillis(), displayName, messageSoundUri, vibrateEnabled); + String channelId = NotificationChannels.createChannelFor(context, "contact_" + address + "_" + System.currentTimeMillis(), displayName, messageSoundUri, vibrateEnabled, null); ContentValues values = new ContentValues(1); values.put("notification_channel", channelId); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupRepository.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupRepository.java index 9212497a8b..22c9882395 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupRepository.java @@ -25,6 +25,7 @@ import org.thoughtcrime.securesms.groups.MembershipNotSuitableForV2Exception; import org.thoughtcrime.securesms.groups.SelectionLimits; import org.thoughtcrime.securesms.groups.ui.GroupChangeErrorCallback; import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason; +import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; @@ -148,6 +149,15 @@ final class ManageGroupRepository { }); } + @WorkerThread + boolean hasCustomNotifications(Recipient recipient) { + if (recipient.getNotificationChannel() != null || !NotificationChannels.supported()) { + return true; + } + + return NotificationChannels.updateWithShortcutBasedChannel(context, recipient); + } + static final class GroupStateResult { private final long threadId; diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupViewModel.java index b4bccde79a..ebd737e24b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupViewModel.java @@ -36,7 +36,6 @@ import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry; import org.thoughtcrime.securesms.groups.ui.addmembers.AddMembersActivity; import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupMentionSettingDialog; import org.thoughtcrime.securesms.groups.v2.GroupLinkUrlAndStatus; -import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; @@ -120,8 +119,7 @@ public class ManageGroupViewModel extends ViewModel { this.canAddMembers = liveGroup.selfCanAddMembers(); this.muteState = Transformations.map(this.groupRecipient, recipient -> new MuteState(recipient.getMuteUntil(), recipient.isMuted())); - this.hasCustomNotifications = Transformations.map(this.groupRecipient, - recipient -> recipient.getNotificationChannel() != null || !NotificationChannels.supported()); + this.hasCustomNotifications = LiveDataUtil.mapAsync(this.groupRecipient, manageGroupRepository::hasCustomNotifications); this.canLeaveGroup = liveGroup.isActive(); this.canBlockGroup = Transformations.map(this.groupRecipient, recipient -> RecipientUtil.isBlockable(recipient) && !recipient.isBlocked()); this.canUnblockGroup = Transformations.map(this.groupRecipient, Recipient::isBlocked); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationCancellationHelper.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationCancellationHelper.java index 37902569cc..842fe59ac6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationCancellationHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationCancellationHelper.java @@ -176,10 +176,15 @@ public final class NotificationCancellationHelper { return true; } - RecipientId recipientId = RecipientId.from(notification.getShortcutId()); - Long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipientId); + RecipientId recipientId = ConversationUtil.getRecipientId(notification.getShortcutId()); + if (recipientId == null) { + Log.d(TAG, "isCancellable: Unable to get recipient from shortcut id"); + return true; + } + Long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipientId); long focusedThreadId = ApplicationDependencies.getMessageNotifier().getVisibleThread(); + if (Objects.equals(threadId, focusedThreadId)) { Log.d(TAG, "isCancellable: user entered full screen thread."); return true; diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationChannels.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationChannels.java index fc4cb974f9..efbcb1e8f6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationChannels.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationChannels.java @@ -31,6 +31,8 @@ import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.util.ConversationUtil; import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -38,8 +40,12 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; +import java.util.Objects; +import java.util.Optional; import java.util.Set; +import static org.thoughtcrime.securesms.util.ConversationUtil.CONVERSATION_SUPPORT_VERSION; + public class NotificationChannels { private static final String TAG = Log.tag(NotificationChannels.class); @@ -165,7 +171,7 @@ public class NotificationChannels { Uri messageRingtone = recipient.getMessageRingtone() != null ? recipient.getMessageRingtone() : getMessageRingtone(context); String displayName = recipient.getDisplayName(context); - return createChannelFor(context, generateChannelIdFor(recipient), displayName, messageRingtone, vibrationEnabled); + return createChannelFor(context, generateChannelIdFor(recipient), displayName, messageRingtone, vibrationEnabled, ConversationUtil.getShortcutId(recipient)); } /** @@ -175,7 +181,8 @@ public class NotificationChannels { @NonNull String channelId, @NonNull String displayName, @Nullable Uri messageSound, - boolean vibrationEnabled) + boolean vibrationEnabled, + @Nullable String shortcutId) { if (!supported()) { return null; @@ -193,6 +200,10 @@ public class NotificationChannels { .build()); } + if (Build.VERSION.SDK_INT >= CONVERSATION_SUPPORT_VERSION && shortcutId != null) { + channel.setConversationId(getMessagesChannel(context), shortcutId); + } + NotificationManager notificationManager = ServiceUtil.getNotificationManager(context); notificationManager.createNotificationChannel(channel); @@ -465,6 +476,32 @@ public class NotificationChannels { } } + /** + * Attempt to update a recipient with shortcut based notification channel if the system made one for us and we don't + * have a channel set yet. + * + * @return true if a shortcut based notification channel was found and then associated with the recipient, false otherwise + */ + @WorkerThread + public static boolean updateWithShortcutBasedChannel(@NonNull Context context, @NonNull Recipient recipient) { + if (Build.VERSION.SDK_INT >= CONVERSATION_SUPPORT_VERSION && TextUtils.isEmpty(recipient.getNotificationChannel())) { + String shortcutId = ConversationUtil.getShortcutId(recipient); + + Optional channel = ServiceUtil.getNotificationManager(context) + .getNotificationChannels() + .stream() + .filter(c -> Objects.equals(shortcutId, c.getConversationId())) + .findFirst(); + + if (channel.isPresent()) { + Log.i(TAG, "Conversation channel created outside of app, while running. Update " + recipient.getId() + " to use '" + channel.get().getId() + "'"); + DatabaseFactory.getRecipientDatabase(context).setNotificationChannel(recipient.getId(), channel.get().getId()); + return true; + } + } + return false; + } + /** * Updates the name of an existing channel to match the recipient's current name. Will have no * effect if the recipient doesn't have an existing valid channel. @@ -516,8 +553,24 @@ public class NotificationChannels { if (existingChannel.getId().startsWith(CONTACT_PREFIX) && !customChannelIds.contains(existingChannel.getId())) { Log.i(TAG, "Consistency: Deleting channel '"+ existingChannel.getId() + "' because the DB has no record of it."); notificationManager.deleteNotificationChannel(existingChannel.getId()); + } else if (existingChannel.getId().startsWith(MESSAGES_PREFIX) && + Build.VERSION.SDK_INT >= CONVERSATION_SUPPORT_VERSION && + existingChannel.getConversationId() != null) + { + if (customChannelIds.contains(existingChannel.getId())) { + continue; + } + + RecipientId id = ConversationUtil.getRecipientId(existingChannel.getConversationId()); + if (id != null) { + Log.i(TAG, "Consistency: Conversation channel created outside of app, update " + id + " to use '" + existingChannel.getId() + "'"); + db.setNotificationChannel(id, existingChannel.getId()); + } else { + Log.i(TAG, "Consistency: Conversation channel created outside of app with no matching recipient, deleting channel '" + existingChannel.getId() + "'"); + notificationManager.deleteNotificationChannel(existingChannel.getId()); + } } else if (existingChannel.getId().startsWith(MESSAGES_PREFIX) && !existingChannel.getId().equals(getMessagesChannel(context))) { - Log.i(TAG, "Consistency: Deleting channel '"+ existingChannel.getId() + "' because it's out of date."); + Log.i(TAG, "Consistency: Deleting channel '" + existingChannel.getId() + "' because it's out of date."); notificationManager.deleteNotificationChannel(existingChannel.getId()); } } @@ -617,6 +670,10 @@ public class NotificationChannels { copy.setLightColor(original.getLightColor()); copy.enableLights(original.shouldShowLights()); + if (Build.VERSION.SDK_INT >= CONVERSATION_SUPPORT_VERSION && original.getConversationId() != null) { + copy.setConversationId(original.getParentChannelId(), original.getConversationId()); + } + return copy; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientRepository.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientRepository.java index 7d8bc63268..6d69d4bc89 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientRepository.java @@ -10,15 +10,12 @@ import com.annimon.stream.Stream; import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.color.MaterialColor; -import org.thoughtcrime.securesms.color.MaterialColors; import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.IdentityDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob; +import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; @@ -106,4 +103,13 @@ final class ManageRecipientRepository { void getActiveGroupCount(@NonNull Consumer onComplete) { SignalExecutors.BOUNDED.execute(() -> onComplete.accept(DatabaseFactory.getGroupDatabase(context).getActiveGroupCount())); } + + @WorkerThread + boolean hasCustomNotifications(Recipient recipient) { + if (recipient.getNotificationChannel() != null || !NotificationChannels.supported()) { + return true; + } + + return NotificationChannels.updateWithShortcutBasedChannel(context, recipient); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientViewModel.java index 9312ab003c..97a83ab35d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientViewModel.java @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.recipients.ui.managerecipient; import android.app.Activity; +import android.app.NotificationChannel; import android.content.Context; import android.database.Cursor; @@ -78,7 +79,7 @@ public final class ManageRecipientViewModel extends ViewModel { this.groupListCollapseState = new DefaultValueLiveData<>(CollapseState.COLLAPSED); this.disappearingMessageTimer = Transformations.map(this.recipient, r -> ExpirationUtil.getExpirationDisplayValue(context, r.getExpireMessages())); this.muteState = Transformations.map(this.recipient, r -> new MuteState(r.getMuteUntil(), r.isMuted())); - this.hasCustomNotifications = Transformations.map(this.recipient, r -> r.getNotificationChannel() != null || !NotificationChannels.supported()); + this.hasCustomNotifications = LiveDataUtil.mapAsync(this.recipient, manageRecipientRepository::hasCustomNotifications); this.canBlock = Transformations.map(this.recipient, r -> RecipientUtil.isBlockable(r) && !r.isBlocked()); this.canUnblock = Transformations.map(this.recipient, Recipient::isBlocked); this.internalDetails = Transformations.map(this.recipient, this::populateInternalDetails); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ConversationUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/ConversationUtil.java index 96c8528484..c145e8f8df 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ConversationUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ConversationUtil.java @@ -8,6 +8,7 @@ import android.content.pm.ShortcutManager; import android.os.Build; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.WorkerThread; @@ -19,7 +20,6 @@ import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.conversation.ConversationIntents; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase; -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.jobs.ConversationShortcutUpdateJob; import org.thoughtcrime.securesms.notifications.NotificationChannels; @@ -132,6 +132,22 @@ public final class ConversationUtil { return getShortcutId(recipient.getId()); } + /** + * Extract the recipient id from the provided shortcutId. + */ + public static @Nullable RecipientId getRecipientId(@Nullable String shortcutId) { + if (shortcutId == null) { + return null; + } + + try { + return RecipientId.from(shortcutId); + } catch (Throwable t) { + Log.d(TAG, "Unable to parse recipientId from shortcutId", t); + return null; + } + } + @RequiresApi(CONVERSATION_SUPPORT_VERSION) public static int getMaxShortcuts(@NonNull Context context) { ShortcutManager shortcutManager = ServiceUtil.getShortcutManager(context);