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 Log.d(Config.LOGTAG, "onCreateIncomingConnection()");
126 final var service = ServiceConnectionService.get(this.serviceFuture);
127 final Bundle extras = request.getExtras();
128 final Bundle extraExtras = extras.getBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS);
129 final String incomingCallAddress =
130 extras.getString(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS);
131 final String sid = extraExtras == null ? null : extraExtras.getString("sid");
132 Log.d(Config.LOGTAG, "sid " + sid);
133 final Uri uri = incomingCallAddress == null ? null : Uri.parse(incomingCallAddress);
134 Log.d(Config.LOGTAG, "uri=" + uri);
135 if (uri == null || sid == null) {
136 return Connection.createFailedConnection(
137 new DisconnectCause(
138 DisconnectCause.ERROR,
139 "connection request is missing required information"));
140 }
141 if (service == null) {
142 return Connection.createFailedConnection(
143 new DisconnectCause(DisconnectCause.ERROR, "service connection not found"));
144 }
145 final var jid = Jid.ofEscaped(uri.getSchemeSpecificPart());
146 final Account account = service.findAccountByUuid(phoneAccountHandle.getId());
147 final var weakReference =
148 service.getJingleConnectionManager().findJingleRtpConnection(account, jid, sid);
149 if (weakReference == null) {
150 Log.d(Config.LOGTAG, "no connection found for " + jid + " and sid=" + sid);
151 return Connection.createFailedConnection(
152 new DisconnectCause(DisconnectCause.ERROR, "no incoming connection found"));
153 }
154 final var jingleRtpConnection = weakReference.get();
155 if (jingleRtpConnection == null) {
156 Log.d(Config.LOGTAG, "connection has been terminated");
157 return Connection.createFailedConnection(
158 new DisconnectCause(DisconnectCause.ERROR, "connection has been terminated"));
159 }
160 Log.d(Config.LOGTAG, "registering call integration for incoming call");
161 return jingleRtpConnection.getCallIntegration();
162 }
163
164 public static void registerPhoneAccount(final Context context, final Account account) {
165 final var builder =
166 PhoneAccount.builder(getHandle(context, account), account.getJid().asBareJid());
167 builder.setSupportedUriSchemes(Collections.singletonList("xmpp"));
168 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
169 builder.setCapabilities(
170 PhoneAccount.CAPABILITY_SELF_MANAGED
171 | PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING);
172 }
173 final var phoneAccount = builder.build();
174
175 context.getSystemService(TelecomManager.class).registerPhoneAccount(phoneAccount);
176 }
177
178 public static void registerPhoneAccounts(
179 final Context context, final Collection<Account> accounts) {
180 for (final Account account : accounts) {
181 registerPhoneAccount(context, account);
182 }
183 }
184
185 public static void unregisterPhoneAccount(final Context context, final Account account) {
186 context.getSystemService(TelecomManager.class)
187 .unregisterPhoneAccount(getHandle(context, account));
188 }
189
190 public static PhoneAccountHandle getHandle(final Context context, final Account account) {
191 final var competentName =
192 new ComponentName(context, CallIntegrationConnectionService.class);
193 return new PhoneAccountHandle(competentName, account.getUuid());
194 }
195
196 public static void placeCall(
197 final Context context, final Account account, final Jid with, final Set<Media> media) {
198 Log.d(Config.LOGTAG, "place call media=" + media);
199 final var extras = new Bundle();
200 extras.putParcelable(
201 TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, getHandle(context, account));
202 extras.putInt(
203 TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
204 Media.audioOnly(media)
205 ? VideoProfile.STATE_AUDIO_ONLY
206 : VideoProfile.STATE_BIDIRECTIONAL);
207 context.getSystemService(TelecomManager.class)
208 .placeCall(CallIntegration.address(with), extras);
209 }
210
211 public static void addNewIncomingCall(
212 final Context context, final AbstractJingleConnection.Id id) {
213 final var phoneAccountHandle =
214 CallIntegrationConnectionService.getHandle(context, id.account);
215 final var bundle = new Bundle();
216 bundle.putString(
217 TelecomManager.EXTRA_INCOMING_CALL_ADDRESS,
218 CallIntegration.address(id.with).toString());
219 final var extras = new Bundle();
220 extras.putString("sid", id.sessionId);
221 bundle.putBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS, extras);
222 context.getSystemService(TelecomManager.class)
223 .addNewIncomingCall(phoneAccountHandle, bundle);
224 }
225
226 public static class ServiceConnectionService {
227 private final ServiceConnection serviceConnection;
228 private final XmppConnectionService service;
229
230 public ServiceConnectionService(
231 final ServiceConnection serviceConnection, final XmppConnectionService service) {
232 this.serviceConnection = serviceConnection;
233 this.service = service;
234 }
235
236 public static XmppConnectionService get(
237 final ListenableFuture<ServiceConnectionService> future) {
238 try {
239 return future.get(2, TimeUnit.SECONDS).service;
240 } catch (final ExecutionException | InterruptedException | TimeoutException e) {
241 return null;
242 }
243 }
244
245 public static ListenableFuture<ServiceConnectionService> bindService(
246 final Context context) {
247 final SettableFuture<ServiceConnectionService> serviceConnectionFuture =
248 SettableFuture.create();
249 final var intent = new Intent(context, XmppConnectionService.class);
250 intent.setAction(XmppConnectionService.ACTION_CALL_INTEGRATION_SERVICE_STARTED);
251 final var serviceConnection =
252 new ServiceConnection() {
253
254 @Override
255 public void onServiceConnected(
256 final ComponentName name, final IBinder iBinder) {
257 final XmppConnectionService.XmppConnectionBinder binder =
258 (XmppConnectionService.XmppConnectionBinder) iBinder;
259 serviceConnectionFuture.set(
260 new ServiceConnectionService(this, binder.getService()));
261 }
262
263 @Override
264 public void onServiceDisconnected(final ComponentName name) {}
265 };
266 context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
267 return serviceConnectionFuture;
268 }
269 }
270}