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