AbstractJingleConnection.java

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