Add support for Android 11 Conversation Bubbles.

This commit is contained in:
Alex Hart
2020-11-25 14:11:17 -04:00
committed by GitHub
parent 3aebadd90d
commit e1bf23251f
22 changed files with 518 additions and 70 deletions

View File

@@ -120,6 +120,15 @@ public final class AvatarUtil {
}
}
@WorkerThread
public static IconCompat getIconCompatForShortcut(@NonNull Context context, @NonNull Recipient recipient) {
try {
return IconCompat.createWithAdaptiveBitmap(getShortcutInfoBitmap(context, recipient));
} catch (ExecutionException | InterruptedException e) {
return IconCompat.createWithAdaptiveBitmap(getFallbackForShortcut(context, recipient));
}
}
@WorkerThread
public static Bitmap getBitmapForNotification(@NonNull Context context, @NonNull Recipient recipient) {
try {

View File

@@ -0,0 +1,117 @@
package org.thoughtcrime.securesms.util;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.service.notification.StatusBarNotification;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.conversation.BubbleConversationActivity;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier;
import org.thoughtcrime.securesms.notifications.NotificationIds;
import org.thoughtcrime.securesms.notifications.SingleRecipientNotificationBuilder;
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import java.util.Objects;
import static org.thoughtcrime.securesms.util.ConversationUtil.CONVERSATION_SUPPORT_VERSION;
/**
* Bubble-related utility methods.
*/
public final class BubbleUtil {
private static final String TAG = Log.tag(BubbleUtil.class);
private BubbleUtil() {
}
/**
* Checks whether we are allowed to create a bubble for the given recipient.
*
* In order to Bubble, a recipient must have a thread, be unblocked, and the user must not have
* notification privacy settings enabled. Furthermore, we check the Notifications system to verify
* that bubbles are allowed in the first place.
*/
@RequiresApi(CONVERSATION_SUPPORT_VERSION)
@WorkerThread
public static boolean canBubble(@NonNull Context context, @NonNull RecipientId recipientId, @Nullable Long threadId) {
if (threadId == null) {
Log.i(TAG, "Cannot bubble recipient without thread");
return false;
}
NotificationPrivacyPreference privacyPreference = TextSecurePreferences.getNotificationPrivacy(context);
if (!privacyPreference.isDisplayMessage()) {
Log.i(TAG, "Bubbles are not available when notification privacy settings are enabled.");
return false;
}
Recipient recipient = Recipient.resolved(recipientId);
if (recipient.isBlocked()) {
Log.i(TAG, "Cannot bubble blocked recipient");
return false;
}
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
NotificationChannel conversationChannel = notificationManager.getNotificationChannel(ConversationUtil.getChannelId(context, recipient),
ConversationUtil.getShortcutId(recipientId));
return notificationManager.areBubblesAllowed() || (conversationChannel != null && conversationChannel.canBubble());
}
/**
* Display a bubble for a given recipient's thread.
*/
public static void displayAsBubble(@NonNull Context context, @NonNull RecipientId recipientId, long threadId) {
if (Build.VERSION.SDK_INT >= CONVERSATION_SUPPORT_VERSION) {
SignalExecutors.BOUNDED.execute(() -> {
if (canBubble(context, recipientId, threadId)) {
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
StatusBarNotification[] notifications = notificationManager.getActiveNotifications();
int threadNotificationId = NotificationIds.getNotificationIdForThread(threadId);
Notification activeThreadNotification = Stream.of(notifications)
.filter(n -> n.getId() == threadNotificationId)
.findFirst()
.map(StatusBarNotification::getNotification)
.orElse(null);
if (activeThreadNotification != null && activeThreadNotification.deleteIntent != null) {
ApplicationDependencies.getMessageNotifier().updateNotification(context, threadId, BubbleState.SHOWN);
} else {
Recipient recipient = Recipient.resolved(recipientId);
SingleRecipientNotificationBuilder builder = new SingleRecipientNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context));
builder.addMessageBody(recipient, recipient, "", System.currentTimeMillis(), null);
builder.setThread(recipient);
builder.setDefaultBubbleState(BubbleState.SHOWN);
builder.setGroup(DefaultMessageNotifier.NOTIFICATION_GROUP);
Log.d(TAG, "Posting Notification for requested bubble");
notificationManager.notify(NotificationIds.getNotificationIdForThread(threadId), builder.build());
}
}
});
}
}
public enum BubbleState {
SHOWN,
HIDDEN
}
}

View File

@@ -1,11 +1,15 @@
package org.thoughtcrime.securesms.util;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Person;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.os.Build;
import android.service.notification.StatusBarNotification;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
@@ -18,7 +22,11 @@ import org.thoughtcrime.securesms.conversation.ConversationActivity;
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.notifications.NotificationChannels;
import org.thoughtcrime.securesms.notifications.NotificationIds;
import org.thoughtcrime.securesms.notifications.SingleRecipientNotificationBuilder;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
@@ -33,10 +41,21 @@ import java.util.Set;
*/
public final class ConversationUtil {
public static final int CONVERSATION_SUPPORT_VERSION = 30;
public static final int CONVERSATION_SUPPORT_VERSION = 30;
private ConversationUtil() {}
/**
* @return The stringified channel id for a given Recipient
*/
@WorkerThread
public static @NonNull String getChannelId(@NonNull Context context, @NonNull Recipient recipient) {
Recipient resolved = recipient.resolve();
return resolved.getNotificationChannel() != null ? resolved.getNotificationChannel() : NotificationChannels.getMessagesChannel(context);
}
/**
* Pushes a new dynamic shortcut for the given recipient and updates the ranks of all current
* shortcuts.
@@ -130,7 +149,7 @@ public final class ConversationUtil {
ShortcutManager shortcutManager = ServiceUtil.getShortcutManager(context);
List<ShortcutInfo> currentShortcuts = shortcutManager.getDynamicShortcuts();
if (Util.isEmpty(currentShortcuts)) {
if (Util.hasItems(currentShortcuts)) {
for (ShortcutInfo shortcutInfo : currentShortcuts) {
RecipientId recipientId = RecipientId.from(shortcutInfo.getId());
Recipient resolved = Recipient.resolved(recipientId);