CallIntegrationConnectionService.java

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