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(); + } + } + +}