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        final var service = ServiceConnectionService.get(this.serviceFuture);
126        final Bundle extras = request.getExtras();
127        final Bundle extraExtras = extras.getBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS);
128        final String incomingCallAddress =
129                extras.getString(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS);
130        final String sid = extraExtras == null ? null : extraExtras.getString("sid");
131        Log.d(Config.LOGTAG, "sid " + sid);
132        final Uri uri = incomingCallAddress == null ? null : Uri.parse(incomingCallAddress);
133        Log.d(Config.LOGTAG, "uri=" + uri);
134        if (uri == null || sid == null) {
135            return Connection.createFailedConnection(
136                    new DisconnectCause(
137                            DisconnectCause.ERROR,
138                            "connection request is missing required information"));
139        }
140        if (service == null) {
141            return Connection.createFailedConnection(
142                    new DisconnectCause(DisconnectCause.ERROR, "service connection not found"));
143        }
144        final var jid = Jid.ofEscaped(uri.getSchemeSpecificPart());
145        final Account account = service.findAccountByUuid(phoneAccountHandle.getId());
146        final var weakReference =
147                service.getJingleConnectionManager().findJingleRtpConnection(account, jid, sid);
148        if (weakReference == null) {
149            Log.d(Config.LOGTAG, "no connection found for " + jid + " and sid=" + sid);
150            return Connection.createFailedConnection(
151                    new DisconnectCause(DisconnectCause.ERROR, "no incoming connection found"));
152        }
153        final var jingleRtpConnection = weakReference.get();
154        if (jingleRtpConnection == null) {
155            Log.d(Config.LOGTAG, "connection has been terminated");
156            return Connection.createFailedConnection(
157                    new DisconnectCause(DisconnectCause.ERROR, "connection has been terminated"));
158        }
159        Log.d(Config.LOGTAG, "registering call integration for incoming call");
160        return jingleRtpConnection.getCallIntegration();
161    }
162
163    public static void registerPhoneAccount(final Context context, final Account account) {
164        final var builder =
165                PhoneAccount.builder(getHandle(context, account), account.getJid().asBareJid());
166        builder.setSupportedUriSchemes(Collections.singletonList("xmpp"));
167        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
168            builder.setCapabilities(
169                    PhoneAccount.CAPABILITY_SELF_MANAGED
170                            | PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING);
171        }
172        final var phoneAccount = builder.build();
173
174        context.getSystemService(TelecomManager.class).registerPhoneAccount(phoneAccount);
175    }
176
177    public static void registerPhoneAccounts(
178            final Context context, final Collection<Account> accounts) {
179        for (final Account account : accounts) {
180            registerPhoneAccount(context, account);
181        }
182    }
183
184    public static PhoneAccountHandle getHandle(final Context context, final Account account) {
185        final var competentName =
186                new ComponentName(context, CallIntegrationConnectionService.class);
187        return new PhoneAccountHandle(competentName, account.getUuid());
188    }
189
190    public static void placeCall(
191            final Context context, final Account account, final Jid with, final Set<Media> media) {
192        Log.d(Config.LOGTAG, "place call media=" + media);
193        final var extras = new Bundle();
194        extras.putParcelable(
195                TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, getHandle(context, account));
196        extras.putInt(
197                TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
198                Media.audioOnly(media)
199                        ? VideoProfile.STATE_AUDIO_ONLY
200                        : VideoProfile.STATE_BIDIRECTIONAL);
201        context.getSystemService(TelecomManager.class)
202                .placeCall(CallIntegration.address(with), extras);
203    }
204
205    public static void addNewIncomingCall(
206            final Context context, final AbstractJingleConnection.Id id) {
207        final var phoneAccountHandle =
208                CallIntegrationConnectionService.getHandle(context, id.account);
209        final var bundle = new Bundle();
210        bundle.putString(
211                TelecomManager.EXTRA_INCOMING_CALL_ADDRESS,
212                CallIntegration.address(id.with).toString());
213        final var extras = new Bundle();
214        extras.putString("sid", id.sessionId);
215        bundle.putBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS, extras);
216        context.getSystemService(TelecomManager.class)
217                .addNewIncomingCall(phoneAccountHandle, bundle);
218    }
219
220    public static class ServiceConnectionService {
221        private final ServiceConnection serviceConnection;
222        private final XmppConnectionService service;
223
224        public ServiceConnectionService(
225                final ServiceConnection serviceConnection, final XmppConnectionService service) {
226            this.serviceConnection = serviceConnection;
227            this.service = service;
228        }
229
230        public static XmppConnectionService get(
231                final ListenableFuture<ServiceConnectionService> future) {
232            try {
233                return future.get(2, TimeUnit.SECONDS).service;
234            } catch (final ExecutionException | InterruptedException | TimeoutException e) {
235                return null;
236            }
237        }
238
239        public static ListenableFuture<ServiceConnectionService> bindService(
240                final Context context) {
241            final SettableFuture<ServiceConnectionService> serviceConnectionFuture =
242                    SettableFuture.create();
243            final var intent = new Intent(context, XmppConnectionService.class);
244            intent.setAction(XmppConnectionService.ACTION_CALL_INTEGRATION_SERVICE_STARTED);
245            final var serviceConnection =
246                    new ServiceConnection() {
247
248                        @Override
249                        public void onServiceConnected(
250                                final ComponentName name, final IBinder iBinder) {
251                            final XmppConnectionService.XmppConnectionBinder binder =
252                                    (XmppConnectionService.XmppConnectionBinder) iBinder;
253                            serviceConnectionFuture.set(
254                                    new ServiceConnectionService(this, binder.getService()));
255                        }
256
257                        @Override
258                        public void onServiceDisconnected(final ComponentName name) {}
259                    };
260            context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
261            return serviceConnectionFuture;
262        }
263    }
264}