CallIntegrationConnectionService.java

  1package eu.siacs.conversations.services;
  2
  3import android.Manifest;
  4import android.content.ComponentName;
  5import android.content.Context;
  6import android.content.Intent;
  7import android.content.ServiceConnection;
  8import android.content.pm.PackageManager;
  9import android.net.Uri;
 10import android.os.Build;
 11import android.os.Bundle;
 12import android.os.IBinder;
 13import android.telecom.Connection;
 14import android.telecom.ConnectionRequest;
 15import android.telecom.ConnectionService;
 16import android.telecom.DisconnectCause;
 17import android.telecom.PhoneAccount;
 18import android.telecom.PhoneAccountHandle;
 19import android.telecom.TelecomManager;
 20import android.telecom.VideoProfile;
 21import android.util.Log;
 22import android.widget.Toast;
 23
 24import androidx.annotation.NonNull;
 25
 26import com.google.common.collect.ImmutableSet;
 27import com.google.common.util.concurrent.ListenableFuture;
 28import com.google.common.util.concurrent.SettableFuture;
 29
 30import eu.siacs.conversations.Config;
 31import eu.siacs.conversations.R;
 32import eu.siacs.conversations.entities.Account;
 33import eu.siacs.conversations.entities.Contact;
 34import eu.siacs.conversations.ui.RtpSessionActivity;
 35import eu.siacs.conversations.xmpp.Jid;
 36import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
 37import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
 38import eu.siacs.conversations.xmpp.jingle.Media;
 39import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
 40import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
 41
 42import java.util.Collection;
 43import java.util.Collections;
 44import java.util.Set;
 45import java.util.UUID;
 46import java.util.concurrent.ExecutionException;
 47import java.util.concurrent.TimeUnit;
 48import java.util.concurrent.TimeoutException;
 49
 50public class CallIntegrationConnectionService extends ConnectionService {
 51
 52    private ListenableFuture<ServiceConnectionService> serviceFuture;
 53
 54    @Override
 55    public void onCreate() {
 56        Log.d(Config.LOGTAG, "CallIntegrationService.onCreate()");
 57        super.onCreate();
 58        this.serviceFuture = ServiceConnectionService.bindService(this);
 59    }
 60
 61    @Override
 62    public void onDestroy() {
 63        Log.d(Config.LOGTAG, "destroying CallIntegrationConnectionService");
 64        super.onDestroy();
 65        final ServiceConnection serviceConnection;
 66        try {
 67            serviceConnection = serviceFuture.get().serviceConnection;
 68        } catch (final Exception e) {
 69            Log.d(Config.LOGTAG, "could not fetch service connection", e);
 70            return;
 71        }
 72        this.unbindService(serviceConnection);
 73    }
 74
 75    private static Connection createOutgoingRtpConnection(
 76            final XmppConnectionService service,
 77            final String phoneAccountHandle,
 78            final Jid with,
 79            final Set<Media> media) {
 80        if (service == null) {
 81            Log.d(
 82                    Config.LOGTAG,
 83                    "CallIntegrationConnection service was unable to bind to XmppConnectionService");
 84            return Connection.createFailedConnection(
 85                    new DisconnectCause(DisconnectCause.ERROR, "service connection not found"));
 86        }
 87        final var account = service.findAccountByUuid(phoneAccountHandle);
 88        return createOutgoingRtpConnection(service, account, with, media);
 89    }
 90
 91    private static Connection createOutgoingRtpConnection(
 92            @NonNull final XmppConnectionService service,
 93            @NonNull final Account account,
 94            final Jid with,
 95            final Set<Media> media) {
 96        Log.d(Config.LOGTAG, "create outgoing rtp connection!");
 97        final Intent intent = new Intent(service, RtpSessionActivity.class);
 98        intent.setAction(Intent.ACTION_VIEW);
 99        intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, account.getJid().toEscapedString());
100        intent.putExtra(RtpSessionActivity.EXTRA_WITH, with.toEscapedString());
101        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
102        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
103        final Connection callIntegration;
104        if (with.isBareJid()) {
105            final var contact = account.getRoster().getContact(with);
106            if (Config.JINGLE_MESSAGE_INIT_STRICT_OFFLINE_CHECK
107                    && contact.getPresences().isEmpty()) {
108                intent.putExtra(
109                        RtpSessionActivity.EXTRA_LAST_REPORTED_STATE,
110                        RtpEndUserState.CONTACT_OFFLINE.toString());
111                callIntegration =
112                        Connection.createFailedConnection(
113                                new DisconnectCause(DisconnectCause.ERROR, "contact is offline"));
114                // we can use a JMI 'finish' message to notify the contact of a call we never
115                // actually attempted
116                // sendJingleFinishMessage(service, contact, Reason.CONNECTIVITY_ERROR);
117            } else {
118                final var proposal =
119                        service.getJingleConnectionManager()
120                                .proposeJingleRtpSession(account, with, media);
121                intent.putExtra(
122                        RtpSessionActivity.EXTRA_LAST_REPORTED_STATE,
123                        RtpEndUserState.FINDING_DEVICE.toString());
124                callIntegration = proposal.getCallIntegration();
125            }
126            if (Media.audioOnly(media)) {
127                intent.putExtra(
128                        RtpSessionActivity.EXTRA_LAST_ACTION,
129                        RtpSessionActivity.ACTION_MAKE_VOICE_CALL);
130            } else {
131                intent.putExtra(
132                        RtpSessionActivity.EXTRA_LAST_ACTION,
133                        RtpSessionActivity.ACTION_MAKE_VIDEO_CALL);
134            }
135        } else {
136            final JingleRtpConnection jingleRtpConnection =
137                    service.getJingleConnectionManager().initializeRtpSession(account, with, media);
138            final String sessionId = jingleRtpConnection.getId().sessionId;
139            intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, sessionId);
140            callIntegration = jingleRtpConnection.getCallIntegration();
141        }
142        service.startActivity(intent);
143        return callIntegration;
144    }
145
146    private static void sendJingleFinishMessage(
147            final XmppConnectionService service, final Contact contact, final Reason reason) {
148        service.getJingleConnectionManager()
149                .sendJingleMessageFinish(contact, UUID.randomUUID().toString(), reason);
150    }
151
152    @Override
153    public Connection onCreateOutgoingConnection(
154            final PhoneAccountHandle phoneAccountHandle, final ConnectionRequest request) {
155        Log.d(Config.LOGTAG, "onCreateOutgoingConnection(" + request.getAddress() + ")");
156        final var uri = request.getAddress();
157        final var jid = Jid.ofEscaped(uri.getSchemeSpecificPart());
158        final var extras = request.getExtras();
159        final int videoState = extras.getInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE);
160        final Set<Media> media =
161                videoState == VideoProfile.STATE_AUDIO_ONLY
162                        ? ImmutableSet.of(Media.AUDIO)
163                        : ImmutableSet.of(Media.AUDIO, Media.VIDEO);
164        Log.d(Config.LOGTAG, "jid=" + jid);
165        Log.d(Config.LOGTAG, "phoneAccountHandle:" + phoneAccountHandle.getId());
166        Log.d(Config.LOGTAG, "media " + media);
167        final var service = ServiceConnectionService.get(this.serviceFuture);
168        return createOutgoingRtpConnection(service, phoneAccountHandle.getId(), jid, media);
169    }
170
171    @Override
172    public Connection onCreateIncomingConnection(
173            final PhoneAccountHandle phoneAccountHandle, final ConnectionRequest request) {
174        Log.d(Config.LOGTAG, "onCreateIncomingConnection()");
175        final var service = ServiceConnectionService.get(this.serviceFuture);
176        final Bundle extras = request.getExtras();
177        final Bundle extraExtras = extras.getBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS);
178        final String incomingCallAddress =
179                extras.getString(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS);
180        final String sid = extraExtras == null ? null : extraExtras.getString("sid");
181        Log.d(Config.LOGTAG, "sid " + sid);
182        final Uri uri = incomingCallAddress == null ? null : Uri.parse(incomingCallAddress);
183        Log.d(Config.LOGTAG, "uri=" + uri);
184        if (uri == null || sid == null) {
185            return Connection.createFailedConnection(
186                    new DisconnectCause(
187                            DisconnectCause.ERROR,
188                            "connection request is missing required information"));
189        }
190        if (service == null) {
191            return Connection.createFailedConnection(
192                    new DisconnectCause(DisconnectCause.ERROR, "service connection not found"));
193        }
194        final var jid = Jid.ofEscaped(uri.getSchemeSpecificPart());
195        final Account account = service.findAccountByUuid(phoneAccountHandle.getId());
196        final var weakReference =
197                service.getJingleConnectionManager().findJingleRtpConnection(account, jid, sid);
198        if (weakReference == null) {
199            Log.d(Config.LOGTAG, "no connection found for " + jid + " and sid=" + sid);
200            return Connection.createFailedConnection(
201                    new DisconnectCause(DisconnectCause.ERROR, "no incoming connection found"));
202        }
203        final var jingleRtpConnection = weakReference.get();
204        if (jingleRtpConnection == null) {
205            Log.d(Config.LOGTAG, "connection has been terminated");
206            return Connection.createFailedConnection(
207                    new DisconnectCause(DisconnectCause.ERROR, "connection has been terminated"));
208        }
209        Log.d(Config.LOGTAG, "registering call integration for incoming call");
210        return jingleRtpConnection.getCallIntegration();
211    }
212
213    public static void registerPhoneAccount(final Context context, final Account account) {
214        final var builder =
215                PhoneAccount.builder(getHandle(context, account), account.getJid().asBareJid());
216        builder.setSupportedUriSchemes(Collections.singletonList("xmpp"));
217        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
218            builder.setCapabilities(
219                    PhoneAccount.CAPABILITY_SELF_MANAGED
220                            | PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING);
221        }
222        final var phoneAccount = builder.build();
223
224        context.getSystemService(TelecomManager.class).registerPhoneAccount(phoneAccount);
225    }
226
227    public static void registerPhoneAccounts(
228            final Context context, final Collection<Account> accounts) {
229        for (final Account account : accounts) {
230            registerPhoneAccount(context, account);
231        }
232    }
233
234    public static void unregisterPhoneAccount(final Context context, final Account account) {
235        context.getSystemService(TelecomManager.class)
236                .unregisterPhoneAccount(getHandle(context, account));
237    }
238
239    public static PhoneAccountHandle getHandle(final Context context, final Account account) {
240        final var competentName =
241                new ComponentName(context, CallIntegrationConnectionService.class);
242        return new PhoneAccountHandle(competentName, account.getUuid());
243    }
244
245    public static void placeCall(
246            final XmppConnectionService service,
247            final Account account,
248            final Jid with,
249            final Set<Media> media) {
250        if (CallIntegration.selfManaged()) {
251            final var extras = new Bundle();
252            extras.putParcelable(
253                    TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, getHandle(service, account));
254            extras.putInt(
255                    TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
256                    Media.audioOnly(media)
257                            ? VideoProfile.STATE_AUDIO_ONLY
258                            : VideoProfile.STATE_BIDIRECTIONAL);
259            if (service.checkSelfPermission(Manifest.permission.MANAGE_OWN_CALLS)
260                    != PackageManager.PERMISSION_GRANTED) {
261                Toast.makeText(service, R.string.no_permission_to_place_call, Toast.LENGTH_SHORT)
262                        .show();
263                return;
264            }
265            service.getSystemService(TelecomManager.class)
266                    .placeCall(CallIntegration.address(with), extras);
267        } else {
268            final var connection = createOutgoingRtpConnection(service, account, with, media);
269            if (connection != null) {
270                Log.d(
271                        Config.LOGTAG,
272                        "not adding outgoing call to TelecomManager on Android "
273                                + Build.VERSION.RELEASE);
274            }
275        }
276    }
277
278    public static void addNewIncomingCall(
279            final Context context, final AbstractJingleConnection.Id id) {
280        if (CallIntegration.notSelfManaged()) {
281            Log.d(
282                    Config.LOGTAG,
283                    "not adding incoming call to TelecomManager on Android "
284                            + Build.VERSION.RELEASE);
285            return;
286        }
287        final var phoneAccountHandle =
288                CallIntegrationConnectionService.getHandle(context, id.account);
289        final var bundle = new Bundle();
290        bundle.putString(
291                TelecomManager.EXTRA_INCOMING_CALL_ADDRESS,
292                CallIntegration.address(id.with).toString());
293        final var extras = new Bundle();
294        extras.putString("sid", id.sessionId);
295        bundle.putBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS, extras);
296        context.getSystemService(TelecomManager.class)
297                .addNewIncomingCall(phoneAccountHandle, bundle);
298    }
299
300    public static class ServiceConnectionService {
301        private final ServiceConnection serviceConnection;
302        private final XmppConnectionService service;
303
304        public ServiceConnectionService(
305                final ServiceConnection serviceConnection, final XmppConnectionService service) {
306            this.serviceConnection = serviceConnection;
307            this.service = service;
308        }
309
310        public static XmppConnectionService get(
311                final ListenableFuture<ServiceConnectionService> future) {
312            try {
313                return future.get(2, TimeUnit.SECONDS).service;
314            } catch (final ExecutionException | InterruptedException | TimeoutException e) {
315                return null;
316            }
317        }
318
319        public static ListenableFuture<ServiceConnectionService> bindService(
320                final Context context) {
321            final SettableFuture<ServiceConnectionService> serviceConnectionFuture =
322                    SettableFuture.create();
323            final var intent = new Intent(context, XmppConnectionService.class);
324            intent.setAction(XmppConnectionService.ACTION_CALL_INTEGRATION_SERVICE_STARTED);
325            final var serviceConnection =
326                    new ServiceConnection() {
327
328                        @Override
329                        public void onServiceConnected(
330                                final ComponentName name, final IBinder iBinder) {
331                            final XmppConnectionService.XmppConnectionBinder binder =
332                                    (XmppConnectionService.XmppConnectionBinder) iBinder;
333                            serviceConnectionFuture.set(
334                                    new ServiceConnectionService(this, binder.getService()));
335                        }
336
337                        @Override
338                        public void onServiceDisconnected(final ComponentName name) {}
339                    };
340            context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
341            return serviceConnectionFuture;
342        }
343    }
344}