Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />

<!-- force compiling emojipicker on sdk<21; runtime checks are required then -->
<uses-sdk tools:overrideLibrary="androidx.emoji2.emojipicker"/>
Expand Down Expand Up @@ -400,11 +402,25 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
</activity>

<service
android:name=".videochat.CallIntegrationService"
android:exported="true"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE">
<intent-filter>
<action android:name="android.telecom.ConnectionService" />
</intent-filter>
</service>

<service
android:name=".connect.KeepAliveService"
android:foregroundServiceType="dataSync"
android:enabled="true" />

<service
android:name=".videochat.CallForegroundService"
android:foregroundServiceType="phoneCall"
android:enabled="true" />

<service
android:name=".geolocation.LocationBackgroundService"
android:foregroundServiceType="location" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}

}
Loading