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