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}