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}