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