AbstractJingleConnection.java

  1package eu.siacs.conversations.xmpp.jingle;
  2
  3import android.util.Log;
  4
  5import androidx.annotation.NonNull;
  6
  7import com.google.common.base.MoreObjects;
  8import com.google.common.base.Objects;
  9import com.google.common.base.Preconditions;
 10import com.google.common.base.Strings;
 11import com.google.common.collect.ImmutableList;
 12import com.google.common.collect.ImmutableMap;
 13
 14import eu.siacs.conversations.Config;
 15import eu.siacs.conversations.entities.Account;
 16import eu.siacs.conversations.entities.Contact;
 17import eu.siacs.conversations.entities.Message;
 18import eu.siacs.conversations.entities.Presence;
 19import eu.siacs.conversations.entities.ServiceDiscoveryResult;
 20import eu.siacs.conversations.services.XmppConnectionService;
 21import eu.siacs.conversations.xmpp.Jid;
 22import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
 23import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
 24import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 25
 26import java.util.Arrays;
 27import java.util.Collection;
 28import java.util.List;
 29import java.util.Map;
 30import java.util.function.Consumer;
 31
 32public abstract class AbstractJingleConnection {
 33
 34    public static final String JINGLE_MESSAGE_PROPOSE_ID_PREFIX = "jm-propose-";
 35    public static final String JINGLE_MESSAGE_PROCEED_ID_PREFIX = "jm-proceed-";
 36
 37    protected static final List<State> TERMINATED =
 38            Arrays.asList(
 39                    State.ACCEPTED,
 40                    State.REJECTED,
 41                    State.REJECTED_RACED,
 42                    State.RETRACTED,
 43                    State.RETRACTED_RACED,
 44                    State.TERMINATED_SUCCESS,
 45                    State.TERMINATED_DECLINED_OR_BUSY,
 46                    State.TERMINATED_CONNECTIVITY_ERROR,
 47                    State.TERMINATED_CANCEL_OR_TIMEOUT,
 48                    State.TERMINATED_APPLICATION_FAILURE,
 49                    State.TERMINATED_SECURITY_ERROR);
 50
 51    private static final Map<State, Collection<State>> VALID_TRANSITIONS;
 52
 53    static {
 54        final ImmutableMap.Builder<State, Collection<State>> transitionBuilder =
 55                new ImmutableMap.Builder<>();
 56        transitionBuilder.put(
 57                State.NULL,
 58                ImmutableList.of(
 59                        State.PROPOSED,
 60                        State.SESSION_INITIALIZED,
 61                        State.TERMINATED_APPLICATION_FAILURE,
 62                        State.TERMINATED_SECURITY_ERROR));
 63        transitionBuilder.put(
 64                State.PROPOSED,
 65                ImmutableList.of(
 66                        State.ACCEPTED,
 67                        State.PROCEED,
 68                        State.REJECTED,
 69                        State.RETRACTED,
 70                        State.TERMINATED_APPLICATION_FAILURE,
 71                        State.TERMINATED_SECURITY_ERROR,
 72                        State.TERMINATED_CONNECTIVITY_ERROR // only used when the xmpp connection
 73                        // rebinds
 74                        ));
 75        transitionBuilder.put(
 76                State.PROCEED,
 77                ImmutableList.of(
 78                        State.REJECTED_RACED,
 79                        State.RETRACTED_RACED,
 80                        State.SESSION_INITIALIZED_PRE_APPROVED,
 81                        State.TERMINATED_SUCCESS,
 82                        State.TERMINATED_APPLICATION_FAILURE,
 83                        State.TERMINATED_SECURITY_ERROR,
 84                        State.TERMINATED_CONNECTIVITY_ERROR // at this state used for error
 85                        // bounces of the proceed message
 86                        ));
 87        transitionBuilder.put(
 88                State.SESSION_INITIALIZED,
 89                ImmutableList.of(
 90                        State.SESSION_ACCEPTED,
 91                        State.TERMINATED_SUCCESS,
 92                        State.TERMINATED_DECLINED_OR_BUSY,
 93                        State.TERMINATED_CONNECTIVITY_ERROR, // at this state used for IQ errors
 94                        // and IQ timeouts
 95                        State.TERMINATED_CANCEL_OR_TIMEOUT,
 96                        State.TERMINATED_APPLICATION_FAILURE,
 97                        State.TERMINATED_SECURITY_ERROR));
 98        transitionBuilder.put(
 99                State.SESSION_INITIALIZED_PRE_APPROVED,
100                ImmutableList.of(
101                        State.SESSION_ACCEPTED,
102                        State.TERMINATED_SUCCESS,
103                        State.TERMINATED_DECLINED_OR_BUSY,
104                        State.TERMINATED_CONNECTIVITY_ERROR, // at this state used for IQ errors
105                        // and IQ timeouts
106                        State.TERMINATED_CANCEL_OR_TIMEOUT,
107                        State.TERMINATED_APPLICATION_FAILURE,
108                        State.TERMINATED_SECURITY_ERROR));
109        transitionBuilder.put(
110                State.SESSION_ACCEPTED,
111                ImmutableList.of(
112                        State.TERMINATED_SUCCESS,
113                        State.TERMINATED_DECLINED_OR_BUSY,
114                        State.TERMINATED_CONNECTIVITY_ERROR,
115                        State.TERMINATED_CANCEL_OR_TIMEOUT,
116                        State.TERMINATED_APPLICATION_FAILURE,
117                        State.TERMINATED_SECURITY_ERROR));
118        VALID_TRANSITIONS = transitionBuilder.build();
119    }
120
121    final JingleConnectionManager jingleConnectionManager;
122    protected final XmppConnectionService xmppConnectionService;
123    protected final Id id;
124    private final Jid initiator;
125
126    protected State state = State.NULL;
127
128    AbstractJingleConnection(
129            final JingleConnectionManager jingleConnectionManager,
130            final Id id,
131            final Jid initiator) {
132        this.jingleConnectionManager = jingleConnectionManager;
133        this.xmppConnectionService = jingleConnectionManager.getXmppConnectionService();
134        this.id = id;
135        this.initiator = initiator;
136    }
137
138    public Id getId() {
139        return id;
140    }
141
142    boolean isInitiator() {
143        return initiator.equals(id.account.getJid());
144    }
145
146    boolean isResponder() {
147        return !initiator.equals(id.account.getJid());
148    }
149
150    public State getState() {
151        return this.state;
152    }
153
154    protected synchronized boolean isInState(State... state) {
155        return Arrays.asList(state).contains(this.state);
156    }
157
158    protected boolean transition(final State target) {
159        return transition(target, null);
160    }
161
162    protected synchronized boolean transition(final State target, final Runnable runnable) {
163        final Collection<State> validTransitions = VALID_TRANSITIONS.get(this.state);
164        if (validTransitions != null && validTransitions.contains(target)) {
165            this.state = target;
166            if (runnable != null) {
167                runnable.run();
168            }
169            Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": transitioned into " + target);
170            return true;
171        } else {
172            return false;
173        }
174    }
175
176    protected void transitionOrThrow(final State target) {
177        if (!transition(target)) {
178            throw new IllegalStateException(
179                    String.format("Unable to transition from %s to %s", this.state, target));
180        }
181    }
182
183    boolean isTerminated() {
184        return TERMINATED.contains(this.state);
185    }
186
187    abstract void deliverPacket(JinglePacket jinglePacket);
188
189    protected void receiveOutOfOrderAction(
190            final JinglePacket jinglePacket, final JinglePacket.Action action) {
191        Log.d(
192                Config.LOGTAG,
193                String.format(
194                        "%s: received %s even though we are in state %s",
195                        id.account.getJid().asBareJid(), action, getState()));
196        if (isTerminated()) {
197            Log.d(
198                    Config.LOGTAG,
199                    String.format(
200                            "%s: got a reason to terminate with out-of-order. but already in state %s",
201                            id.account.getJid().asBareJid(), getState()));
202            respondWithOutOfOrder(jinglePacket);
203        } else {
204            terminateWithOutOfOrder(jinglePacket);
205        }
206    }
207
208    protected void terminateWithOutOfOrder(final JinglePacket jinglePacket) {
209        Log.d(
210                Config.LOGTAG,
211                id.account.getJid().asBareJid() + ": terminating session with out-of-order");
212        terminateTransport();
213        transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE);
214        respondWithOutOfOrder(jinglePacket);
215        this.finish();
216    }
217
218    protected void finish() {
219        if (isTerminated()) {
220            this.jingleConnectionManager.finishConnectionOrThrow(this);
221        } else {
222            throw new AssertionError(
223                    String.format("Unable to call finish from %s", this.state));
224        }
225    }
226
227    protected abstract void terminateTransport();
228
229    abstract void notifyRebound();
230
231    protected void sendSessionTerminate(
232            final Reason reason, final String text, final Consumer<State> trigger) {
233        final State previous = this.state;
234        final State target = reasonToState(reason);
235        transitionOrThrow(target);
236        if (previous != State.NULL && trigger != null) {
237            trigger.accept(target);
238        }
239        final JinglePacket jinglePacket =
240                new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId);
241        jinglePacket.setReason(reason, text);
242        send(jinglePacket);
243        finish();
244    }
245
246    protected void send(final JinglePacket jinglePacket) {
247        jinglePacket.setTo(id.with);
248        xmppConnectionService.sendIqPacket(id.account, jinglePacket, this::handleIqResponse);
249    }
250
251    protected void respondOk(final JinglePacket jinglePacket) {
252        xmppConnectionService.sendIqPacket(
253                id.account, jinglePacket.generateResponse(IqPacket.TYPE.RESULT), null);
254    }
255
256    protected void respondWithTieBreak(final JinglePacket jinglePacket) {
257        respondWithJingleError(jinglePacket, "tie-break", "conflict", "cancel");
258    }
259
260    protected void respondWithOutOfOrder(final JinglePacket jinglePacket) {
261        respondWithJingleError(jinglePacket, "out-of-order", "unexpected-request", "wait");
262    }
263
264    protected void respondWithItemNotFound(final JinglePacket jinglePacket) {
265        respondWithJingleError(jinglePacket, null, "item-not-found", "cancel");
266    }
267
268    private void respondWithJingleError(
269            final IqPacket original,
270            String jingleCondition,
271            String condition,
272            String conditionType) {
273        jingleConnectionManager.respondWithJingleError(
274                id.account, original, jingleCondition, condition, conditionType);
275    }
276
277    private synchronized void handleIqResponse(final Account account, final IqPacket response) {
278        if (response.getType() == IqPacket.TYPE.ERROR) {
279            handleIqErrorResponse(response);
280            return;
281        }
282        if (response.getType() == IqPacket.TYPE.TIMEOUT) {
283            handleIqTimeoutResponse(response);
284        }
285    }
286
287    protected void handleIqErrorResponse(final IqPacket response) {
288        Preconditions.checkArgument(response.getType() == IqPacket.TYPE.ERROR);
289        final String errorCondition = response.getErrorCondition();
290        Log.d(
291                Config.LOGTAG,
292                id.account.getJid().asBareJid()
293                        + ": received IQ-error from "
294                        + response.getFrom()
295                        + " in RTP session. "
296                        + errorCondition);
297        if (isTerminated()) {
298            Log.i(
299                    Config.LOGTAG,
300                    id.account.getJid().asBareJid()
301                            + ": ignoring error because session was already terminated");
302            return;
303        }
304        this.terminateTransport();
305        final State target;
306        if (Arrays.asList(
307                        "service-unavailable",
308                        "recipient-unavailable",
309                        "remote-server-not-found",
310                        "remote-server-timeout")
311                .contains(errorCondition)) {
312            target = State.TERMINATED_CONNECTIVITY_ERROR;
313        } else {
314            target = State.TERMINATED_APPLICATION_FAILURE;
315        }
316        transitionOrThrow(target);
317        this.finish();
318    }
319
320    protected void handleIqTimeoutResponse(final IqPacket response) {
321        Preconditions.checkArgument(response.getType() == IqPacket.TYPE.TIMEOUT);
322        Log.d(
323                Config.LOGTAG,
324                id.account.getJid().asBareJid()
325                        + ": received IQ timeout in RTP session with "
326                        + id.with
327                        + ". terminating with connectivity error");
328        if (isTerminated()) {
329            Log.i(
330                    Config.LOGTAG,
331                    id.account.getJid().asBareJid()
332                            + ": ignoring error because session was already terminated");
333            return;
334        }
335        this.terminateTransport();
336        transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR);
337        this.finish();
338    }
339
340    protected boolean remoteHasFeature(final String feature) {
341        final Contact contact = id.getContact();
342        final Presence presence =
343                contact.getPresences().get(Strings.nullToEmpty(id.with.getResource()));
344        final ServiceDiscoveryResult serviceDiscoveryResult =
345                presence == null ? null : presence.getServiceDiscoveryResult();
346        final List<String> features =
347                serviceDiscoveryResult == null ? null : serviceDiscoveryResult.getFeatures();
348        return features != null && features.contains(feature);
349    }
350
351    public static class Id implements OngoingRtpSession {
352        public final Account account;
353        public final Jid with;
354        public final String sessionId;
355
356        private Id(final Account account, final Jid with, final String sessionId) {
357            Preconditions.checkNotNull(account);
358            Preconditions.checkNotNull(with);
359            Preconditions.checkNotNull(sessionId);
360            this.account = account;
361            this.with = with;
362            this.sessionId = sessionId;
363        }
364
365        public static Id of(Account account, JinglePacket jinglePacket) {
366            return new Id(account, jinglePacket.getFrom(), jinglePacket.getSessionId());
367        }
368
369        public static Id of(Account account, Jid with, final String sessionId) {
370            return new Id(account, with, sessionId);
371        }
372
373        public static Id of(Account account, Jid with) {
374            return new Id(account, with, JingleConnectionManager.nextRandomId());
375        }
376
377        public static Id of(Message message) {
378            return new Id(
379                    message.getConversation().getAccount(),
380                    message.getCounterpart(),
381                    JingleConnectionManager.nextRandomId());
382        }
383
384        public Contact getContact() {
385            return account.getRoster().getContact(with);
386        }
387
388        @Override
389        public boolean equals(Object o) {
390            if (this == o) return true;
391            if (o == null || getClass() != o.getClass()) return false;
392            Id id = (Id) o;
393            return Objects.equal(account.getUuid(), id.account.getUuid())
394                    && Objects.equal(with, id.with)
395                    && Objects.equal(sessionId, id.sessionId);
396        }
397
398        @Override
399        public int hashCode() {
400            return Objects.hashCode(account.getUuid(), with, sessionId);
401        }
402
403        @Override
404        public Account getAccount() {
405            return account;
406        }
407
408        @Override
409        public Jid getWith() {
410            return with;
411        }
412
413        @Override
414        public String getSessionId() {
415            return sessionId;
416        }
417
418        @Override
419        @NonNull
420        public String toString() {
421            return MoreObjects.toStringHelper(this)
422                    .add("account", account.getJid())
423                    .add("with", with)
424                    .add("sessionId", sessionId)
425                    .toString();
426        }
427    }
428
429    protected static State reasonToState(Reason reason) {
430        return switch (reason) {
431            case SUCCESS -> State.TERMINATED_SUCCESS;
432            case DECLINE, BUSY -> State.TERMINATED_DECLINED_OR_BUSY;
433            case CANCEL, TIMEOUT -> State.TERMINATED_CANCEL_OR_TIMEOUT;
434            case SECURITY_ERROR -> State.TERMINATED_SECURITY_ERROR;
435            case FAILED_APPLICATION, UNSUPPORTED_TRANSPORTS, UNSUPPORTED_APPLICATIONS -> State
436                    .TERMINATED_APPLICATION_FAILURE;
437            default -> State.TERMINATED_CONNECTIVITY_ERROR;
438        };
439    }
440
441    public enum State {
442        NULL, // default value; nothing has been sent or received yet
443        PROPOSED,
444        ACCEPTED,
445        PROCEED,
446        REJECTED,
447        REJECTED_RACED, // used when we want to reject but haven’t received session init yet
448        RETRACTED,
449        RETRACTED_RACED, // used when receiving a retract after we already asked to proceed
450        SESSION_INITIALIZED, // equal to 'PENDING'
451        SESSION_INITIALIZED_PRE_APPROVED,
452        SESSION_ACCEPTED, // equal to 'ACTIVE'
453        TERMINATED_SUCCESS, // equal to 'ENDED' (after successful call) ui will just close
454        TERMINATED_DECLINED_OR_BUSY, // equal to 'ENDED' (after other party declined the call)
455        TERMINATED_CONNECTIVITY_ERROR, // equal to 'ENDED' (but after network failures; ui will
456        // display retry button)
457        TERMINATED_CANCEL_OR_TIMEOUT, // more or less the same as retracted; caller pressed end call
458        // before session was accepted
459        TERMINATED_APPLICATION_FAILURE,
460        TERMINATED_SECURITY_ERROR
461    }
462}