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