play ringtone through notification

in 484f633180 we switched from letting the notification play the ringtone and handle the vibration to doing it ourselves.

this had two reasons. The ringtone selector in the android notification settings was broken at times and we wanted to silence the notification when pressing volume down.

This commit essentially reverts that change. We fixed the ringtone selection by handling it internally in Conversations and simply recreating the entire channel. Silencing the notification can be achieved by re-posting it with onlyAlertOnce set to true (I guess we didn’t know that at the time)
This commit is contained in:
Daniel Gultsch 2024-04-17 09:12:49 +02:00
parent cf5c038611
commit 9d01af239a
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
4 changed files with 183 additions and 104 deletions

View file

@ -18,12 +18,10 @@ import android.graphics.Bitmap;
import android.graphics.Typeface; import android.graphics.Typeface;
import android.media.AudioAttributes; import android.media.AudioAttributes;
import android.media.AudioManager; import android.media.AudioManager;
import android.media.Ringtone;
import android.media.RingtoneManager; import android.media.RingtoneManager;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.SystemClock; import android.os.SystemClock;
import android.os.Vibrator;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.provider.Settings; import android.provider.Settings;
import android.text.SpannableString; import android.text.SpannableString;
@ -45,9 +43,12 @@ import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.graphics.drawable.IconCompat; import androidx.core.graphics.drawable.IconCompat;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Splitter;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.primitives.Ints;
import eu.siacs.conversations.AppSettings; import eu.siacs.conversations.AppSettings;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
@ -73,6 +74,7 @@ import eu.siacs.conversations.xmpp.jingle.Media;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
@ -80,24 +82,17 @@ import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
public class NotificationService { public class NotificationService {
private static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE =
Executors.newSingleThreadScheduledExecutor();
public static final Object CATCHUP_LOCK = new Object(); public static final Object CATCHUP_LOCK = new Object();
private static final int LED_COLOR = 0xff00ff00; private static final int LED_COLOR = 0xff00ff00;
private static final long[] CALL_PATTERN = {0, 500, 300, 600}; private static final long[] CALL_PATTERN = {0, 500, 300, 600, 3000};
private static final String MESSAGES_GROUP = "eu.siacs.conversations.messages"; private static final String MESSAGES_GROUP = "eu.siacs.conversations.messages";
private static final String MISSED_CALLS_GROUP = "eu.siacs.conversations.missed_calls"; private static final String MISSED_CALLS_GROUP = "eu.siacs.conversations.missed_calls";
@ -121,9 +116,9 @@ public class NotificationService {
private long mLastNotification; private long mLastNotification;
private static final String INCOMING_CALLS_NOTIFICATION_CHANNEL = "incoming_calls_channel"; private static final String INCOMING_CALLS_NOTIFICATION_CHANNEL = "incoming_calls_channel";
private static final String INCOMING_CALLS_NOTIFICATION_CHANNEL_PREFIX =
"incoming_calls_channel#";
private static final String MESSAGES_NOTIFICATION_CHANNEL = "messages"; private static final String MESSAGES_NOTIFICATION_CHANNEL = "messages";
private Ringtone currentlyPlayingRingtone = null;
private ScheduledFuture<?> vibrationFuture;
NotificationService(final XmppConnectionService service) { NotificationService(final XmppConnectionService service) {
this.mXmppConnectionService = service; this.mXmppConnectionService = service;
@ -164,6 +159,7 @@ public class NotificationService {
notificationManager.deleteNotificationChannel("export"); notificationManager.deleteNotificationChannel("export");
notificationManager.deleteNotificationChannel("incoming_calls"); notificationManager.deleteNotificationChannel("incoming_calls");
notificationManager.deleteNotificationChannel(INCOMING_CALLS_NOTIFICATION_CHANNEL);
notificationManager.createNotificationChannelGroup( notificationManager.createNotificationChannelGroup(
new NotificationChannelGroup( new NotificationChannelGroup(
@ -214,19 +210,7 @@ public class NotificationService {
exportChannel.setGroup("status"); exportChannel.setGroup("status");
notificationManager.createNotificationChannel(exportChannel); notificationManager.createNotificationChannel(exportChannel);
final NotificationChannel incomingCallsChannel = createInitialIncomingCallChannelIfNecessary(c);
new NotificationChannel(
INCOMING_CALLS_NOTIFICATION_CHANNEL,
c.getString(R.string.incoming_calls_channel_name),
NotificationManager.IMPORTANCE_HIGH);
incomingCallsChannel.setSound(null, null);
incomingCallsChannel.setShowBadge(false);
incomingCallsChannel.setLightColor(LED_COLOR);
incomingCallsChannel.enableLights(true);
incomingCallsChannel.setGroup("calls");
incomingCallsChannel.setBypassDnd(true);
incomingCallsChannel.enableVibration(false);
notificationManager.createNotificationChannel(incomingCallsChannel);
final NotificationChannel ongoingCallsChannel = final NotificationChannel ongoingCallsChannel =
new NotificationChannel( new NotificationChannel(
@ -259,7 +243,7 @@ public class NotificationService {
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION),
new AudioAttributes.Builder() new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT) .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
.build()); .build());
messagesChannel.setLightColor(LED_COLOR); messagesChannel.setLightColor(LED_COLOR);
final int dat = 70; final int dat = 70;
@ -298,6 +282,98 @@ public class NotificationService {
notificationManager.createNotificationChannel(deliveryFailedChannel); notificationManager.createNotificationChannel(deliveryFailedChannel);
} }
@RequiresApi(api = Build.VERSION_CODES.O)
private static void createInitialIncomingCallChannelIfNecessary(final Context context) {
final var currentIteration = getCurrentIncomingCallChannelIteration(context);
if (currentIteration.isPresent()) {
return;
}
createInitialIncomingCallChannel(context);
}
@RequiresApi(api = Build.VERSION_CODES.O)
public static Optional<Integer> getCurrentIncomingCallChannelIteration(final Context context) {
final var notificationManager = context.getSystemService(NotificationManager.class);
for (final NotificationChannel channel : notificationManager.getNotificationChannels()) {
final String id = channel.getId();
if (Strings.isNullOrEmpty(id)) {
continue;
}
if (id.startsWith(INCOMING_CALLS_NOTIFICATION_CHANNEL_PREFIX)) {
final var parts = Splitter.on('#').splitToList(id);
if (parts.size() == 2) {
final var iteration = Ints.tryParse(parts.get(1));
if (iteration != null) {
return Optional.of(iteration);
}
}
}
}
return Optional.absent();
}
@RequiresApi(api = Build.VERSION_CODES.O)
public static Optional<NotificationChannel> getCurrentIncomingCallChannel(
final Context context) {
final var iteration = getCurrentIncomingCallChannelIteration(context);
return iteration.transform(
i -> {
final var notificationManager =
context.getSystemService(NotificationManager.class);
return notificationManager.getNotificationChannel(
INCOMING_CALLS_NOTIFICATION_CHANNEL_PREFIX + i);
});
}
@RequiresApi(api = Build.VERSION_CODES.O)
private static void createInitialIncomingCallChannel(final Context context) {
final var appSettings = new AppSettings(context);
final var ringtoneUri = appSettings.getRingtone();
createIncomingCallChannel(context, ringtoneUri, 0);
}
@RequiresApi(api = Build.VERSION_CODES.O)
public static void recreateIncomingCallChannel(final Context context, final Uri ringtone) {
final var currentIteration = getCurrentIncomingCallChannelIteration(context);
final int nextIteration;
if (currentIteration.isPresent()) {
final var notificationManager = context.getSystemService(NotificationManager.class);
notificationManager.deleteNotificationChannel(
INCOMING_CALLS_NOTIFICATION_CHANNEL_PREFIX + currentIteration.get());
nextIteration = currentIteration.get() + 1;
} else {
nextIteration = 0;
}
createIncomingCallChannel(context, ringtone, nextIteration);
}
@RequiresApi(api = Build.VERSION_CODES.O)
private static void createIncomingCallChannel(
final Context context, final Uri ringtoneUri, final int iteration) {
final var notificationManager = context.getSystemService(NotificationManager.class);
final var id = INCOMING_CALLS_NOTIFICATION_CHANNEL_PREFIX + iteration;
Log.d(Config.LOGTAG, "creating incoming call channel with id " + id);
final NotificationChannel incomingCallsChannel =
new NotificationChannel(
id,
context.getString(R.string.incoming_calls_channel_name),
NotificationManager.IMPORTANCE_HIGH);
incomingCallsChannel.setSound(
ringtoneUri,
new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build());
incomingCallsChannel.setShowBadge(false);
incomingCallsChannel.setLightColor(LED_COLOR);
incomingCallsChannel.enableLights(true);
incomingCallsChannel.setGroup("calls");
incomingCallsChannel.setBypassDnd(true);
incomingCallsChannel.enableVibration(true);
incomingCallsChannel.setVibrationPattern(CALL_PATTERN);
notificationManager.createNotificationChannel(incomingCallsChannel);
}
private boolean notifyMessage(final Message message) { private boolean notifyMessage(final Message message) {
final Conversation conversation = (Conversation) message.getConversation(); final Conversation conversation = (Conversation) message.getConversation();
return message.getStatus() == Message.STATUS_RECEIVED return message.getStatus() == Message.STATUS_RECEIVED
@ -471,58 +547,13 @@ public class NotificationService {
public synchronized void startRinging( public synchronized void startRinging(
final AbstractJingleConnection.Id id, final Set<Media> media) { final AbstractJingleConnection.Id id, final Set<Media> media) {
showIncomingCallNotification(id, media); showIncomingCallNotification(id, media, false);
final NotificationManager notificationManager =
mXmppConnectionService.getSystemService(NotificationManager.class);
final int currentInterruptionFilter;
if (notificationManager != null) {
currentInterruptionFilter = notificationManager.getCurrentInterruptionFilter();
} else {
currentInterruptionFilter = 1; // INTERRUPTION_FILTER_ALL
}
if (currentInterruptionFilter != 1) {
Log.d(
Config.LOGTAG,
"do not ring or vibrate because interruption filter has been set to "
+ currentInterruptionFilter);
return;
}
final ScheduledFuture<?> currentVibrationFuture = this.vibrationFuture;
this.vibrationFuture =
SCHEDULED_EXECUTOR_SERVICE.scheduleAtFixedRate(
new VibrationRunnable(), 0, 3, TimeUnit.SECONDS);
if (currentVibrationFuture != null) {
currentVibrationFuture.cancel(true);
}
final var preexistingRingtone = this.currentlyPlayingRingtone;
if (preexistingRingtone != null) {
preexistingRingtone.stop();
}
final SharedPreferences preferences =
PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
final Resources resources = mXmppConnectionService.getResources();
final String ringtonePreference =
preferences.getString(
"call_ringtone", resources.getString(R.string.incoming_call_ringtone));
if (Strings.isNullOrEmpty(ringtonePreference)) {
Log.d(Config.LOGTAG, "ringtone has been set to none");
return;
}
final Uri uri = Uri.parse(ringtonePreference);
this.currentlyPlayingRingtone = RingtoneManager.getRingtone(mXmppConnectionService, uri);
if (this.currentlyPlayingRingtone == null) {
Log.d(Config.LOGTAG, "unable to find ringtone for uri " + uri);
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
this.currentlyPlayingRingtone.setLooping(true);
}
Log.d(Config.LOGTAG,"start playing ringtone: "+uri);
this.currentlyPlayingRingtone.play();
} }
private void showIncomingCallNotification( private void showIncomingCallNotification(
final AbstractJingleConnection.Id id, final Set<Media> media) { final AbstractJingleConnection.Id id,
final Set<Media> media,
final boolean onlyAlertOnce) {
final Intent fullScreenIntent = final Intent fullScreenIntent =
new Intent(mXmppConnectionService, RtpSessionActivity.class); new Intent(mXmppConnectionService, RtpSessionActivity.class);
fullScreenIntent.putExtra( fullScreenIntent.putExtra(
@ -532,9 +563,21 @@ public class NotificationService {
fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId); fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId);
fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
final int channelIteration;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
channelIteration = getCurrentIncomingCallChannelIteration(mXmppConnectionService).or(0);
} else {
channelIteration = 0;
}
final var channelId = INCOMING_CALLS_NOTIFICATION_CHANNEL_PREFIX + channelIteration;
Log.d(
Config.LOGTAG,
"showing incoming call notification on channel "
+ channelId
+ ", onlyAlertOnce="
+ onlyAlertOnce);
final NotificationCompat.Builder builder = final NotificationCompat.Builder builder =
new NotificationCompat.Builder( new NotificationCompat.Builder(mXmppConnectionService, channelId);
mXmppConnectionService, INCOMING_CALLS_NOTIFICATION_CHANNEL);
if (media.contains(Media.VIDEO)) { if (media.contains(Media.VIDEO)) {
builder.setSmallIcon(R.drawable.ic_videocam_24dp); builder.setSmallIcon(R.drawable.ic_videocam_24dp);
builder.setContentTitle( builder.setContentTitle(
@ -553,11 +596,20 @@ public class NotificationService {
if (systemAccount != null) { if (systemAccount != null) {
builder.addPerson(systemAccount.toString()); builder.addPerson(systemAccount.toString());
} }
if (!onlyAlertOnce) {
final var appSettings = new AppSettings(mXmppConnectionService);
final var ringtone = appSettings.getRingtone();
if (ringtone != null) {
builder.setSound(ringtone, AudioManager.STREAM_RING);
}
builder.setVibrate(CALL_PATTERN);
}
builder.setOnlyAlertOnce(onlyAlertOnce);
builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName()); builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName());
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
builder.setPriority(NotificationCompat.PRIORITY_HIGH); builder.setPriority(NotificationCompat.PRIORITY_HIGH);
builder.setCategory(NotificationCompat.CATEGORY_CALL); builder.setCategory(NotificationCompat.CATEGORY_CALL);
PendingIntent pendingIntent = createPendingRtpSession(id, Intent.ACTION_VIEW, 101); final PendingIntent pendingIntent = createPendingRtpSession(id, Intent.ACTION_VIEW, 101);
builder.setFullScreenIntent(pendingIntent, true); builder.setFullScreenIntent(pendingIntent, true);
builder.setContentIntent(pendingIntent); // old androids need this? builder.setContentIntent(pendingIntent); // old androids need this?
builder.setOngoing(true); builder.setOngoing(true);
@ -579,6 +631,10 @@ public class NotificationService {
.build()); .build());
modifyIncomingCall(builder); modifyIncomingCall(builder);
final Notification notification = builder.build(); final Notification notification = builder.build();
notification.audioAttributes = new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
.build();
notification.flags = notification.flags | Notification.FLAG_INSISTENT; notification.flags = notification.flags | Notification.FLAG_INSISTENT;
notify(INCOMING_CALL_NOTIFICATION_ID, notification); notify(INCOMING_CALL_NOTIFICATION_ID, notification);
} }
@ -643,25 +699,25 @@ public class NotificationService {
} }
public void cancelIncomingCallNotification() { public void cancelIncomingCallNotification() {
stopSoundAndVibration();
cancel(INCOMING_CALL_NOTIFICATION_ID); cancel(INCOMING_CALL_NOTIFICATION_ID);
} }
public boolean stopSoundAndVibration() { public boolean stopSoundAndVibration() {
int stopped = 0; final var jingleRtpConnection =
if (this.currentlyPlayingRingtone != null) { mXmppConnectionService.getJingleConnectionManager().getOngoingRtpConnection();
if (this.currentlyPlayingRingtone.isPlaying()) { if (jingleRtpConnection == null) {
Log.d(Config.LOGTAG, "stop playing ringtone"); return false;
++stopped;
}
this.currentlyPlayingRingtone.stop();
} }
if (this.vibrationFuture != null && !this.vibrationFuture.isCancelled()) { final var notificationManager = mXmppConnectionService.getSystemService(NotificationManager.class);
Log.d(Config.LOGTAG, "stop vibration"); if (Iterables.any(
this.vibrationFuture.cancel(true); Arrays.asList(notificationManager.getActiveNotifications()),
++stopped; n -> n.getId() == INCOMING_CALL_NOTIFICATION_ID)) {
Log.d(Config.LOGTAG, "stopping sound and vibration for incoming call notification");
showIncomingCallNotification(
jingleRtpConnection.getId(), jingleRtpConnection.getMedia(), true);
return true;
} }
return stopped > 0; return false;
} }
public static void cancelIncomingCallNotification(final Context context) { public static void cancelIncomingCallNotification(final Context context) {
@ -2006,14 +2062,4 @@ public class NotificationService {
return lastTime; return lastTime;
} }
} }
private class VibrationRunnable implements Runnable {
@Override
public void run() {
final Vibrator vibrator =
(Vibrator) mXmppConnectionService.getSystemService(Context.VIBRATOR_SERVICE);
vibrator.vibrate(CALL_PATTERN, -1);
}
}
} }

View file

@ -1,7 +1,9 @@
package eu.siacs.conversations.ui.fragment.settings; package eu.siacs.conversations.ui.fragment.settings;
import android.app.NotificationChannel;
import android.media.RingtoneManager; import android.media.RingtoneManager;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
@ -10,9 +12,12 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.preference.Preference; import androidx.preference.Preference;
import com.google.common.base.Optional;
import eu.siacs.conversations.AppSettings; import eu.siacs.conversations.AppSettings;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.services.NotificationService;
import eu.siacs.conversations.ui.activity.result.PickRingtone; import eu.siacs.conversations.ui.activity.result.PickRingtone;
import eu.siacs.conversations.utils.Compatibility; import eu.siacs.conversations.utils.Compatibility;
@ -41,6 +46,9 @@ public class NotificationsSettingsFragment extends XmppPreferenceFragment {
final Uri uri = PickRingtone.noneToNull(result); final Uri uri = PickRingtone.noneToNull(result);
appSettings().setRingtone(uri); appSettings().setRingtone(uri);
Log.i(Config.LOGTAG, "User set ringtone to " + uri); Log.i(Config.LOGTAG, "User set ringtone to " + uri);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationService.recreateIncomingCallChannel(requireContext(), uri);
}
}); });
@Override @Override
@ -107,12 +115,25 @@ public class NotificationsSettingsFragment extends XmppPreferenceFragment {
} }
private void pickRingtone() { private void pickRingtone() {
final Uri uri = appSettings().getRingtone(); final Optional<Uri> channelRingtone;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
channelRingtone =
NotificationService.getCurrentIncomingCallChannel(requireContext())
.transform(NotificationChannel::getSound);
} else {
channelRingtone = Optional.absent();
}
final Uri uri;
if (channelRingtone.isPresent()) {
uri = channelRingtone.get();
Log.d(Config.LOGTAG, "ringtone came from channel");
} else {
uri = appSettings().getRingtone();
}
Log.i(Config.LOGTAG, "current ringtone: " + uri); Log.i(Config.LOGTAG, "current ringtone: " + uri);
this.pickRingtoneLauncher.launch(uri); this.pickRingtoneLauncher.launch(uri);
} }
private AppSettings appSettings() { private AppSettings appSettings() {
return new AppSettings(requireContext()); return new AppSettings(requireContext());
} }

View file

@ -648,6 +648,18 @@ public class JingleConnectionManager extends AbstractConnectionManager {
return Optional.absent(); return Optional.absent();
} }
public JingleRtpConnection getOngoingRtpConnection() {
for(final AbstractJingleConnection jingleConnection : this.connections.values()) {
if (jingleConnection instanceof JingleRtpConnection jingleRtpConnection) {
if (jingleRtpConnection.isTerminated()) {
continue;
}
return jingleRtpConnection;
}
}
return null;
}
void finishConnectionOrThrow(final AbstractJingleConnection connection) { void finishConnectionOrThrow(final AbstractJingleConnection connection) {
final AbstractJingleConnection.Id id = connection.getId(); final AbstractJingleConnection.Id id = connection.getId();
if (this.connections.remove(id) == null) { if (this.connections.remove(id) == null) {

View file

@ -2804,7 +2804,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
request, request,
(account, response) -> { (account, response) -> {
final var iceServers = IceServers.parse(response); final var iceServers = IceServers.parse(response);
if (iceServers.size() == 0) { if (iceServers.isEmpty()) {
Log.w( Log.w(
Config.LOGTAG, Config.LOGTAG,
id.account.getJid().asBareJid() id.account.getJid().asBareJid()