Harden notification channels.

There's odd corner cases where channels can be duplicated. This commit
adds some hard checks where we trim any dead channels, and unset any
notification channels from recipients whose notification channel isn't
present in the system settings.
This commit is contained in:
Greyson Parrelli 2019-01-30 10:19:03 -08:00
parent ba67796992
commit e3b22dabce
2 changed files with 87 additions and 40 deletions

View File

@ -282,6 +282,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
RecipientDatabase db = DatabaseFactory.getRecipientDatabase(getContext());
db.setMessageRingtone(recipient, NotificationChannels.getMessageRingtone(context, recipient));
db.setMessageVibrate(recipient, NotificationChannels.getMessageVibrate(context, recipient) ? VibrateState.ENABLED : VibrateState.DISABLED);
NotificationChannels.ensureCustomChannelConsistency(context);
return null;
}
}.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);

View File

@ -9,6 +9,7 @@ import android.content.Intent;
import android.graphics.Color;
import android.media.AudioAttributes;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.provider.Settings;
import android.support.annotation.NonNull;
@ -16,6 +17,9 @@ import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
import android.text.TextUtils;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.Address;
@ -23,10 +27,15 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.logging.Log;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class NotificationChannels {
@ -51,12 +60,12 @@ public class NotificationChannels {
* Ensures all of the notification channels are created. No harm in repeat calls. Call is safely
* ignored for API < 26.
*/
public static void create(@NonNull Context context) {
public static synchronized void create(@NonNull Context context) {
if (!supported()) {
return;
}
NotificationManager notificationManager = getNotificationManager(context);
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
int oldVersion = TextSecurePreferences.getNotificationChannelVersion(context);
if (oldVersion != VERSION) {
@ -65,6 +74,10 @@ public class NotificationChannels {
}
onCreate(context, notificationManager);
AsyncTask.SERIAL_EXECUTOR.execute(() -> {
ensureCustomChannelConsistency(context);
});
}
/**
@ -72,7 +85,7 @@ public class NotificationChannels {
* safe to call repeatedly. Needs to be executed on a background thread.
*/
@WorkerThread
public static void restoreContactNotificationChannels(@NonNull Context context) {
public static synchronized void restoreContactNotificationChannels(@NonNull Context context) {
if (!NotificationChannels.supported()) {
return;
}
@ -82,19 +95,21 @@ public class NotificationChannels {
try (RecipientDatabase.RecipientReader reader = db.getRecipientsWithNotificationChannels()) {
Recipient recipient;
while ((recipient = reader.getNext()) != null) {
NotificationManager notificationManager = getNotificationManager(context);
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
if (!channelExists(notificationManager.getNotificationChannel(recipient.getNotificationChannel()))) {
String id = createChannelFor(context, recipient);
db.setNotificationChannel(recipient, id);
}
}
}
ensureCustomChannelConsistency(context);
}
/**
* @return The channel ID for the default messages channel.
*/
public static @NonNull String getMessagesChannel(@NonNull Context context) {
public static synchronized @NonNull String getMessagesChannel(@NonNull Context context) {
return getMessagesChannelId(TextSecurePreferences.getNotificationMessagesChannelVersion(context));
}
@ -124,7 +139,7 @@ public class NotificationChannels {
* Creates a channel for the specified recipient.
* @return The channel ID for the newly-created channel.
*/
public static String createChannelFor(@NonNull Context context, @NonNull Recipient recipient) {
public static synchronized String createChannelFor(@NonNull Context context, @NonNull Recipient recipient) {
VibrateState vibrateState = recipient.getMessageVibrate();
boolean vibrationEnabled = vibrateState == VibrateState.DEFAULT ? TextSecurePreferences.isNotificationVibrateEnabled(context) : vibrateState == VibrateState.ENABLED;
Uri messageRingtone = recipient.getMessageRingtone() != null ? recipient.getMessageRingtone() : getMessageRingtone(context);
@ -136,11 +151,11 @@ public class NotificationChannels {
/**
* More verbose version of {@link #createChannelFor(Context, Recipient)}.
*/
public static @Nullable String createChannelFor(@NonNull Context context,
@NonNull Address address,
@NonNull String displayName,
@Nullable Uri messageSound,
boolean vibrationEnabled)
public static synchronized @Nullable String createChannelFor(@NonNull Context context,
@NonNull Address address,
@NonNull String displayName,
@Nullable Uri messageSound,
boolean vibrationEnabled)
{
if (!supported()) {
return null;
@ -159,7 +174,7 @@ public class NotificationChannels {
.build());
}
NotificationManager notificationManager = getNotificationManager(context);
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
notificationManager.createNotificationChannel(channel);
return channelId;
@ -169,12 +184,12 @@ public class NotificationChannels {
* Deletes the channel generated for the provided recipient. Safe to call even if there was never
* a channel made for that recipient.
*/
public static void deleteChannelFor(@NonNull Context context, @NonNull Recipient recipient) {
public static synchronized void deleteChannelFor(@NonNull Context context, @NonNull Recipient recipient) {
if (!supported()) {
return;
}
NotificationManager notificationManager = getNotificationManager(context);
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
String channel = recipient.getNotificationChannel();
if (channel != null) {
@ -202,36 +217,38 @@ public class NotificationChannels {
* channels. Performs database operations and should therefore be invoked on a background thread.
*/
@WorkerThread
public static void updateMessagesLedColor(@NonNull Context context, @NonNull String color) {
public static synchronized void updateMessagesLedColor(@NonNull Context context, @NonNull String color) {
if (!supported()) {
return;
}
Log.i(TAG, "Updating LED color.");
NotificationManager notificationManager = getNotificationManager(context);
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
updateMessageChannel(context, channel -> setLedPreference(channel, color));
updateAllRecipientChannelLedColors(context, notificationManager, color);
ensureCustomChannelConsistency(context);
}
/**
* @return The message ringtone set for the default message channel.
*/
public static @NonNull Uri getMessageRingtone(@NonNull Context context) {
public static synchronized @NonNull Uri getMessageRingtone(@NonNull Context context) {
if (!supported()) {
return Uri.EMPTY;
}
Uri sound = getNotificationManager(context).getNotificationChannel(getMessagesChannel(context)).getSound();
Uri sound = ServiceUtil.getNotificationManager(context).getNotificationChannel(getMessagesChannel(context)).getSound();
return sound == null ? Uri.EMPTY : sound;
}
public static @Nullable Uri getMessageRingtone(@NonNull Context context, @NonNull Recipient recipient) {
public static synchronized @Nullable Uri getMessageRingtone(@NonNull Context context, @NonNull Recipient recipient) {
if (!supported() || recipient.getNotificationChannel() == null) {
return null;
}
NotificationManager notificationManager = getNotificationManager(context);
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
NotificationChannel channel = notificationManager.getNotificationChannel(recipient.getNotificationChannel());
if (!channelExists(channel)) {
@ -245,7 +262,7 @@ public class NotificationChannels {
/**
* Update the message ringtone for the default message channel.
*/
public static void updateMessageRingtone(@NonNull Context context, @Nullable Uri uri) {
public static synchronized void updateMessageRingtone(@NonNull Context context, @Nullable Uri uri) {
if (!supported()) {
return;
}
@ -263,42 +280,43 @@ public class NotificationChannels {
* This has to update the database, and therefore should be run on a background thread.
*/
@WorkerThread
public static void updateMessageRingtone(@NonNull Context context, @NonNull Recipient recipient, @Nullable Uri uri) {
public static synchronized void updateMessageRingtone(@NonNull Context context, @NonNull Recipient recipient, @Nullable Uri uri) {
if (!supported() || recipient.getNotificationChannel() == null) {
return;
}
Log.i(TAG, "Updating recipient message ringtone with URI: " + String.valueOf(uri));
String newChannelId = generateChannelIdFor(recipient.getAddress());
boolean success = updateExistingChannel(getNotificationManager(context),
boolean success = updateExistingChannel(ServiceUtil.getNotificationManager(context),
recipient.getNotificationChannel(),
generateChannelIdFor(recipient.getAddress()),
channel -> channel.setSound(uri == null ? Settings.System.DEFAULT_NOTIFICATION_URI : uri, getRingtoneAudioAttributes()));
DatabaseFactory.getRecipientDatabase(context).setNotificationChannel(recipient, success ? newChannelId : null);
ensureCustomChannelConsistency(context);
}
/**
* @return The vibrate settings for the default message channel.
*/
public static boolean getMessageVibrate(@NonNull Context context) {
public static synchronized boolean getMessageVibrate(@NonNull Context context) {
if (!supported()) {
return false;
}
return getNotificationManager(context).getNotificationChannel(getMessagesChannel(context)).shouldVibrate();
return ServiceUtil.getNotificationManager(context).getNotificationChannel(getMessagesChannel(context)).shouldVibrate();
}
/**
* @return The vibrate setting for a specific recipient. If that recipient has no channel, this
* will return the setting for the default message channel.
*/
public static boolean getMessageVibrate(@NonNull Context context, @NonNull Recipient recipient) {
public static synchronized boolean getMessageVibrate(@NonNull Context context, @NonNull Recipient recipient) {
if (!supported()) {
return getMessageVibrate(context);
}
NotificationManager notificationManager = getNotificationManager(context);
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
NotificationChannel channel = notificationManager.getNotificationChannel(recipient.getNotificationChannel());
if (!channelExists(channel)) {
@ -312,7 +330,7 @@ public class NotificationChannels {
/**
* Sets the vibrate property for the default message channel.
*/
public static void updateMessageVibrate(@NonNull Context context, boolean enabled) {
public static synchronized void updateMessageVibrate(@NonNull Context context, boolean enabled) {
if (!supported()) {
return;
}
@ -328,7 +346,7 @@ public class NotificationChannels {
* This has to update the database and should therefore be run on a background thread.
*/
@WorkerThread
public static void updateMessageVibrate(@NonNull Context context, @NonNull Recipient recipient, VibrateState vibrateState) {
public static synchronized void updateMessageVibrate(@NonNull Context context, @NonNull Recipient recipient, VibrateState vibrateState) {
if (!supported() || recipient.getNotificationChannel() == null) {
return ;
}
@ -336,25 +354,26 @@ public class NotificationChannels {
boolean enabled = vibrateState == VibrateState.DEFAULT ? getMessageVibrate(context) : vibrateState == VibrateState.ENABLED;
String newChannelId = generateChannelIdFor(recipient.getAddress());
boolean success = updateExistingChannel(getNotificationManager(context),
boolean success = updateExistingChannel(ServiceUtil.getNotificationManager(context),
recipient.getNotificationChannel(),
newChannelId,
channel -> channel.enableVibration(enabled));
DatabaseFactory.getRecipientDatabase(context).setNotificationChannel(recipient, success ? newChannelId : null);
ensureCustomChannelConsistency(context);
}
/**
* 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.
*/
public static void updateContactChannelName(@NonNull Context context, @NonNull Recipient recipient) {
public static synchronized void updateContactChannelName(@NonNull Context context, @NonNull Recipient recipient) {
if (!supported() || recipient.getNotificationChannel() == null) {
return;
}
Log.i(TAG, "Updating contact channel name");
NotificationManager notificationManager = getNotificationManager(context);
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
if (notificationManager.getNotificationChannel(recipient.getNotificationChannel()) == null) {
Log.w(TAG, "Tried to update the name of a channel, but that channel doesn't exist.");
@ -368,6 +387,38 @@ public class NotificationChannels {
notificationManager.createNotificationChannel(channel);
}
@TargetApi(26)
@WorkerThread
public static synchronized void ensureCustomChannelConsistency(@NonNull Context context) {
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
RecipientDatabase db = DatabaseFactory.getRecipientDatabase(context);
List<Recipient> customRecipients = new ArrayList<>();
Set<String> customChannelIds = new HashSet<>();
Set<String> existingChannelIds = Stream.of(notificationManager.getNotificationChannels()).map(NotificationChannel::getId).collect(Collectors.toSet());
try (RecipientDatabase.RecipientReader reader = db.getRecipientsWithNotificationChannels()) {
Recipient recipient;
while ((recipient = reader.getNext()) != null) {
customRecipients.add(recipient);
customChannelIds.add(recipient.getNotificationChannel());
}
}
for (NotificationChannel existingChannel : notificationManager.getNotificationChannels()) {
if (existingChannel.getId().startsWith(CONTACT_PREFIX) && !customChannelIds.contains(existingChannel.getId())) {
notificationManager.deleteNotificationChannel(existingChannel.getId());
} else if (existingChannel.getId().startsWith(MESSAGES_PREFIX) && !existingChannel.getId().equals(getMessagesChannel(context))) {
notificationManager.deleteNotificationChannel(existingChannel.getId());
}
}
for (Recipient customRecipient : customRecipients) {
if (!existingChannelIds.contains(customRecipient.getNotificationChannel())) {
db.setNotificationChannel(customRecipient, null);
}
}
}
@TargetApi(26)
private static void onCreate(@NonNull Context context, @NonNull NotificationManager notificationManager) {
NotificationChannelGroup messagesGroup = new NotificationChannelGroup(CATEGORY_MESSAGES, context.getResources().getString(R.string.NotificationChannel_group_messages));
@ -465,11 +516,13 @@ public class NotificationChannels {
database.setNotificationChannel(recipient, success ? newChannelId : null);
}
}
ensureCustomChannelConsistency(context);
}
@TargetApi(26)
private static void updateMessageChannel(@NonNull Context context, @NonNull ChannelUpdater updater) {
NotificationManager notificationManager = getNotificationManager(context);
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
int existingVersion = TextSecurePreferences.getNotificationMessagesChannelVersion(context);
int newVersion = existingVersion + 1;
@ -508,13 +561,6 @@ public class NotificationChannels {
.build();
}
@TargetApi(26)
private static @NonNull NotificationManager getNotificationManager(@NonNull Context context) {
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
assert notificationManager != null;
return notificationManager;
}
@TargetApi(26)
private static boolean channelExists(@Nullable NotificationChannel channel) {
return channel != null && !NotificationChannel.DEFAULT_CHANNEL_ID.equals(channel.getId());