diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index eba9a14cf..212af7c42 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -40,6 +40,8 @@
+
+
@@ -400,11 +402,25 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
+
+
+
+
+
+
+
+
diff --git a/src/main/java/org/thoughtcrime/securesms/videochat/CallForegroundService.java b/src/main/java/org/thoughtcrime/securesms/videochat/CallForegroundService.java
new file mode 100644
index 000000000..e5a07b632
--- /dev/null
+++ b/src/main/java/org/thoughtcrime/securesms/videochat/CallForegroundService.java
@@ -0,0 +1,169 @@
+package org.thoughtcrime.securesms.videochat;
+
+import android.annotation.TargetApi;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.os.Build;
+import android.os.IBinder;
+import android.util.Log;
+
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.NotificationManagerCompat;
+import androidx.core.app.Person;
+import androidx.core.content.ContextCompat;
+import androidx.core.graphics.drawable.IconCompat;
+
+import com.b44t.messenger.DcChat;
+import com.b44t.messenger.DcContext;
+
+import org.thoughtcrime.securesms.ApplicationContext;
+import org.thoughtcrime.securesms.ConversationListActivity;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.notifications.NotificationCenter;
+import org.thoughtcrime.securesms.util.IntentUtils;
+
+public class CallForegroundService extends Service {
+
+ private static final String TAG = CallForegroundService.class.getSimpleName();
+
+ private static final String ACC_ID_EXTRA = "acc_id";
+ private static final String CALL_ID_EXTRA = "call_id";
+ private static final String PAYLOAD_EXTRA = "payload";
+
+ static CallForegroundService s_this = null;
+
+ public static void startSelf(Context context, int accId, int callId, String payload) {
+ Intent intent = new Intent(context, CallForegroundService.class);
+ intent.putExtra(ACC_ID_EXTRA, accId);
+ intent.putExtra(CALL_ID_EXTRA, callId);
+ intent.putExtra(PAYLOAD_EXTRA, payload);
+ try {
+ ContextCompat.startForegroundService(context, intent);
+ } catch(Exception e) {
+ Log.i(TAG, "Error calling ContextCompat.startForegroundService()", e);
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ Log.i("DeltaChat", "*** CallForegroundService.onCreate()");
+ s_this = this;
+ startForeground(NotificationCenter.ID_PERMANENT, createNotification());
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ Log.i("DeltaChat", "*** CallForegroundService.onStartCommand()");
+ int accId = intent.getIntExtra(ACC_ID_EXTRA, -1);
+ int callId = intent.getIntExtra(CALL_ID_EXTRA, 0);
+ String payload = intent.getStringExtra(PAYLOAD_EXTRA);
+ Notification notif = buildIncomingCall(this, accId, callId, payload);
+ try {
+ startForeground(NotificationCenter.ID_PERMANENT, notif);
+ }
+ catch (Exception e) {
+ Log.i(TAG, "Error", e);
+ }
+ return START_STICKY;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ public void onDestroy() {
+ Log.i("DeltaChat", "*** CallForegroundService.onDestroy()");
+ // the service will be restarted due to START_STICKY automatically, there's nothing more to do.
+ }
+
+ static public CallForegroundService getInstance()
+ {
+ return s_this; // may be null
+ }
+
+ public Notification buildIncomingCall(Context service, int accId, int callId, String payload) {
+ NotificationCenter notificationCenter = ApplicationContext.getInstance(this).notificationCenter;
+ NotificationManagerCompat notificationManager = NotificationManagerCompat.from(service);
+ DcContext dcContext = ApplicationContext.dcAccounts.getAccount(accId);
+ int chatId = dcContext.getMsg(callId).getChatId();
+ DcChat dcChat = dcContext.getChat(chatId);
+ String name = dcChat.getName();
+ NotificationCenter.ChatData chatData = new NotificationCenter.ChatData(accId, chatId);
+ String notificationChannel = notificationCenter.getCallNotificationChannel(notificationManager, chatData, name);
+
+ PendingIntent declineIntent = notificationCenter.getDeclineCallIntent(chatData, callId);
+ PendingIntent answerIntent = notificationCenter.getAnswerIntent(chatData, callId, payload);
+ Bitmap bitmap = notificationCenter.getAvatar(dcChat);
+
+ Person.Builder callerBuilder = new Person.Builder()
+ .setName(name);
+
+ if (bitmap != null) {
+ callerBuilder.setIcon(IconCompat.createWithBitmap(bitmap));
+ }
+
+ NotificationCompat.CallStyle style = NotificationCompat.CallStyle
+ .forIncomingCall(callerBuilder.build(), declineIntent, answerIntent);
+
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(service, notificationChannel)
+ .setSmallIcon(R.drawable.icon_notification)
+ .setColor(getResources().getColor(R.color.delta_primary))
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setCategory(NotificationCompat.CATEGORY_CALL)
+ .setOngoing(true)
+ .setOnlyAlertOnce(false)
+ .setTicker(name)
+ .setContentTitle(name)
+ .setContentText("Incoming Call")
+ .setStyle(style);
+
+ if (bitmap != null) {
+ builder.setLargeIcon(bitmap);
+ }
+
+ Notification notif = builder.build();
+ notif.flags = notif.flags | Notification.FLAG_INSISTENT;
+ return notif;
+ }
+
+ private Notification createNotification()
+ {
+ Intent intent = new Intent(this, ConversationListActivity.class);
+ PendingIntent contentIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | IntentUtils.FLAG_MUTABLE());
+ // a notification _must_ contain a small icon, a title and a text, see https://developer.android.com/guide/topics/ui/notifiers/notifications.html#Required
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
+
+ builder.setContentTitle(getString(R.string.app_name));
+ builder.setContentText("Incoming Call");
+
+ builder.setPriority(NotificationCompat.PRIORITY_MIN);
+ builder.setWhen(0);
+ builder.setContentIntent(contentIntent);
+ builder.setSmallIcon(R.drawable.notification_permanent);
+ if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O) {
+ createFgNotificationChannel(this);
+ builder.setChannelId(NotificationCenter.CH_PERMANENT);
+ }
+ return builder.build();
+ }
+
+ private static boolean ch_created = false;
+ @TargetApi(Build.VERSION_CODES.O)
+ static private void createFgNotificationChannel(Context context) {
+ if(!ch_created) {
+ ch_created = true;
+ NotificationChannel channel = new NotificationChannel(NotificationCenter.CH_PERMANENT,
+ "Receive messages in background.", NotificationManager.IMPORTANCE_MIN); // IMPORTANCE_DEFAULT will play a sound
+ channel.setDescription("Ensure reliable message receiving.");
+ NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
+ notificationManager.createNotificationChannel(channel);
+ }
+ }
+}
diff --git a/src/main/java/org/thoughtcrime/securesms/videochat/CallIntegrationService.java b/src/main/java/org/thoughtcrime/securesms/videochat/CallIntegrationService.java
new file mode 100644
index 000000000..1e7f523e4
--- /dev/null
+++ b/src/main/java/org/thoughtcrime/securesms/videochat/CallIntegrationService.java
@@ -0,0 +1,120 @@
+package org.thoughtcrime.securesms.videochat;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.telecom.Connection;
+import android.telecom.ConnectionRequest;
+import android.telecom.ConnectionService;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.util.Log;
+import android.widget.Toast;
+
+import androidx.annotation.RequiresApi;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+@RequiresApi(api = Build.VERSION_CODES.M)
+public class CallIntegrationService extends ConnectionService {
+ private final static String TAG = CallIntegrationService.class.getSimpleName();
+
+ private static final String EXTRA_ACCOUNT_ID = "accid";
+ private static final String EXTRA_CHAT_ID = "chatid";
+ private static final String EXTRA_CALL_ID = "callid";
+ private static final String EXTRA_CALL_PAYLOAD = "callpayload";
+
+ @Override
+ public Connection onCreateOutgoingConnection(final PhoneAccountHandle phoneAccountHandle, final ConnectionRequest request) {
+ Log.d(TAG, "onCreateOutgoingConnection(" + phoneAccountHandle.getId() + ", " + request.getAddress() + ")");
+ final Uri uri = request.getAddress();
+ final Bundle extras = request.getExtras();
+ if (uri == null || !Arrays.asList("dc", "tel").contains(uri.getScheme())) {
+ return Connection.createFailedConnection(new DisconnectCause(DisconnectCause.ERROR, "invalid address"));
+ }
+ final int chatId = extras.getInt(EXTRA_CHAT_ID);
+ return new Connection() {
+ @Override
+ public void onAnswer() {
+ super.onAnswer();
+ }
+ };
+ }
+
+ @Override
+ public Connection onCreateIncomingConnection(final PhoneAccountHandle phoneAccountHandle, final ConnectionRequest request) {
+ Log.d(TAG, "onCreateIncomingConnection()");
+ return new Connection() {
+ @Override
+ public void onAnswer() {
+ super.onAnswer();
+ }
+ };
+ }
+
+ private static PhoneAccountHandle getHandle(final Context context, int accountId) {
+ final ComponentName componentName = new ComponentName(context, CallIntegrationService.class);
+ return new PhoneAccountHandle(componentName, accountId+"");
+ }
+
+ public static void registerPhoneAccount(final Context context, final int accountId) {
+ final PhoneAccountHandle handle = getHandle(context, accountId);
+ final TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (telecomManager.getOwnSelfManagedPhoneAccounts().contains(handle)) {
+ Log.d(TAG, "a phone account for " + accountId + " already exists");
+ return;
+ }
+ }
+ final PhoneAccount.Builder builder =
+ PhoneAccount.builder(getHandle(context, accountId), accountId+"");
+ builder.setSupportedUriSchemes(Collections.singletonList("dc"));
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ builder.setCapabilities(
+ PhoneAccount.CAPABILITY_SELF_MANAGED
+ | PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING);
+ }
+ final PhoneAccount phoneAccount = builder.build();
+ telecomManager.registerPhoneAccount(phoneAccount);
+ }
+
+ public static void placeCall(Context context, int accountId, int chatId) {
+ final Bundle extras = new Bundle();
+ extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, getHandle(context, accountId));
+ final Bundle outgoingCallExtras = new Bundle();
+ outgoingCallExtras.putInt(EXTRA_ACCOUNT_ID, accountId);
+ outgoingCallExtras.putInt(EXTRA_CHAT_ID, chatId);
+ extras.putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, outgoingCallExtras);
+
+ final Uri address = Uri.parse("tel:0");
+ try {
+ context.getSystemService(TelecomManager.class).placeCall(address, extras);
+ } catch (final SecurityException e) {
+ Log.e(TAG, "call integration not available", e);
+ Toast.makeText(context, "call integration not available", Toast.LENGTH_LONG).show();
+ }
+ }
+
+ public static void addNewIncomingCall(final Context context, int accId, int chatId, int callId, String payload) {
+ final PhoneAccountHandle phoneAccountHandle = getHandle(context, accId);
+ final Bundle bundle = new Bundle();
+ bundle.putString(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, chatId+"");
+ final Bundle extras = new Bundle();
+ extras.putInt(EXTRA_CALL_ID, callId);
+ extras.putString(EXTRA_CALL_PAYLOAD, payload);
+ bundle.putBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS, extras);
+
+ try {
+ context.getSystemService(TelecomManager.class).addNewIncomingCall(phoneAccountHandle, bundle);
+ } catch (final SecurityException e) {
+ Log.e(TAG, "call integration not available", e);
+ Toast.makeText(context, "call integration not available", Toast.LENGTH_LONG).show();
+ }
+ }
+
+}