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