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