let Conversations (not Android) play ringtone and vibration

Daniel Gultsch created

fixes #3972 fixes #3801 fixes #3931

Change summary

src/main/java/eu/siacs/conversations/services/NotificationService.java    | 83 
src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java           | 11 
src/main/java/eu/siacs/conversations/utils/Compatibility.java             |  9 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java |  2 
src/main/res/xml/preferences.xml                                          | 13 
5 files changed, 77 insertions(+), 41 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/services/NotificationService.java 🔗

@@ -12,10 +12,12 @@ import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.Typeface;
 import android.media.AudioAttributes;
+import android.media.Ringtone;
 import android.media.RingtoneManager;
 import android.net.Uri;
 import android.os.Build;
 import android.os.SystemClock;
+import android.os.Vibrator;
 import android.preference.PreferenceManager;
 import android.text.SpannableString;
 import android.text.style.StyleSpan;
@@ -43,6 +45,10 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 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.regex.Matcher;
 import java.util.regex.Pattern;
@@ -70,12 +76,13 @@ import eu.siacs.conversations.xmpp.jingle.Media;
 
 public class NotificationService {
 
+    private static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE = Executors.newSingleThreadScheduledExecutor();
+
     public static final Object CATCHUP_LOCK = new Object();
 
     private static final int LED_COLOR = 0xff00ff00;
 
-    private static final int CALL_DAT = 120;
-    private static final long[] CALL_PATTERN = {0, 3 * CALL_DAT, CALL_DAT, CALL_DAT, 3 * CALL_DAT, CALL_DAT, CALL_DAT};
+    private static final long[] CALL_PATTERN = {0, 500, 300, 600};
 
     private static final String CONVERSATIONS_GROUP = "eu.siacs.conversations";
     private static final int NOTIFICATION_ID_MULTIPLIER = 1024 * 1024;
@@ -92,6 +99,10 @@ public class NotificationService {
     private boolean mIsInForeground;
     private long mLastNotification;
 
+    private static final String INCOMING_CALLS_NOTIFICATION_CHANNEL = "incoming_calls_channel";
+    private Ringtone currentlyPlayingRingtone = null;
+    private ScheduledFuture<?> vibrationFuture;
+
     NotificationService(final XmppConnectionService service) {
         this.mXmppConnectionService = service;
     }
@@ -129,6 +140,7 @@ public class NotificationService {
         }
 
         notificationManager.deleteNotificationChannel("export");
+        notificationManager.deleteNotificationChannel("incoming_calls");
 
         notificationManager.createNotificationChannelGroup(new NotificationChannelGroup("status", c.getString(R.string.notification_group_status_information)));
         notificationManager.createNotificationChannelGroup(new NotificationChannelGroup("chats", c.getString(R.string.notification_group_messages)));
@@ -162,20 +174,16 @@ public class NotificationService {
         exportChannel.setGroup("status");
         notificationManager.createNotificationChannel(exportChannel);
 
-        final NotificationChannel incomingCallsChannel = new NotificationChannel("incoming_calls",
+        final NotificationChannel incomingCallsChannel = new NotificationChannel(INCOMING_CALLS_NOTIFICATION_CHANNEL,
                 c.getString(R.string.incoming_calls_channel_name),
                 NotificationManager.IMPORTANCE_HIGH);
-        incomingCallsChannel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE), new AudioAttributes.Builder()
-                .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
-                .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
-                .build());
+        incomingCallsChannel.setSound(null, null);
         incomingCallsChannel.setShowBadge(false);
         incomingCallsChannel.setLightColor(LED_COLOR);
         incomingCallsChannel.enableLights(true);
         incomingCallsChannel.setGroup("calls");
         incomingCallsChannel.setBypassDnd(true);
-        incomingCallsChannel.enableVibration(true);
-        incomingCallsChannel.setVibrationPattern(CALL_PATTERN);
+        incomingCallsChannel.enableVibration(false);
         notificationManager.createNotificationChannel(incomingCallsChannel);
 
         final NotificationChannel ongoingCallsChannel = new NotificationChannel("ongoing_calls",
@@ -387,14 +395,32 @@ public class NotificationService {
         notify(DELIVERY_FAILED_NOTIFICATION_ID, summaryNotification);
     }
 
-    public void showIncomingCallNotification(final AbstractJingleConnection.Id id, final Set<Media> media) {
+    public void startRinging(final AbstractJingleConnection.Id id, final Set<Media> media) {
+        showIncomingCallNotification(id, media);
+        final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
+        final Resources resources = mXmppConnectionService.getResources();
+        final Uri uri = Uri.parse(preferences.getString("call_ringtone", resources.getString(R.string.incoming_call_ringtone)));
+        this.currentlyPlayingRingtone = RingtoneManager.getRingtone(mXmppConnectionService, uri);
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            this.currentlyPlayingRingtone.setLooping(true);
+        }
+        this.currentlyPlayingRingtone.play();
+        this.vibrationFuture = SCHEDULED_EXECUTOR_SERVICE.scheduleAtFixedRate(
+                new VibrationRunnable(),
+                0,
+                3,
+                TimeUnit.SECONDS
+        );
+    }
+
+    private void showIncomingCallNotification(final AbstractJingleConnection.Id id, final Set<Media> media) {
         final Intent fullScreenIntent = new Intent(mXmppConnectionService, RtpSessionActivity.class);
         fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, id.account.getJid().asBareJid().toEscapedString());
         fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString());
         fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId);
         fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
-        final NotificationCompat.Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "incoming_calls");
+        final NotificationCompat.Builder builder = new NotificationCompat.Builder(mXmppConnectionService, INCOMING_CALLS_NOTIFICATION_CHANNEL);
         if (media.contains(Media.VIDEO)) {
             builder.setSmallIcon(R.drawable.ic_videocam_white_24dp);
             builder.setContentTitle(mXmppConnectionService.getString(R.string.rtp_state_incoming_video_call));
@@ -468,9 +494,23 @@ public class NotificationService {
     }
 
     public void cancelIncomingCallNotification() {
+        stopSoundAndVibration();
         cancel(INCOMING_CALL_NOTIFICATION_ID);
     }
 
+    public void stopSoundAndVibration() {
+        if (this.currentlyPlayingRingtone != null) {
+            if (this.currentlyPlayingRingtone.isPlaying()) {
+                Log.d(Config.LOGTAG, "stop playing ring tone");
+            }
+            this.currentlyPlayingRingtone.stop();
+        }
+        if (this.vibrationFuture != null && !this.vibrationFuture.isCancelled()) {
+            Log.d(Config.LOGTAG, "cancel vibration");
+            this.vibrationFuture.cancel(true);
+        }
+    }
+
     public static void cancelIncomingCallNotification(final Context context) {
         final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
         try {
@@ -636,17 +676,7 @@ public class NotificationService {
         }
     }
 
-    private void modifyIncomingCall(Builder mBuilder) {
-        final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService);
-        final Resources resources = mXmppConnectionService.getResources();
-        final String ringtone = preferences.getString("call_ringtone", resources.getString(R.string.incoming_call_ringtone));
-        mBuilder.setVibrate(CALL_PATTERN);
-        final Uri uri = Uri.parse(ringtone);
-        try {
-            mBuilder.setSound(fixRingtoneUri(uri));
-        } catch (SecurityException e) {
-            Log.d(Config.LOGTAG, "unable to use custom notification sound " + uri.toString());
-        }
+    private void modifyIncomingCall(final Builder mBuilder) {
         mBuilder.setPriority(NotificationCompat.PRIORITY_HIGH);
         setNotificationColor(mBuilder);
         mBuilder.setLights(LED_COLOR, 2000, 3000);
@@ -1253,4 +1283,13 @@ public class NotificationService {
             Log.d(Config.LOGTAG, "unable to cancel notification", e);
         }
     }
+
+    private class VibrationRunnable implements Runnable {
+
+        @Override
+        public void run() {
+            final Vibrator vibrator = (Vibrator) mXmppConnectionService.getSystemService(Context.VIBRATOR_SERVICE);
+            vibrator.vibrate(CALL_PATTERN, -1);
+        }
+    }
 }

src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java 🔗

@@ -14,6 +14,7 @@ import android.os.PowerManager;
 import android.os.SystemClock;
 import android.util.Log;
 import android.util.Rational;
+import android.view.KeyEvent;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
@@ -146,6 +147,16 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
         return super.onCreateOptionsMenu(menu);
     }
 
