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}