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 CallIntegration callIntegration;
101        if (with.isBareJid()) {
102            final var proposal =
103                    service.getJingleConnectionManager()
104                            .proposeJingleRtpSession(account, with, media);
105
106            intent.putExtra(
107                    RtpSessionActivity.EXTRA_LAST_REPORTED_STATE,
108                    RtpEndUserState.FINDING_DEVICE.toString());
109            if (Media.audioOnly(media)) {
110                intent.putExtra(
111                        RtpSessionActivity.EXTRA_LAST_ACTION,
112                        RtpSessionActivity.ACTION_MAKE_VOICE_CALL);
113            } else {
114                intent.putExtra(
115                        RtpSessionActivity.EXTRA_LAST_ACTION,
116                        RtpSessionActivity.ACTION_MAKE_VIDEO_CALL);
117            }
118            callIntegration = proposal.getCallIntegration();
119        } else {
120            final JingleRtpConnection jingleRtpConnection =
121                    service.getJingleConnectionManager().initializeRtpSession(account, with, media);
122            final String sessionId = jingleRtpConnection.getId().sessionId;
123            intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, sessionId);
124            callIntegration = jingleRtpConnection.getCallIntegration();
125        }
126        service.startActivity(intent);
127        return callIntegration;
128    }
129
130    @Override
131    public Connection onCreateOutgoingConnection(
132            final PhoneAccountHandle phoneAccountHandle, final ConnectionRequest request) {
133        Log.d(Config.LOGTAG, "onCreateOutgoingConnection(" + request.getAddress() + ")");
134        final var uri = request.getAddress();
135        final var jid = Jid.ofEscaped(uri.getSchemeSpecificPart());
136        final var extras = request.getExtras();
137        final int videoState = extras.getInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE);
138        final Set<Media> media =
139                videoState == VideoProfile.STATE_AUDIO_ONLY
140                        ? ImmutableSet.of(Media.AUDIO)
141                        : ImmutableSet.of(Media.AUDIO, Media.VIDEO);
142        Log.d(Config.LOGTAG, "jid=" + jid);
143        Log.d(Config.LOGTAG, "phoneAccountHandle:" + phoneAccountHandle.getId());
144        Log.d(Config.LOGTAG, "media " + media);
145        final var service = ServiceConnectionService.get(this.serviceFuture);
146        return createOutgoingRtpConnection(service, phoneAccountHandle.getId(), jid, media);
147    }
148
149    @Override
150    public Connection onCreateIncomingConnection(
151            final PhoneAccountHandle phoneAccountHandle, final ConnectionRequest request) {
152        Log.d(Config.LOGTAG, "onCreateIncomingConnection()");
153        final var service = ServiceConnectionService.get(this.serviceFuture);
154        final Bundle extras = request.getExtras();
155        final Bundle extraExtras = extras.getBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS);
156        final String incomingCallAddress =
157                extras.getString(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS);
158        final String sid = extraExtras == null ? null : extraExtras.getString("sid");
159        Log.d(Config.LOGTAG, "sid " + sid);
160        final Uri uri = incomingCallAddress == null ? null : Uri.parse(incomingCallAddress);
161        Log.d(Config.LOGTAG, "uri=" + uri);
162        if (uri == null || sid == null) {
163            return Connection.createFailedConnection(
164                    new DisconnectCause(
165                            DisconnectCause.ERROR,
166                            "connection request is missing required information"));
167        }
168        if (service == null) {
169            return Connection.createFailedConnection(
170                    new DisconnectCause(DisconnectCause.ERROR, "service connection not found"));
171        }
172        final var jid = Jid.ofEscaped(uri.getSchemeSpecificPart());
173        final Account account = service.findAccountByUuid(phoneAccountHandle.getId());
174        final var weakReference =
175                service.getJingleConnectionManager().findJingleRtpConnection(account, jid, sid);
176        if (weakReference == null) {
177            Log.d(Config.LOGTAG, "no connection found for " + jid + " and sid=" + sid);
178            return Connection.createFailedConnection(
179                    new DisconnectCause(DisconnectCause.ERROR, "no incoming connection found"));
180        }
181        final var jingleRtpConnection = weakReference.get();
182        if (jingleRtpConnection == null) {
183            Log.d(Config.LOGTAG, "connection has been terminated");
184            return Connection.createFailedConnection(
185                    new DisconnectCause(DisconnectCause.ERROR, "connection has been terminated"));
186        }
187        Log.d(Config.LOGTAG, "registering call integration for incoming call");
188        return jingleRtpConnection.getCallIntegration();
189    }
190
191    public static void registerPhoneAccount(final Context context, final Account account) {
192        final var builder =
193                PhoneAccount.builder(getHandle(context, account), account.getJid().asBareJid());
194        builder.setSupportedUriSchemes(Collections.singletonList("xmpp"));
195        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
196            builder.setCapabilities(
197                    PhoneAccount.CAPABILITY_SELF_MANAGED
198                            | PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING);
199        }
200        final var phoneAccount = builder.build();
201
202        context.getSystemService(TelecomManager.class).registerPhoneAccount(phoneAccount);
203    }
204
205    public static void registerPhoneAccounts(
206            final Context context, final Collection<Account> accounts) {
207        for (final Account account : accounts) {
208            registerPhoneAccount(context, account);
209        }
210    }
211
212    public static void unregisterPhoneAccount(final Context context, final Account account) {
213        context.getSystemService(TelecomManager.class)
214                .unregisterPhoneAccount(getHandle(context, account));
215    }
216
217    public static PhoneAccountHandle getHandle(final Context context, final Account account) {
218        final var competentName =
219                new ComponentName(context, CallIntegrationConnectionService.class);
220        return new PhoneAccountHandle(competentName, account.getUuid());
221    }
222
223    public static void placeCall(
224            final XmppConnectionService service,
225            final Account account,
226            final Jid with,
227            final Set<Media> media) {
228        if (CallIntegration.selfManaged()) {
229            final var extras = new Bundle();
230            extras.putParcelable(
231                    TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, getHandle(service, account));
232            extras.putInt(
233                    TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
234                    Media.audioOnly(media)
235                            ? VideoProfile.STATE_AUDIO_ONLY
236                            : VideoProfile.STATE_BIDIRECTIONAL);
237            if (service.checkSelfPermission(Manifest.permission.MANAGE_OWN_CALLS)
238                    != PackageManager.PERMISSION_GRANTED) {
239                Toast.makeText(service, R.string.no_permission_to_place_call, Toast.LENGTH_SHORT)
240                        .show();
241                return;
242            }
243            service.getSystemService(TelecomManager.class)
244                    .placeCall(CallIntegration.address(with), extras);
245        } else {
246            final var connection = createOutgoingRtpConnection(service, account, with, media);
247            if (connection != null) {
248                Log.d(
249                        Config.LOGTAG,
250                        "not adding outgoing call to TelecomManager on Android "
251                                + Build.VERSION.RELEASE);
252            }
253        }
254    }
255
256    public static void addNewIncomingCall(
257            final Context context, final AbstractJingleConnection.Id id) {
258        if (CallIntegration.notSelfManaged()) {
259            Log.d(
260                    Config.LOGTAG,
261                    "not adding incoming call to TelecomManager on Android "
262                            + Build.VERSION.RELEASE);
263            return;
264        }
265        final var phoneAccountHandle =
266                CallIntegrationConnectionService.getHandle(context, id.account);
267        final var bundle = new Bundle();
268        bundle.putString(
269                TelecomManager.EXTRA_INCOMING_CALL_ADDRESS,
270                CallIntegration.address(id.with).toString());
271        final var extras = new Bundle();
272        extras.putString("sid", id.sessionId);
273        bundle.putBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS, extras);
274        context.getSystemService(TelecomManager.class)
275                .addNewIncomingCall(phoneAccountHandle, bundle);
276    }
277
278    public static class ServiceConnectionService {
279        private final ServiceConnection serviceConnection;
280        private final XmppConnectionService service;
281
282        public ServiceConnectionService(
283                final ServiceConnection serviceConnection, final XmppConnectionService service) {
284            this.serviceConnection = serviceConnection;
285            this.service = service;
286        }
287
288        public static XmppConnectionService get(
289                final ListenableFuture<ServiceConnectionService> future) {
290            try {
291                return future.get(2, TimeUnit.SECONDS).service;
292            } catch (final ExecutionException | InterruptedException | TimeoutException e) {
293                return null;
294            }
295        }
296
297        public static ListenableFuture<ServiceConnectionService> bindService(
298                final Context context) {
299            final SettableFuture<ServiceConnectionService> serviceConnectionFuture =
300                    SettableFuture.create();
301            final var intent = new Intent(context, XmppConnectionService.class);
302            intent.setAction(XmppConnectionService.ACTION_CALL_INTEGRATION_SERVICE_STARTED);
303            final var serviceConnection =
304                    new ServiceConnection() {
305
306                        @Override
307                        public void onServiceConnected(
308                                final ComponentName name, final IBinder iBinder) {
309                            final XmppConnectionService.XmppConnectionBinder binder =
310                                    (XmppConnectionService.XmppConnectionBinder) iBinder;
311                            serviceConnectionFuture.set(
312                                    new ServiceConnectionService(this, binder.getService()));
313                        }
314
315                        @Override
316                        public void onServiceDisconnected(final ComponentName name) {}
317                    };
318            context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
319            return serviceConnectionFuture;
320        }
321    }
322}