+    @Override
+    public boolean onKeyDown(final int keyCode, final KeyEvent event) {
+        if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN){
+            if (xmppConnectionService != null) {
+                xmppConnectionService.getNotificationService().stopSoundAndVibration();
+            }
+        }
+        return super.onKeyDown(keyCode, event);
+    }
+
     private boolean isHelpButtonVisible() {
         try {
             return STATES_SHOWING_HELP_BUTTON.contains(requireRtpConnection().getEndUserState());

src/main/java/eu/siacs/conversations/utils/Compatibility.java 🔗

@@ -16,6 +16,7 @@ import androidx.annotation.BoolRes;
 import androidx.core.content.ContextCompat;
 
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 
 import eu.siacs.conversations.Config;
@@ -31,12 +32,10 @@ public class Compatibility {
             "led",
             "notification_ringtone",
             "notification_headsup",
-            "vibrate_on_notification",
-            "call_ringtone"
+            "vibrate_on_notification"
     );
-    private static final List<String> UNUESD_SETTINGS_PRE_TWENTYSIX = Arrays.asList(
-            "message_notification_settings",
-            "call_notification_settings"
+    private static final List<String> UNUESD_SETTINGS_PRE_TWENTYSIX = Collections.singletonList(
+            "message_notification_settings"
     );
 
 

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java 🔗

@@ -597,7 +597,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
     private void startRinging() {
         Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received call from " + id.with + ". start ringing");
         ringingTimeoutFuture = jingleConnectionManager.schedule(this::ringingTimeout, BUSY_TIME_OUT, TimeUnit.SECONDS);
-        xmppConnectionService.getNotificationService().showIncomingCallNotification(id, getMedia());
+        xmppConnectionService.getNotificationService().startRinging(id, getMedia());
     }
 
     private synchronized void ringingTimeout() {

src/main/res/xml/preferences.xml 🔗

@@ -113,19 +113,6 @@
                     android:value="messages" />
             </intent>
         </PreferenceScreen>
-        <PreferenceScreen
-            android:key="call_notification_settings"
-            android:summary="@string/pref_more_notification_settings_summary"
-            android:title="@string/pref_incoming_call_notification_settings">
-            <intent android:action="android.settings.CHANNEL_NOTIFICATION_SETTINGS">
-                <extra
-                    android:name="android.provider.extra.APP_PACKAGE"
-                    android:value="@string/applicationId" />
-                <extra
-                    android:name="android.provider.extra.CHANNEL_ID"
-                    android:value="incoming_calls" />
-            </intent>
-        </PreferenceScreen>
         <RingtonePreference
             android:defaultValue="@string/notification_ringtone"
             android:key="notification_ringtone"