mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-09-28 07:51:42 +01:00
Fix conversation bubbles disappearing.
This commit is contained in:
committed by
Michelle Tang
parent
b8185e715d
commit
7051d10cef
@@ -840,6 +840,18 @@ class ConversationFragment :
|
||||
super.onCreate(savedInstanceState)
|
||||
SignalLocalMetrics.ConversationOpen.start()
|
||||
registerForResults()
|
||||
|
||||
if (args.conversationScreenType.isInBubble) {
|
||||
AppDependencies.messageNotifier.addActiveBubbleThread(ConversationId.forConversation(args.threadId))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
if (args.conversationScreenType.isInBubble) {
|
||||
AppDependencies.messageNotifier.removeActiveBubbleThread(ConversationId.forConversation(args.threadId))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
|
||||
@@ -49,10 +49,12 @@ public class MarkReadReceiver extends BroadcastReceiver {
|
||||
notifier.removeStickyThread(thread);
|
||||
}
|
||||
|
||||
NotificationCancellationHelper.cancelLegacy(context, intent.getIntExtra(NOTIFICATION_ID_EXTRA, -1));
|
||||
int notificationId = intent.getIntExtra(NOTIFICATION_ID_EXTRA, -1);
|
||||
|
||||
PendingResult finisher = goAsync();
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
NotificationCancellationHelper.cancel(context, notificationId);
|
||||
|
||||
List<MarkedMessageInfo> messageIdsCollection = new LinkedList<>();
|
||||
|
||||
for (ConversationId thread : threads) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.notifications.v2.ConversationId;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
public interface MessageNotifier {
|
||||
void setVisibleThread(@Nullable ConversationId conversationId);
|
||||
@@ -21,8 +22,10 @@ public interface MessageNotifier {
|
||||
void clearVisibleThread();
|
||||
void clearVisibleThread(@NonNull ConversationId conversationId);
|
||||
void setVisibleBubbleThread(@Nullable ConversationId conversationId);
|
||||
@NonNull Optional<ConversationId> getVisibleBubbleThread();
|
||||
void clearVisibleBubbleThread();
|
||||
void addActiveBubbleThread(@NonNull ConversationId conversationId);
|
||||
void removeActiveBubbleThread(@NonNull ConversationId conversationId);
|
||||
@NonNull Set<ConversationId> getActiveBubbleThreads();
|
||||
void notifyMessageDeliveryFailed(@NonNull Context context, @NonNull Recipient recipient, @NonNull ConversationId conversationId);
|
||||
void notifyStoryDeliveryFailed(@NonNull Context context, @NonNull Recipient recipient, @NonNull ConversationId conversationId);
|
||||
void notifyProofRequired(@NonNull Context context, @NonNull Recipient recipient, @NonNull ConversationId conversationId);
|
||||
|
||||
+41
-16
@@ -21,7 +21,6 @@ import org.signal.core.util.ServiceUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -111,8 +110,8 @@ public final class NotificationCancellationHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to cancel the given notification. If the notification is backing a bubble the user
|
||||
* currently has open, we do not cancel it.
|
||||
* Attempts to cancel the given notification. If the notification is backing an active bubble, we
|
||||
* suppress it from the shade instead.
|
||||
*
|
||||
* @return Whether or not the notification is considered cancelled.
|
||||
*/
|
||||
@@ -135,8 +134,7 @@ public final class NotificationCancellationHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel method which first checks whether the notification in question is tied to a bubble the
|
||||
* user currently has open.
|
||||
* Cancel method which first checks whether the notification in question is tied to an active bubble.
|
||||
*
|
||||
* @return true if the notification was cancelled.
|
||||
*/
|
||||
@@ -152,8 +150,8 @@ public final class NotificationCancellationHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancelling a notification tears down the bubble it backs, so we hold off while the user has that
|
||||
* conversation open as a bubble.
|
||||
* Cancelling a notification tears down the bubble it backs, so we hold off while it is showing as a
|
||||
* bubble, even if it is collapsed.
|
||||
*/
|
||||
@RequiresApi(ConversationUtil.CONVERSATION_SUPPORT_VERSION)
|
||||
private static boolean isCancellable(@NonNull Context context, int notificationId) {
|
||||
@@ -180,25 +178,52 @@ public final class NotificationCancellationHelper {
|
||||
|
||||
Long threadId = SignalDatabase.threads().getThreadIdFor(recipientId);
|
||||
|
||||
if (isVisible(AppDependencies.getMessageNotifier().getVisibleThread(), threadId)) {
|
||||
if (isThread(AppDependencies.getMessageNotifier().getVisibleThread().orElse(null), threadId)) {
|
||||
Log.d(TAG, "isCancellable: user entered full screen thread.");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isVisible(AppDependencies.getMessageNotifier().getVisibleBubbleThread(), threadId)) {
|
||||
Log.d(TAG, "isCancellable: conversation is currently open as a bubble.");
|
||||
boolean showingAsBubble = (notification.flags & Notification.FLAG_BUBBLE) != 0;
|
||||
boolean hasActiveBubble = AppDependencies.getMessageNotifier().getActiveBubbleThreads().stream().anyMatch(bubbleThread -> isThread(bubbleThread, threadId));
|
||||
|
||||
if (showingAsBubble || hasActiveBubble) {
|
||||
Log.d(TAG, "isCancellable: bubble is active (flag: " + showingAsBubble + ", opened: " + hasActiveBubble + "), suppressing instead.");
|
||||
suppressNotification(context, notificationId, notification);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether the given conversation is the non-story conversation the user is currently looking at.
|
||||
*/
|
||||
private static boolean isVisible(@NonNull Optional<ConversationId> visibleThread, @Nullable Long threadId) {
|
||||
ConversationId visible = visibleThread.orElse(null);
|
||||
private static boolean isThread(@Nullable ConversationId conversationId, @Nullable Long threadId) {
|
||||
return conversationId != null && conversationId.getGroupStoryId() == null && Objects.equals(threadId, conversationId.getThreadId());
|
||||
}
|
||||
|
||||
return visible != null && visible.getGroupStoryId() == null && Objects.equals(threadId, visible.getThreadId());
|
||||
/**
|
||||
* Re-posts the notification with its shade entry suppressed, which keeps the bubble it backs.
|
||||
*/
|
||||
@RequiresApi(ConversationUtil.CONVERSATION_SUPPORT_VERSION)
|
||||
private static void suppressNotification(@NonNull Context context, int notificationId, @NonNull Notification notification) {
|
||||
Notification.BubbleMetadata bubbleMetadata = notification.getBubbleMetadata();
|
||||
|
||||
if (bubbleMetadata == null || bubbleMetadata.isNotificationSuppressed() || bubbleMetadata.getIntent() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Notification.BubbleMetadata suppressedMetadata = new Notification.BubbleMetadata.Builder(bubbleMetadata.getIntent(), bubbleMetadata.getIcon())
|
||||
.setDesiredHeight(bubbleMetadata.getDesiredHeight())
|
||||
.setSuppressNotification(true)
|
||||
.build();
|
||||
|
||||
Notification suppressed = Notification.Builder.recoverBuilder(context, notification)
|
||||
.setBubbleMetadata(suppressedMetadata)
|
||||
.setOnlyAlertOnce(true)
|
||||
.build();
|
||||
|
||||
try {
|
||||
ServiceUtil.getNotificationManager(context).notify(notificationId, suppressed);
|
||||
} catch (SecurityException e) {
|
||||
Log.w(TAG, "Unable to suppress bubble notification", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
-4
@@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.LeakyBucketLimiter;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Uses a leaky-bucket strategy to limiting notification updates.
|
||||
@@ -65,13 +66,23 @@ public class OptimizedMessageNotifier implements MessageNotifier {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Optional<ConversationId> getVisibleBubbleThread() {
|
||||
return getNotifier().getVisibleBubbleThread();
|
||||
public void clearVisibleBubbleThread() {
|
||||
getNotifier().clearVisibleBubbleThread();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearVisibleBubbleThread() {
|
||||
getNotifier().clearVisibleBubbleThread();
|
||||
public void addActiveBubbleThread(@NonNull ConversationId conversationId) {
|
||||
getNotifier().addActiveBubbleThread(conversationId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeActiveBubbleThread(@NonNull ConversationId conversationId) {
|
||||
getNotifier().removeActiveBubbleThread(conversationId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Set<ConversationId> getActiveBubbleThreads() {
|
||||
return getNotifier().getActiveBubbleThreads();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+14
-4
@@ -49,6 +49,8 @@ class DefaultMessageNotifier(context: Application) : MessageNotifier {
|
||||
|
||||
@Volatile private var visibleBubbleThread: ConversationId? = null
|
||||
|
||||
private val activeBubbleThreads: MutableSet<ConversationId> = ConcurrentHashMap.newKeySet()
|
||||
|
||||
@Volatile private var lastAudibleNotification: Long = -1
|
||||
|
||||
@Volatile private var lastScheduledReminder: Long = 0
|
||||
@@ -89,14 +91,22 @@ class DefaultMessageNotifier(context: Application) : MessageNotifier {
|
||||
visibleBubbleThread = conversationId
|
||||
}
|
||||
|
||||
override fun getVisibleBubbleThread(): Optional<ConversationId> {
|
||||
return Optional.ofNullable(visibleBubbleThread)
|
||||
}
|
||||
|
||||
override fun clearVisibleBubbleThread() {
|
||||
setVisibleBubbleThread(null)
|
||||
}
|
||||
|
||||
override fun addActiveBubbleThread(conversationId: ConversationId) {
|
||||
activeBubbleThreads += conversationId
|
||||
}
|
||||
|
||||
override fun removeActiveBubbleThread(conversationId: ConversationId) {
|
||||
activeBubbleThreads -= conversationId
|
||||
}
|
||||
|
||||
override fun getActiveBubbleThreads(): Set<ConversationId> {
|
||||
return activeBubbleThreads.toSet()
|
||||
}
|
||||
|
||||
override fun notifyMessageDeliveryFailed(context: Context, recipient: Recipient, conversationId: ConversationId) {
|
||||
NotificationFactory.notifyMessageDeliveryFailed(context, recipient, conversationId, visibleThread.get(), visibleBubbleThread)
|
||||
}
|
||||
|
||||
@@ -116,6 +116,9 @@ public final class BubbleUtil {
|
||||
ConversationId conversationId = ConversationId.forConversation(threadId);
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
if (canBubble(context, recipientId, threadId)) {
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
ConversationUtil.ensureShortcutForRecipientSync(context, recipient);
|
||||
|
||||
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
|
||||
StatusBarNotification[] notifications = notificationManager.getActiveNotifications();
|
||||
int threadNotificationId = NotificationIds.getNotificationIdForThread(conversationId);
|
||||
@@ -128,7 +131,6 @@ public final class BubbleUtil {
|
||||
if (activeThreadNotification != null && activeThreadNotification.deleteIntent != null) {
|
||||
AppDependencies.getMessageNotifier().forceBubbleNotification(context, conversationId);
|
||||
} else {
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
NotificationFactory.notifyToBubbleConversation(context, recipient, threadId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.database.GroupTable;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.jobs.ConversationShortcutUpdateJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.signal.core.ui.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
@@ -88,6 +89,24 @@ public final class ConversationUtil {
|
||||
return pushShortcutForRecipientInternal(context, recipient, 0, direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes a dynamic shortcut for the given recipient if one is not already published. The OS will
|
||||
* not treat a notification as a conversation, or let it bubble, unless its shortcut exists.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static void ensureShortcutForRecipientSync(@NonNull Context context, @NonNull Recipient recipient) {
|
||||
if (SignalStore.settings().getScreenLockEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String shortcutId = getShortcutId(recipient);
|
||||
|
||||
if (ShortcutManagerCompat.getDynamicShortcuts(context).stream().noneMatch(shortcut -> shortcut.getId().equals(shortcutId))) {
|
||||
Log.d(TAG, "Pushing missing shortcut for " + recipient.getId());
|
||||
pushShortcutForRecipientSync(context, recipient, Direction.NONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all currently set dynamic shortcuts
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user