show proper notification on incoming call

Daniel Gultsch created

Change summary

src/main/AndroidManifest.xml                                              |  3 
src/main/java/eu/siacs/conversations/services/NotificationService.java    | 99 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java  |  6 
src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java           | 19 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java | 12 
src/main/res/values/strings.xml                                           |  4 
6 files changed, 119 insertions(+), 24 deletions(-)

Detailed changes

src/main/AndroidManifest.xml 🔗

@@ -10,7 +10,6 @@
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
     <uses-permission android:name="android.permission.WAKE_LOCK" />
-    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
     <uses-permission android:name="android.permission.VIBRATE" />
     <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
@@ -33,6 +32,8 @@
     <uses-permission android:name="android.permission.CAMERA" />
     <uses-permission android:name="android.permission.RECORD_AUDIO" />
     <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
+    <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
+    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
 
     <uses-permission
         android:name="android.permission.READ_PHONE_STATE"

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

@@ -55,12 +55,14 @@ import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.persistance.FileBackend;
 import eu.siacs.conversations.ui.ConversationsActivity;
 import eu.siacs.conversations.ui.EditAccountActivity;
+import eu.siacs.conversations.ui.RtpSessionActivity;
 import eu.siacs.conversations.ui.TimePreference;
 import eu.siacs.conversations.utils.AccountUtils;
 import eu.siacs.conversations.utils.Compatibility;
 import eu.siacs.conversations.utils.GeoHelper;
 import eu.siacs.conversations.utils.UIHelper;
 import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
 
 public class NotificationService {
 
@@ -70,9 +72,10 @@ public class NotificationService {
 
     private static final String CONVERSATIONS_GROUP = "eu.siacs.conversations";
     private static final int NOTIFICATION_ID_MULTIPLIER = 1024 * 1024;
-    private static final int NOTIFICATION_ID = 2 * NOTIFICATION_ID_MULTIPLIER;
     static final int FOREGROUND_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 4;
+    private static final int NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 2;
     private static final int ERROR_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 6;
+    private static final int INCOMING_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 8;
     private final XmppConnectionService mXmppConnectionService;
     private final LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<>();
     private final HashMap<Conversation, AtomicInteger> mBacklogMessageCounter = new HashMap<>();
@@ -100,6 +103,14 @@ public class NotificationService {
         return Pattern.compile("(?<=(^|\\s))" + Pattern.quote(nick) + "(?=\\s|$|\\p{Punct})");
     }
 
+    private static boolean isImageMessage(Message message) {
+        return message.getType() != Message.TYPE_TEXT
+                && message.getTransferable() == null
+                && !message.isDeleted()
+                && message.getEncryption() != Message.ENCRYPTION_PGP
+                && message.getFileParams().height > 0;
+    }
+
     @RequiresApi(api = Build.VERSION_CODES.O)
     void initializeChannels() {
         final Context c = mXmppConnectionService;
@@ -112,6 +123,7 @@ public class NotificationService {
 
         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)));
+        notificationManager.createNotificationChannelGroup(new NotificationChannelGroup("calls", c.getString(R.string.notification_group_calls)));
         final NotificationChannel foregroundServiceChannel = new NotificationChannel("foreground",
                 c.getString(R.string.foreground_service_channel_name),
                 NotificationManager.IMPORTANCE_MIN);
@@ -141,6 +153,20 @@ public class NotificationService {
         exportChannel.setGroup("status");
         notificationManager.createNotificationChannel(exportChannel);
 
+        final NotificationChannel incomingCallsChannel = new NotificationChannel("incoming_calls",
+                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.setShowBadge(false);
+        incomingCallsChannel.setLightColor(LED_COLOR);
+        incomingCallsChannel.enableLights(true);
+        incomingCallsChannel.setGroup("calls");
+        notificationManager.createNotificationChannel(incomingCallsChannel);
+
+
         final NotificationChannel messagesChannel = new NotificationChannel("messages",
                 c.getString(R.string.messages_channel_name),
                 NotificationManager.IMPORTANCE_HIGH);
@@ -300,6 +326,53 @@ public class NotificationService {
         }
     }
 
+    public void showIncomingCallNotification(AbstractJingleConnection.Id id) {
+        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 PendingIntent pendingIntent = PendingIntent.getActivity(mXmppConnectionService, 101, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+        final NotificationCompat.Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "incoming_calls");
+        builder.setSmallIcon(R.drawable.ic_call_white_24dp);
+        builder.setContentTitle(mXmppConnectionService.getString(R.string.rtp_state_incoming_call));
+        builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName());
+        builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
+        builder.setPriority(NotificationCompat.PRIORITY_HIGH);
+        builder.setCategory(NotificationCompat.CATEGORY_CALL);
+        builder.setFullScreenIntent(createPendingRtpSession(id, Intent.ACTION_VIEW, 101), true);
+        builder.setOngoing(true);
+        builder.addAction(new NotificationCompat.Action.Builder(
+                R.drawable.ic_call_end_white_48dp,
+                mXmppConnectionService.getString(R.string.dismiss_call),
+                createDismissCall(id.sessionId, 102))
+                .build());
+        builder.addAction(new NotificationCompat.Action.Builder(
+                R.drawable.ic_call_white_24dp,
+                mXmppConnectionService.getString(R.string.answer_call),
+                createPendingRtpSession(id, RtpSessionActivity.ACTION_ACCEPT, 103))
+                .build());
+        final Notification notification = builder.build();
+        notification.flags = notification.flags | Notification.FLAG_INSISTENT;
+        notify(INCOMING_CALL_NOTIFICATION_ID, builder.build());
+    }
+
+    private PendingIntent createPendingRtpSession(final AbstractJingleConnection.Id id, final String action, final int requestCode) {
+        final Intent fullScreenIntent = new Intent(mXmppConnectionService, RtpSessionActivity.class);
+        fullScreenIntent.setAction(action);
+        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);
+        return PendingIntent.getActivity(mXmppConnectionService, requestCode, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+    }
+
+    public void cancelIncomingCallNotification() {
+        cancel(INCOMING_CALL_NOTIFICATION_ID);
+    }
+
     private void pushNow(final Message message) {
         mXmppConnectionService.updateUnreadCountBadge();
         if (!notify(message)) {
@@ -467,7 +540,7 @@ public class NotificationService {
     private Builder buildMultipleConversation(final boolean notify, final boolean quietHours) {
         final Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService, quietHours ? "quiet_hours" : (notify ? "messages" : "silent_messages"));
         final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
-        style.setBigContentTitle(mXmppConnectionService.getString(R.string.x_unread_conversations,notifications.size()));
+        style.setBigContentTitle(mXmppConnectionService.getString(R.string.x_unread_conversations, notifications.size()));
         final StringBuilder names = new StringBuilder();
         Conversation conversation = null;
         for (final ArrayList<Message> messages : notifications.values()) {
@@ -652,7 +725,7 @@ public class NotificationService {
         return builder.build();
     }
 
-    private void modifyForTextOnly(final Builder builder,  final ArrayList<Message> messages) {
+    private void modifyForTextOnly(final Builder builder, final ArrayList<Message> messages) {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
             final Conversation conversation = (Conversation) messages.get(0).getConversation();
             final Person.Builder meBuilder = new Person.Builder().setName(mXmppConnectionService.getString(R.string.me));
@@ -668,7 +741,7 @@ public class NotificationService {
             for (Message message : messages) {
                 final Person sender = message.getStatus() == Message.STATUS_RECEIVED ? getPerson(message) : null;
                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isImageMessage(message)) {
-                    final Uri dataUri = FileBackend.getMediaUri(mXmppConnectionService,mXmppConnectionService.getFileBackend().getFile(message));
+                    final Uri dataUri = FileBackend.getMediaUri(mXmppConnectionService, mXmppConnectionService.getFileBackend().getFile(message));
                     NotificationCompat.MessagingStyle.Message imageMessage = new NotificationCompat.MessagingStyle.Message(UIHelper.getMessagePreview(mXmppConnectionService, message).first, message.getTimeSent(), sender);
                     if (dataUri != null) {
                         imageMessage.setData(message.getMimeType(), dataUri);
@@ -683,7 +756,7 @@ public class NotificationService {
         } else {
             if (messages.get(0).getConversation().getMode() == Conversation.MODE_SINGLE) {
                 builder.setStyle(new NotificationCompat.BigTextStyle().bigText(getMergedBodies(messages)));
-                final CharSequence preview = UIHelper.getMessagePreview(mXmppConnectionService, messages.get(messages.size()-1)).first;
+                final CharSequence preview = UIHelper.getMessagePreview(mXmppConnectionService, messages.get(messages.size() - 1)).first;
                 builder.setContentText(preview);
                 builder.setTicker(preview);
                 builder.setNumber(messages.size());
@@ -726,14 +799,6 @@ public class NotificationService {
         return image;
     }
 
-    private static boolean isImageMessage(Message message) {
-        return message.getType() != Message.TYPE_TEXT
-                && message.getTransferable() == null
-                && !message.isDeleted()
-                && message.getEncryption() != Message.ENCRYPTION_PGP
-                && message.getFileParams().height > 0;
-    }
-
     private Message getFirstDownloadableMessage(final Iterable<Message> messages) {
         for (final Message message : messages) {
             if (message.getTransferable() != null || (message.getType() == Message.TYPE_TEXT && message.treatAsDownloadable())) {
@@ -834,6 +899,14 @@ public class NotificationService {
         return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 16), intent, PendingIntent.FLAG_UPDATE_CURRENT);
     }
 
+    private PendingIntent createDismissCall(String sessionId, int requestCode) {
+        final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
+        intent.setAction(XmppConnectionService.ACTION_DISMISS_CALL);
+        intent.setPackage(mXmppConnectionService.getPackageName());
+        intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, sessionId);
+        return PendingIntent.getService(mXmppConnectionService, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+    }
+
     private PendingIntent createSnoozeIntent(Conversation conversation) {
         final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
         intent.setAction(XmppConnectionService.ACTION_SNOOZE);

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

@@ -107,6 +107,7 @@ import eu.siacs.conversations.parser.PresenceParser;
 import eu.siacs.conversations.persistance.DatabaseBackend;
 import eu.siacs.conversations.persistance.FileBackend;
 import eu.siacs.conversations.ui.ChooseAccountForProfilePictureActivity;
+import eu.siacs.conversations.ui.RtpSessionActivity;
 import eu.siacs.conversations.ui.SettingsActivity;
 import eu.siacs.conversations.ui.UiCallback;
 import eu.siacs.conversations.ui.interfaces.OnAvatarPublication;
@@ -165,6 +166,7 @@ public class XmppConnectionService extends Service {
     public static final String ACTION_IDLE_PING = "idle_ping";
     public static final String ACTION_FCM_TOKEN_REFRESH = "fcm_token_refresh";
     public static final String ACTION_FCM_MESSAGE_RECEIVED = "fcm_message_received";
+    public static final String ACTION_DISMISS_CALL = "dismiss_call";
     private static final String ACTION_POST_CONNECTIVITY_CHANGE = "eu.siacs.conversations.POST_CONNECTIVITY_CHANGE";
 
     private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp";
@@ -638,6 +640,10 @@ public class XmppConnectionService extends Service {
                         }
                     });
                     break;
+                case ACTION_DISMISS_CALL:
+                    final String sessionId = intent.getStringExtra(RtpSessionActivity.EXTRA_SESSION_ID);
+                    Log.d(Config.LOGTAG,"received intent to dismiss call with session id "+sessionId);
+                    break;
                 case ACTION_DISMISS_ERROR_NOTIFICATIONS:
                     dismissErrorNotifications();
                     break;

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

@@ -25,6 +25,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
     public static final String EXTRA_WITH = "with";
     public static final String EXTRA_SESSION_ID = "session_id";
 
+    public static final String ACTION_ACCEPT = "accept";
+
     private WeakReference<JingleRtpConnection> rtpConnectionReference;
 
     private ActivityRtpSessionBinding binding;
@@ -32,6 +34,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+                | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
+                | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+                | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
+        ;
+        Log.d(Config.LOGTAG, "RtpSessionActivity.onCreate()");
         this.binding = DataBindingUtil.setContentView(this, R.layout.activity_rtp_session);
         this.binding.rejectCall.setOnClickListener(this::rejectCall);
         this.binding.endCall.setOnClickListener(this::endCall);
@@ -41,7 +49,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
     @Override
     public void onStart() {
         super.onStart();
-        Log.d(Config.LOGTAG,"RtpSessionActivity.onStart()");
+        Log.d(Config.LOGTAG, "RtpSessionActivity.onStart()");
     }
 
     private void endCall(View view) {
@@ -78,8 +86,13 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
             this.rtpConnectionReference = reference;
             binding.with.setText(getWith().getDisplayName());
             final RtpEndUserState currentState = requireRtpConnection().getEndUserState();
+            final String action = intent.getAction();
             updateStateDisplay(currentState);
             updateButtonConfiguration(currentState);
+            if (ACTION_ACCEPT.equals(action)) {
+                Log.d(Config.LOGTAG,"intent action was accept");
+                requireRtpConnection().acceptCall();
+            }
         }
     }
 
@@ -137,12 +150,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
     public void onJingleRtpConnectionUpdate(Account account, Jid with, RtpEndUserState state) {
         final AbstractJingleConnection.Id id = requireRtpConnection().getId();
         if (account == id.account && id.with.equals(with)) {
-            runOnUiThread(()->{
+            runOnUiThread(() -> {
                 updateStateDisplay(state);
                 updateButtonConfiguration(state);
             });
         } else {
-            Log.d(Config.LOGTAG,"received update for other rtp session");
+            Log.d(Config.LOGTAG, "received update for other rtp session");
         }
 
     }

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

@@ -228,12 +228,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");
-        final Intent intent = new Intent(xmppConnectionService, RtpSessionActivity.class);
-        intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, id.account.getJid().asBareJid().toEscapedString());
-        intent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString());
-        intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId);
-        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        xmppConnectionService.startActivity(intent);
+        xmppConnectionService.getNotificationService().showIncomingCallNotification(id);
     }
 
     private void receiveProceed(final Jid from, final Element proceed) {
@@ -342,6 +337,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
 
     public void rejectCall() {
         Log.d(Config.LOGTAG, "todo rejecting call");
+        xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
     }
 
     public void endCall() {
@@ -359,6 +355,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
 
     private void acceptCallFromProposed() {
         transitionOrThrow(State.PROCEED);
+        xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
         final MessagePacket messagePacket = new MessagePacket();
         messagePacket.setTo(id.with);
         //Note that Movim needs 'accept', correct is 'proceed' https://github.com/movim/movim/issues/916
@@ -368,7 +365,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
     }
 
     private void acceptCallFromSessionInitialized() {
-
+        xmppConnectionService.getNotificationService().cancelIncomingCallNotification();
+        throw new IllegalStateException("accepting from this state has not been implemented yet");
     }
 
     private synchronized boolean isInState(State... state) {

src/main/res/values/strings.xml 🔗

@@ -748,7 +748,9 @@
     <string name="error_channel_name">Connectivity Problems</string>
     <string name="error_channel_description">This notification category is used to display a notification in case there is a problem connecting to an account.</string>
     <string name="notification_group_messages">Messages</string>
+    <string name="notification_group_calls">Calls</string>
     <string name="messages_channel_name">Messages</string>
+    <string name="incoming_calls_channel_name">Incoming calls</string>
     <string name="silent_messages_channel_name">Silent messages</string>
     <string name="silent_messages_channel_description">This notification group is used to display notifications that should not trigger any sound. For example when being active on another device (Grace Period).</string>
     <string name="pref_more_notification_settings">Notification Settings</string>
@@ -890,6 +892,8 @@
     <string name="rtp_state_connected">Connected</string>
     <string name="rtp_state_accepting_call">Accepting call</string>
     <string name="rtp_state_ending_call">Ending call</string>
+    <string name="answer_call">Answer</string>
+    <string name="dismiss_call">Dismiss</string>
     <plurals name="view_users">
         <item quantity="one">View %1$d Participant</item>
         <item quantity="other">View %1$d Participants</item>