Browse Source

Import existing sources

Many things may still be broken.
tags/v0.1.0
Lennart Grahl 3 years ago
commit
80e1905516
43 changed files with 2683 additions and 0 deletions
  1. 10
    0
      .gitignore
  2. 23
    0
      build.gradle
  3. BIN
      gradle/wrapper/gradle-wrapper.jar
  4. 6
    0
      gradle/wrapper/gradle-wrapper.properties
  5. 164
    0
      gradlew
  6. 90
    0
      gradlew.bat
  7. BIN
      libs/armeabi-v7a/libjingle_peerconnection_so.so
  8. BIN
      libs/autobahn-0.5.0.jar
  9. BIN
      libs/libjingle_peerconnection.jar
  10. BIN
      libs/x86/libjingle_peerconnection_so.so
  11. 1
    0
      settings.gradle
  12. 70
    0
      src/main/java/org/saltyrtc/client/Chunkifier.java
  13. 62
    0
      src/main/java/org/saltyrtc/client/ClientState.java
  14. 374
    0
      src/main/java/org/saltyrtc/client/DataChannel.java
  15. 16
    0
      src/main/java/org/saltyrtc/client/DataChannelMessageDispatcher.java
  16. 62
    0
      src/main/java/org/saltyrtc/client/EncryptedChannel.java
  17. 5
    0
      src/main/java/org/saltyrtc/client/ErrorStateHandler.java
  18. 29
    0
      src/main/java/org/saltyrtc/client/Handler.java
  19. 45
    0
      src/main/java/org/saltyrtc/client/InternalState.java
  20. 93
    0
      src/main/java/org/saltyrtc/client/InternalStateListener.java
  21. 161
    0
      src/main/java/org/saltyrtc/client/KeyStore.java
  22. 20
    0
      src/main/java/org/saltyrtc/client/MessageDispatcher.java
  23. 443
    0
      src/main/java/org/saltyrtc/client/PeerConnection.java
  24. 28
    0
      src/main/java/org/saltyrtc/client/PeerConnectionMessageDispatcher.java
  25. 31
    0
      src/main/java/org/saltyrtc/client/Session.java
  26. 513
    0
      src/main/java/org/saltyrtc/client/Signaling.java
  27. 56
    0
      src/main/java/org/saltyrtc/client/SignalingMessageDispatcher.java
  28. 65
    0
      src/main/java/org/saltyrtc/client/State.java
  29. 39
    0
      src/main/java/org/saltyrtc/client/StateDispatcher.java
  30. 5
    0
      src/main/java/org/saltyrtc/client/StateHandler.java
  31. 8
    0
      src/main/java/org/saltyrtc/client/StateListener.java
  32. 10
    0
      src/main/java/org/saltyrtc/client/StateType.java
  33. 101
    0
      src/main/java/org/saltyrtc/client/States.java
  34. 73
    0
      src/main/java/org/saltyrtc/client/Unchunkifier.java
  35. 7
    0
      src/main/java/org/saltyrtc/client/exceptions/ConversionException.java
  36. 19
    0
      src/main/java/org/saltyrtc/client/exceptions/CryptoException.java
  37. 7
    0
      src/main/java/org/saltyrtc/client/exceptions/CryptoFailedException.java
  38. 7
    0
      src/main/java/org/saltyrtc/client/exceptions/DispatchException.java
  39. 10
    0
      src/main/java/org/saltyrtc/client/exceptions/InvalidChunkException.java
  40. 7
    0
      src/main/java/org/saltyrtc/client/exceptions/InvalidKeyException.java
  41. 4
    0
      src/main/java/org/saltyrtc/client/exceptions/OtherKeyMissingException.java
  42. 4
    0
      src/main/java/org/saltyrtc/client/exceptions/SessionUnavailableException.java
  43. 15
    0
      src/test/java/LibraryTest.java

+ 10
- 0
.gitignore View File

@@ -0,0 +1,10 @@
1
+# Vim
2
+*.swp
3
+
4
+# IntelliJ
5
+*.iml
6
+.idea
7
+
8
+# Gradle
9
+.gradle
10
+local.properties

+ 23
- 0
build.gradle View File

@@ -0,0 +1,23 @@
1
+// Apply the java plugin to add support for Java
2
+apply plugin: 'java'
3
+
4
+// In this section you declare where to find the dependencies of your project
5
+repositories {
6
+    jcenter()
7
+}
8
+
9
+// In this section you declare the dependencies for your production and test code
10
+dependencies {
11
+    // The production code uses the SLF4J logging API at compile time
12
+    compile 'org.slf4j:slf4j-api:1.7.21'
13
+
14
+    // Dependencies
15
+    compile files('libs/libjingle_peerconnection.jar')
16
+    compile files('libs/autobahn-0.5.0.jar')
17
+
18
+    // Declare the dependency for your favourite test framework you want to use in your tests.
19
+    // TestNG is also supported by the Gradle Test task. Just change the
20
+    // testCompile dependency to testCompile 'org.testng:testng:6.8.1' and add
21
+    // 'test.useTestNG()' to your build script.
22
+    testCompile 'junit:junit:4.12'
23
+}

BIN
gradle/wrapper/gradle-wrapper.jar View File


+ 6
- 0
gradle/wrapper/gradle-wrapper.properties View File

@@ -0,0 +1,6 @@
1
+#Mon May 23 09:10:43 CEST 2016
2
+distributionBase=GRADLE_USER_HOME
3
+distributionPath=wrapper/dists
4
+zipStoreBase=GRADLE_USER_HOME
5
+zipStorePath=wrapper/dists
6
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.13-bin.zip

+ 164
- 0
gradlew View File

@@ -0,0 +1,164 @@
1
+#!/usr/bin/env bash
2
+
3
+##############################################################################
4
+##
5
+##  Gradle start up script for UN*X
6
+##
7
+##############################################################################
8
+
9
+# Attempt to set APP_HOME
10
+# Resolve links: $0 may be a link
11
+PRG="$0"
12
+# Need this for relative symlinks.
13
+while [ -h "$PRG" ] ; do
14
+    ls=`ls -ld "$PRG"`
15
+    link=`expr "$ls" : '.*-> \(.*\)$'`
16
+    if expr "$link" : '/.*' > /dev/null; then
17
+        PRG="$link"
18
+    else
19
+        PRG=`dirname "$PRG"`"/$link"
20
+    fi
21
+done
22
+SAVED="`pwd`"
23
+cd "`dirname \"$PRG\"`/" >/dev/null
24
+APP_HOME="`pwd -P`"
25
+cd "$SAVED" >/dev/null
26
+
27
+APP_NAME="Gradle"
28
+APP_BASE_NAME=`basename "$0"`
29
+
30
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31
+DEFAULT_JVM_OPTS=""
32
+
33
+# Use the maximum available, or set MAX_FD != -1 to use that value.
34
+MAX_FD="maximum"
35
+
36
+warn ( ) {
37
+    echo "$*"
38
+}
39
+
40
+die ( ) {
41
+    echo
42
+    echo "$*"
43
+    echo
44
+    exit 1
45
+}
46
+
47
+# OS specific support (must be 'true' or 'false').
48
+cygwin=false
49
+msys=false
50
+darwin=false
51
+nonstop=false
52
+case "`uname`" in
53
+  CYGWIN* )
54
+    cygwin=true
55
+    ;;
56
+  Darwin* )
57
+    darwin=true
58
+    ;;
59
+  MINGW* )
60
+    msys=true
61
+    ;;
62
+  NONSTOP* )
63
+    nonstop=true
64
+    ;;
65
+esac
66
+
67
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68
+
69
+# Determine the Java command to use to start the JVM.
70
+if [ -n "$JAVA_HOME" ] ; then
71
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72
+        # IBM's JDK on AIX uses strange locations for the executables
73
+        JAVACMD="$JAVA_HOME/jre/sh/java"
74
+    else
75
+        JAVACMD="$JAVA_HOME/bin/java"
76
+    fi
77
+    if [ ! -x "$JAVACMD" ] ; then
78
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79
+
80
+Please set the JAVA_HOME variable in your environment to match the
81
+location of your Java installation."
82
+    fi
83
+else
84
+    JAVACMD="java"
85
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86
+
87
+Please set the JAVA_HOME variable in your environment to match the
88
+location of your Java installation."
89
+fi
90
+
91
+# Increase the maximum file descriptors if we can.
92
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93
+    MAX_FD_LIMIT=`ulimit -H -n`
94
+    if [ $? -eq 0 ] ; then
95
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96
+            MAX_FD="$MAX_FD_LIMIT"
97
+        fi
98
+        ulimit -n $MAX_FD
99
+        if [ $? -ne 0 ] ; then
100
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
101
+        fi
102
+    else
103
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104
+    fi
105
+fi
106
+
107
+# For Darwin, add options to specify how the application appears in the dock
108
+if $darwin; then
109
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110
+fi
111
+
112
+# For Cygwin, switch paths to Windows format before running java
113
+if $cygwin ; then
114
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116
+    JAVACMD=`cygpath --unix "$JAVACMD"`
117
+
118
+    # We build the pattern for arguments to be converted via cygpath
119
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120
+    SEP=""
121
+    for dir in $ROOTDIRSRAW ; do
122
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
123
+        SEP="|"
124
+    done
125
+    OURCYGPATTERN="(^($ROOTDIRS))"
126
+    # Add a user-defined pattern to the cygpath arguments
127
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129
+    fi
130
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
131
+    i=0
132
+    for arg in "$@" ; do
133
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
135
+
136
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
137
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138
+        else
139
+            eval `echo args$i`="\"$arg\""
140
+        fi
141
+        i=$((i+1))
142
+    done
143
+    case $i in
144
+        (0) set -- ;;
145
+        (1) set -- "$args0" ;;
146
+        (2) set -- "$args0" "$args1" ;;
147
+        (3) set -- "$args0" "$args1" "$args2" ;;
148
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154
+    esac
155
+fi
156
+
157
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158
+function splitJvmOpts() {
159
+    JVM_OPTS=("$@")
160
+}
161
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163
+
164
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

+ 90
- 0
gradlew.bat View File

@@ -0,0 +1,90 @@
1
+@if "%DEBUG%" == "" @echo off
2
+@rem ##########################################################################
3
+@rem
4
+@rem  Gradle startup script for Windows
5
+@rem
6
+@rem ##########################################################################
7
+
8
+@rem Set local scope for the variables with windows NT shell
9
+if "%OS%"=="Windows_NT" setlocal
10
+
11
+set DIRNAME=%~dp0
12
+if "%DIRNAME%" == "" set DIRNAME=.
13
+set APP_BASE_NAME=%~n0
14
+set APP_HOME=%DIRNAME%
15
+
16
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17
+set DEFAULT_JVM_OPTS=
18
+
19
+@rem Find java.exe
20
+if defined JAVA_HOME goto findJavaFromJavaHome
21
+
22
+set JAVA_EXE=java.exe
23
+%JAVA_EXE% -version >NUL 2>&1
24
+if "%ERRORLEVEL%" == "0" goto init
25
+
26
+echo.
27
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28
+echo.
29
+echo Please set the JAVA_HOME variable in your environment to match the
30
+echo location of your Java installation.
31
+
32
+goto fail
33
+
34
+:findJavaFromJavaHome
35
+set JAVA_HOME=%JAVA_HOME:"=%
36
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37
+
38
+if exist "%JAVA_EXE%" goto init
39
+
40
+echo.
41
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42
+echo.
43
+echo Please set the JAVA_HOME variable in your environment to match the
44
+echo location of your Java installation.
45
+
46
+goto fail
47
+
48
+:init
49
+@rem Get command-line arguments, handling Windows variants
50
+
51
+if not "%OS%" == "Windows_NT" goto win9xME_args
52
+if "%@eval[2+2]" == "4" goto 4NT_args
53
+
54
+:win9xME_args
55
+@rem Slurp the command line arguments.
56
+set CMD_LINE_ARGS=
57
+set _SKIP=2
58
+
59
+:win9xME_args_slurp
60
+if "x%~1" == "x" goto execute
61
+
62
+set CMD_LINE_ARGS=%*
63
+goto execute
64
+
65
+:4NT_args
66
+@rem Get arguments from the 4NT Shell from JP Software
67
+set CMD_LINE_ARGS=%$
68
+
69
+:execute
70
+@rem Setup the command line
71
+
72
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73
+
74
+@rem Execute Gradle
75
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76
+
77
+:end
78
+@rem End local scope for the variables with windows NT shell
79
+if "%ERRORLEVEL%"=="0" goto mainEnd
80
+
81
+:fail
82
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83
+rem the _cmd.exe /c_ return code!
84
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85
+exit /b 1
86
+
87
+:mainEnd
88
+if "%OS%"=="Windows_NT" endlocal
89
+
90
+:omega

BIN
libs/armeabi-v7a/libjingle_peerconnection_so.so View File


BIN
libs/autobahn-0.5.0.jar View File


BIN
libs/libjingle_peerconnection.jar View File


BIN
libs/x86/libjingle_peerconnection_so.so View File


+ 1
- 0
settings.gradle View File

@@ -0,0 +1 @@
1
+rootProject.name = 'saltyrtc-client-java'

+ 70
- 0
src/main/java/org/saltyrtc/client/Chunkifier.java View File

@@ -0,0 +1,70 @@
1
+package org.saltyrtc.client;
2
+
3
+import java.nio.ByteBuffer;
4
+import java.util.Iterator;
5
+
6
+/**
7
+ * Iterates over chunks of bytes and injects a 'more' flag byte.
8
+ */
9
+public class Chunkifier implements Iterable<ByteBuffer> {
10
+    private final byte[] bytes;
11
+    private final int chunkSize; // First byte is being used as a 'more' flag
12
+
13
+    public Chunkifier(byte[] bytes, int chunkSize) {
14
+        this.bytes = bytes;
15
+        this.chunkSize = chunkSize;
16
+    }
17
+
18
+    @Override
19
+    public Iterator<ByteBuffer> iterator() {
20
+        return new Iterator<ByteBuffer>() {
21
+            private int index = 0;
22
+
23
+            public int offset() {
24
+                return this.offset(this.index);
25
+            }
26
+
27
+            public int offset(int index) {
28
+                return index * (chunkSize - 1);
29
+            }
30
+
31
+            @Override
32
+            public boolean hasNext() {
33
+                return offset() < bytes.length;
34
+            }
35
+
36
+            public boolean hasNext(int index) {
37
+                return offset(index) < bytes.length;
38
+            }
39
+
40
+            @Override
41
+            public ByteBuffer next() {
42
+                // More chunks?
43
+                byte moreChunks;
44
+                if (hasNext(this.index + 1)) {
45
+                    moreChunks = (byte) 0x01;
46
+                } else {
47
+                    moreChunks = (byte) 0x00;
48
+                }
49
+
50
+                // Put more chunks indicator and bytes into buffer
51
+                // Note: 'allocateDirect' does NOT work, DO NOT CHANGE!
52
+                int offset = offset();
53
+                int length = Math.min(chunkSize, bytes.length + 1 - offset);
54
+                ByteBuffer buffer = ByteBuffer.allocate(length);
55
+                buffer.put(moreChunks);
56
+                buffer.put(bytes, offset, length - 1);
57
+
58
+                // Flip offset and remaining length for reading
59
+                buffer.flip();
60
+                this.index += 1;
61
+                return buffer;
62
+            }
63
+
64
+            @Override
65
+            public void remove() {
66
+                throw new UnsupportedOperationException();
67
+            }
68
+        };
69
+    }
70
+}

+ 62
- 0
src/main/java/org/saltyrtc/client/ClientState.java View File

@@ -0,0 +1,62 @@
1
+package org.saltyrtc.client;
2
+
3
+import java.util.HashMap;
4
+
5
+/**
6
+ * Summarises various general states to simplify recognition of the current state.
7
+ */
8
+public class ClientState extends State {
9
+    public class StateValue {
10
+        public final static String RESET = "reset";
11
+        public final static String TIMEOUT = "timeout";
12
+        public final static String LOST = "lost";
13
+        public final static String CONNECTED = "connected";
14
+        public final static String UNSTABLE = "unstable";
15
+        public final static String DISCONNECTED = "disconnected";
16
+    }
17
+
18
+    public ClientState(String name) {
19
+        super(name);
20
+        this.type = StateType.DANGER;
21
+        this.value = StateValue.DISCONNECTED;
22
+    }
23
+
24
+    public void update(int type, String value) {
25
+        this.type = type;
26
+        this.value = value;
27
+
28
+        // Broadcast
29
+        this.notifyListeners();
30
+    }
31
+
32
+    public void update(HashMap<String, InternalState> states) {
33
+        int weight = 0;
34
+
35
+        // Calculate client state type and value
36
+        for (InternalState state : states.values()) {
37
+            weight += state.type;
38
+        }
39
+
40
+        // Data channel open and PC at most unstable: Force warning if danger
41
+        if (weight >= StateType.DANGER
42
+                && states.get("dc").type == StateType.SUCCESS
43
+                && states.get("pc").type != StateType.DANGER) {
44
+            weight = StateType.WARNING;
45
+        }
46
+
47
+        // Calculate state type
48
+        if (weight < StateType.WARNING) {
49
+            this.type = StateType.SUCCESS;
50
+            this.value = StateValue.CONNECTED;
51
+        } else if (weight < StateType.DANGER) {
52
+            this.type = StateType.WARNING;
53
+            this.value = StateValue.UNSTABLE;
54
+        } else {
55
+            this.type = StateType.DANGER;
56
+            this.value = StateValue.DISCONNECTED;
57
+        }
58
+
59
+        // Broadcast
60
+        this.notifyListeners();
61
+    }
62
+}

+ 374
- 0
src/main/java/org/saltyrtc/client/DataChannel.java View File

@@ -0,0 +1,374 @@
1
+package org.saltyrtc.client;
2
+
3
+import android.util.Log;
4
+
5
+import org.saltyrtc.client.exceptions.CryptoException;
6
+import org.saltyrtc.client.exceptions.InvalidChunkException;
7
+import org.saltyrtc.client.Utils;
8
+
9
+import org.json.JSONException;
10
+import org.json.JSONObject;
11
+import org.webrtc.DataChannel.Buffer;
12
+import org.webrtc.DataChannel.Observer;
13
+import org.webrtc.DataChannel.State;
14
+
15
+import java.nio.ByteBuffer;
16
+import java.util.ArrayList;
17
+
18
+/**
19
+ * Handles the communication between browser and app.
20
+ * Note: Public methods can be used safely from any thread.
21
+ */
22
+public class DataChannel extends EncryptedChannel {
23
+    protected static final String NAME = "DataChannel";
24
+    protected static final String LABEL = "saltyrtc";
25
+    protected static final int HEARTBEAT_ACK_TIMEOUT = 10000;
26
+    protected static final int MTU = 16384;
27
+    protected State state;
28
+    protected org.webrtc.DataChannel dc;
29
+    protected ArrayList<JSONObject> cached;
30
+    protected Events events;
31
+    // Ex protected
32
+    public final StateDispatcher stateDispatcher = new StateDispatcher();
33
+    // Ex protected
34
+    public final DataChannelMessageDispatcher messageDispatcher = new DataChannelMessageDispatcher();
35
+    protected final HeartbeatAckTimer heartbeatAckTimer = new HeartbeatAckTimer();
36
+    protected String heartbeat = null;
37
+
38
+    public interface MessageListener {
39
+        void onMessage(JSONObject message);
40
+    }
41
+
42
+    /**
43
+     * Handles data channel events and dispatches messages.
44
+     * TODO: So... where are the error events?
45
+     */
46
+    protected class Events implements Observer, Unchunkifier.Events {
47
+        private volatile boolean stopped = false;
48
+        private final Unchunkifier unchunkifier;
49
+
50
+        public Events() {
51
+            this.unchunkifier = new Unchunkifier(this);
52
+        }
53
+
54
+        public void stop() {
55
+            this.stopped = true;
56
+        }
57
+
58
+        // Note: For some reason this method is called twice when the data channel closes.
59
+        @Override
60
+        public void onStateChange() {
61
+            Handler.post(new Runnable() {
62
+                @Override
63
+                public void run() {
64
+                    if (!stopped) {
65
+                        setState(dc.state());
66
+                    }
67
+                }
68
+            });
69
+        }
70
+
71
+        @Override
72
+        public void onMessage(Buffer buffer) {
73
+            if (this.stopped) {
74
+                return;
75
+            }
76
+            if (!buffer.binary) {
77
+                Log.w(NAME, "Ignored ASCII message");
78
+            } else {
79
+                // Note: Buffer need to be received directly as it will be disposed after return
80
+                try {
81
+                    this.unchunkifier.add(buffer.data);
82
+                } catch (InvalidChunkException e) {
83
+                    stateDispatcher.error("chunk", "Invalid chunk received: " + e.moreChunks);
84
+                }
85
+            }
86
+        }
87
+
88
+        @Override
89
+        public void onCompletedMessage(final ByteBuffer buffer) {
90
+            // Now that the bytes have been fetched from the buffer, we can safely dispatch
91
+            Handler.post(new Runnable() {
92
+                @Override
93
+                public void run() {
94
+                    if (!stopped) {
95
+                        receive(buffer);
96
+                    }
97
+                }
98
+            });
99
+        }
100
+    }
101
+
102
+    protected class HeartbeatAckTimer implements Runnable {
103
+        @Override
104
+        public void run() {
105
+            Log.e(NAME, "Heartbeat ack timeout");
106
+            stateDispatcher.error("timeout", "Heartbeat ack timeout");
107
+            dc.close();
108
+        }
109
+    }
110
+
111
+    public DataChannel() {
112
+        this.reset(true);
113
+    }
114
+
115
+    // Ex protected
116
+    public void reset() {
117
+        this.reset(false);
118
+    }
119
+
120
+    // Ex protected
121
+    public void reset(boolean hard) {
122
+        // Set to unknown state
123
+        this.setState(null);
124
+
125
+        // Close and reset event instance
126
+        if (this.events != null) {
127
+            this.events.stop();
128
+        }
129
+        this.events = new Events();
130
+
131
+        // Cancel and reset heartbeat ack timer
132
+        this.cancelHeartbeatAckTimer();
133
+        // Reset heartbeat content
134
+        this.heartbeat = null;
135
+
136
+        // Close data channel instance
137
+        if (this.dc != null) {
138
+            this.dc.close();
139
+            this.dc.dispose();
140
+            this.dc = null;
141
+        }
142
+
143
+        // Hard reset?
144
+        if (!hard) {
145
+            return;
146
+        }
147
+
148
+        // Cached messages
149
+        this.cached = new ArrayList<>();
150
+    }
151
+
152
+    protected void setInstance(final org.webrtc.DataChannel dc) {
153
+        this.dc = dc;
154
+        // Register events on data channel instance
155
+        dc.registerObserver(this.events);
156
+        // Set the initial state of the data channel instance
157
+        setState(dc.state());
158
+    }
159
+
160
+    protected void setState(final State state) {
161
+        // Special case: Unknown state
162
+        if (state == null) {
163
+            // Ignore repeated state changes
164
+            if (this.state == null) {
165
+                Log.d(NAME, "Ignoring repeated state: unknown");
166
+                return;
167
+            }
168
+
169
+            // Update state and notify listeners
170
+            this.state = null;
171
+            this.stateDispatcher.state("unknown");
172
+        } else {
173
+            // Ignore repeated state changes
174
+            if (state == this.state) {
175
+                Log.d(NAME, "Ignoring repeated state: " + state.toString().toLowerCase());
176
+                return;
177
+            }
178
+
179
+            // Update state and notify listeners
180
+            this.state = state;
181
+            this.stateDispatcher.state(state.toString().toLowerCase());
182
+        }
183
+    }
184
+
185
+    // Ex protected
186
+    public boolean close() {
187
+        if (this.dc != null) {
188
+            this.dc.close();
189
+            return true;
190
+        } else {
191
+            return false;
192
+        }
193
+    }
194
+
195
+    protected void startHeartbeatAckTimer() {
196
+        Handler.postDelayed(this.heartbeatAckTimer, HEARTBEAT_ACK_TIMEOUT);
197
+    }
198
+
199
+    protected void cancelHeartbeatAckTimer() {
200
+        Handler.removeCallbacks(this.heartbeatAckTimer);
201
+    }
202
+
203
+    // Ex protected
204
+    public void sendCached() {
205
+        Log.d(NAME, "Sending " + this.cached.size() + " delayed messages");
206
+        for (JSONObject message : this.cached) {
207
+            this.send(message);
208
+        }
209
+        this.cached.clear();
210
+    }
211
+
212
+    // Ex protected
213
+    public void sendMessage(JSONObject inner) {
214
+        // Build JSON
215
+        JSONObject message = new JSONObject();
216
+        try {
217
+            // Prepare data
218
+            message.put("type", "message");
219
+            message.put("data", inner);
220
+        } catch (JSONException e) {
221
+            Log.e(NAME, "Message encode error: " + e.toString());
222
+            e.printStackTrace();
223
+            this.stateDispatcher.error("encode", e.toString());
224
+            return;
225
+        }
226
+
227
+        // Send message
228
+        this.send(message);
229
+    }
230
+
231
+    // Ex protected
232
+    public void receiveMessage(JSONObject inner) {
233
+        Log.d(NAME, "Broadcasting message");
234
+        this.messageDispatcher.message(inner);
235
+    }
236
+
237
+    protected void sendHeartbeat() {
238
+        this.sendHeartbeat(Utils.getRandomString());
239
+    }
240
+
241
+    protected void sendHeartbeat(String content) {
242
+        Log.d(NAME, "Sending heartbeat");
243
+
244
+        // Store heartbeat
245
+        this.heartbeat = content;
246
+
247
+        // Build JSON
248
+        JSONObject message = new JSONObject();
249
+        try {
250
+            // Prepare data
251
+            message.put("type", "heartbeat");
252
+            message.put("data", content);
253
+        } catch (JSONException e) {
254
+            Log.e(NAME, "Heartbeat encode error: " + e.toString());
255
+            e.printStackTrace();
256
+            this.stateDispatcher.error("encode", e.toString());
257
+            return;
258
+        }
259
+
260
+        // Start timer and send heartbeat
261
+        this.startHeartbeatAckTimer();
262
+        this.send(message);
263
+    }
264
+
265
+    protected void receiveHeartbeatAck(String content) {
266
+        // Validate heartbeat ack
267
+        if (this.heartbeat == null) {
268
+            Log.w(NAME, "Ignored heartbeat-ack that has not been sent");
269
+            return;
270
+        }
271
+        if (!content.equals(this.heartbeat)) {
272
+            Log.e(NAME, "Heartbeat-ack does not match, expected: " + this.heartbeat +
273
+                    "received: " + content);
274
+            this.stateDispatcher.error("heartbeat", "Content did not match");
275
+        } else {
276
+            Log.d(NAME, "Received heartbeat-ack");
277
+            this.heartbeat = null;
278
+            // Cancel heartbeat ack timer
279
+            this.cancelHeartbeatAckTimer();
280
+        }
281
+    }
282
+
283
+    protected void receiveHeartbeat(String content) {
284
+        Log.d(NAME, "Received heartbeat");
285
+        this.sendHeartbeatAck(content);
286
+    }
287
+
288
+    protected void sendHeartbeatAck(String content) {
289
+        Log.d(NAME, "Sending heartbeat-ack");
290
+
291
+        // Build JSON
292
+        JSONObject message = new JSONObject();
293
+        try {
294
+            // Prepare data
295
+            message.put("type", "heartbeat-ack");
296
+            message.put("data", content);
297
+        } catch (JSONException e) {
298
+            this.stateDispatcher.error("encode", e.toString());
299
+            Log.e(NAME, "Heartbeat ack encode error: " + e.toString());
300
+            e.printStackTrace();
301
+            return;
302
+        }
303
+
304
+        // Send heartbeat ack
305
+        this.send(message);
306
+    }
307
+
308
+    protected void send(JSONObject message) {
309
+        // Delay sending until connected
310
+        if (this.dc != null && this.state == State.OPEN) {
311
+            KeyStore.Box box;
312
+            try {
313
+                // Encrypt data
314
+                box = this.encrypt(message.toString());
315
+            } catch (CryptoException e) {
316
+                this.stateDispatcher.error(e.getState(), e.getError());
317
+                return;
318
+            }
319
+
320
+            // Send chunks
321
+            String sizeKb = String.format("%.2f", ((float) box.getSize()) / 1024);
322
+            Log.d(NAME, "Sending message (size: " + sizeKb + " KB): " + message);
323
+            Chunkifier chunkifier = new Chunkifier(box.getBuffer().array(), MTU);
324
+            for (ByteBuffer chunk : chunkifier) {
325
+                // Wrap buffer into data channel buffer and set the 'binary' flag
326
+                Buffer buffer = new Buffer(chunk, true);
327
+
328
+                // Send buffer content
329
+                this.dc.send(buffer);
330
+            }
331
+        } else {
332
+            Log.d(NAME, "Delaying message until channel is open");
333
+            this.cached.add(message);
334
+        }
335
+    }
336
+
337
+    protected void receive(ByteBuffer buffer) {
338
+        final String data;
339
+        KeyStore.Box box = new KeyStore.Box(buffer);
340
+        String sizeKb = String.format("%.2f", ((float) box.getSize()) / 1024);
341
+
342
+        // Decrypt data
343
+        try {
344
+            data = this.decrypt(box);
345
+        } catch (CryptoException e) {
346
+            stateDispatcher.error(e.getState(), e.getError());
347
+            return;
348
+        }
349
+
350
+        try {
351
+            // Decode data
352
+            Log.d(NAME, "Received message (size: " + sizeKb + " KB): " + data);
353
+            JSONObject message = new JSONObject(data);
354
+            String type = message.getString("type");
355
+
356
+            // Relay message
357
+            //noinspection IfCanBeSwitch
358
+            if (type.equals("message")) {
359
+                JSONObject inner = message.getJSONObject("data");
360
+                receiveMessage(inner);
361
+            } else if (type.equals("heartbeat-ack")) {
362
+                String content = message.getString("data");
363
+                receiveHeartbeatAck(content);
364
+            } else if (type.equals("heartbeat")) {
365
+                String content = message.getString("data");
366
+                receiveHeartbeat(content);
367
+            } else {
368
+                Log.w(NAME, "Ignored message: " + data);
369
+            }
370
+        } catch (JSONException e) {
371
+            Log.w(NAME, "Ignored invalid message: " + data);
372
+        }
373
+    }
374
+}

+ 16
- 0
src/main/java/org/saltyrtc/client/DataChannelMessageDispatcher.java View File

@@ -0,0 +1,16 @@
1
+package org.saltyrtc.client;
2
+
3
+import org.json.JSONObject;
4
+
5
+public class DataChannelMessageDispatcher extends MessageDispatcher<DataChannel.MessageListener> {
6
+    protected void message(final JSONObject message) {
7
+        Handler.post(new Runnable() {
8
+            @Override
9
+            public void run() {
10
+                if (listener != null) {
11
+                    listener.onMessage(message);
12
+                }
13
+            }
14
+        });
15
+    }
16
+}

+ 62
- 0
src/main/java/org/saltyrtc/client/EncryptedChannel.java View File

@@ -0,0 +1,62 @@
1
+package org.saltyrtc.client;
2
+
3
+import org.saltyrtc.client.exceptions.CryptoException;
4
+import org.saltyrtc.client.exceptions.CryptoFailedException;
5
+import org.saltyrtc.client.exceptions.OtherKeyMissingException;
6
+
7
+import java.io.UnsupportedEncodingException;
8
+
9
+/**
10
+ * Handles binary packing and unpacking.
11
+ * Simplifies the usage of KeyStore by handling all exceptions.
12
+ */
13
+public class EncryptedChannel {
14
+    protected static final String NAME = "EncryptedChannel";
15
+
16
+    protected KeyStore.Box encrypt(String message) throws CryptoException {
17
+        // Encrypt data
18
+        try {
19
+            return KeyStore.encrypt(message);
20
+        } catch (OtherKeyMissingException e) {
21
+            this.throwCryptoException(
22
+                    e, "key", "Cannot encrypt, public key of recipient is missing");
23
+        } catch (UnsupportedEncodingException e) {
24
+            this.throwCryptoException(
25
+                    e, "encode", "Cannot encrypt, UTF-8 encoding not supported");
26
+        } catch (CryptoFailedException e) {
27
+            this.throwCryptoException(
28
+                    e, "crypto", "Cannot encrypt, invalid data or keys don't match");
29
+        }
30
+
31
+        // Unreachable section
32
+        return null;
33
+    }
34
+
35
+    protected final String decrypt(KeyStore.Box box) throws CryptoException {
36
+        // Decrypt data
37
+        try {
38
+            return KeyStore.decrypt(box);
39
+        } catch (OtherKeyMissingException e) {
40
+            this.throwCryptoException(
41
+                    e, "key", "Cannot decrypt, public key of recipient is missing");
42
+        } catch (UnsupportedEncodingException e) {
43
+            this.throwCryptoException(
44
+                    e, "encode", "Cannot decrypt, UTF-8 encoding not supported");
45
+        } catch (CryptoFailedException e) {
46
+            this.throwCryptoException(
47
+                    e, "crypto", "Cannot decrypt, invalid data or keys don't match");
48
+        }
49
+
50
+        // Unreachable section
51
+        return null;
52
+    }
53
+
54
+    protected void throwCryptoException(
55
+            Exception e,
56
+            final String state,
57
+            final String error
58
+    ) throws CryptoException {
59
+        e.printStackTrace();
60
+        throw new CryptoException(state, error);
61
+    }
62
+}

+ 5
- 0
src/main/java/org/saltyrtc/client/ErrorStateHandler.java View File

@@ -0,0 +1,5 @@
1
+package org.saltyrtc.client;
2
+
3
+public interface ErrorStateHandler {
4
+    void handle(final String error);
5
+}

+ 29
- 0
src/main/java/org/saltyrtc/client/Handler.java View File

@@ -0,0 +1,29 @@
1
+package org.saltyrtc.client;
2
+
3
+/**
4
+ * A globally accessible handler proxy that is used for the whole package
5
+ * to avoid the need for synchronisation.
6
+ */
7
+public class Handler {
8
+    protected static android.os.Handler handler;
9
+
10
+    /**
11
+     * Note: This method has to be called before any other method can be called safely.
12
+     * @param handler A handler instance.
13
+     */
14
+    public static void setHandler(android.os.Handler handler) {
15
+        Handler.handler = handler;
16
+    }
17
+
18
+    public static boolean post(Runnable runnable) {
19
+        return handler.post(runnable);
20
+    }
21
+
22
+    public static boolean postDelayed(Runnable runnable, long delay) {
23
+        return handler.postDelayed(runnable, delay);
24
+    }
25
+
26
+    public static void removeCallbacks(Runnable runnable) {
27
+        handler.removeCallbacks(runnable);
28
+    }
29
+}

+ 45
- 0
src/main/java/org/saltyrtc/client/InternalState.java View File

@@ -0,0 +1,45 @@
1
+package org.saltyrtc.client;
2
+
3
+import android.util.Log;
4
+
5
+import java.util.HashMap;
6
+
7
+/**
8
+ * Contains type and value of a specific state instance.
9
+ * Note: Public methods can be used safely from any thread.
10
+ */
11
+public class InternalState extends State {
12
+    protected final HashMap<String, Integer> rules = new HashMap<>();
13
+
14
+    public InternalState(String name) {
15
+        super(name);
16
+        this.reset();
17
+    }
18
+
19
+    protected void reset() {
20
+        this.type = StateType.DANGER;
21
+        this.value = "unknown";
22
+    }
23
+
24
+    protected void addRules(HashMap<String, Integer> rules) {
25
+        this.rules.putAll(rules);
26
+    }
27
+
28
+    protected void update(String value) {
29
+        // Find state type for value in rules
30
+        Integer type = this.rules.get(value);
31
+        if (type == null) {
32
+            Log.w(name, "Unknown state '" + value + "' for " + this.toString());
33
+            Log.d(name, "Rules: " + this.rules.toString());
34
+            type = StateType.DANGER;
35
+            value = "unknown";
36
+        }
37
+
38
+        // Update type and value
39
+        this.type = type;
40
+        this.value = value;
41
+
42
+        // Broadcast
43
+        this.notifyListeners();
44
+    }
45
+}

+ 93
- 0
src/main/java/org/saltyrtc/client/InternalStateListener.java View File

@@ -0,0 +1,93 @@
1
+package org.saltyrtc.client;
2
+
3
+import android.util.Log;
4
+
5
+import java.util.HashMap;
6
+
7
+/**
8
+ * Listener for state change events.
9
+ */
10
+public abstract class InternalStateListener {
11
+    protected static final String NAME = "InternalStateListener";
12
+    protected final HashMap<String, StateHandler> stateHandler = new HashMap<>();
13
+    protected final HashMap<String, ErrorStateHandler> errorHandler = new HashMap<>();
14
+
15
+    /**
16
+     * Will be called for all state changes.
17
+     * @param state A state.
18
+     */
19
+    public abstract void onState(final String state);
20
+
21
+    /**
22
+     * Will always be called for all error states.
23
+     * @param state An error state.
24
+     * @param error An error message.
25
+     */
26
+    public abstract void onError(final String state, final String error);
27
+
28
+    /**
29
+     * Will pass the state to onState first.
30
+     * Checks if a state handler for a specific state exists and invokes it.
31
+     */
32
+    protected void handleState(String state) {
33
+        this.onState(state);
34
+        if (this.stateHandler != null && this.stateHandler.containsKey(state)) {
35
+            Log.d(NAME, "Calling handler for state: " + state);
36
+            final StateHandler handler = this.stateHandler.get(state);
37
+
38
+            // Handle state in runnable
39
+            Handler.post(new Runnable() {
40
+                @Override
41
+                public void run() {
42
+                    handler.handle();
43
+                }
44
+            });
45
+        }
46
+    }
47
+
48
+    /**
49
+     * Will pass the error state to onError first.
50
+     * Checks if a error state handler for a specific error state exists and invokes it.
51
+     */
52
+    protected void handleError(String state, final String error) {
53
+        this.onError(state, error);
54
+        if (this.errorHandler != null && this.errorHandler.containsKey(state)) {
55
+            Log.d(NAME, "Calling handler for error state: " + state);
56
+            final ErrorStateHandler handler = this.errorHandler.get(state);
57
+
58
+            // Handle error state in runnable
59
+            Handler.post(new Runnable() {
60
+                @Override
61
+                public void run() {
62
+                    handler.handle(error);
63
+                }
64
+            });
65
+        }
66
+    }
67
+
68
+    /**
69
+     * Add a state handler. State handlers will be executed in the event looper.
70
+     * @param state The state the handler will be applied on.
71
+     * @param handler State handler instance.
72
+     */
73
+    protected void addStateHandler(String state, StateHandler handler) {
74
+        this.stateHandler.put(state, handler);
75
+    }
76
+
77
+    protected void removeStateHandler(String state) {
78
+        this.stateHandler.remove(state);
79
+    }
80
+
81
+    /**
82
+     * Add an error state handler. Error state handlers will be executed in the event looper.
83
+     * @param state The error state the handler will be applied on.
84
+     * @param handler Error state handler instance.
85
+     */
86
+    protected void addErrorHandler(String state, ErrorStateHandler handler) {
87
+        this.errorHandler.put(state, handler);
88
+    }
89
+
90
+    protected void removeErrorHandler(String state) {
91
+        this.errorHandler.remove(state);
92
+    }
93
+}

+ 161
- 0
src/main/java/org/saltyrtc/client/KeyStore.java View File

@@ -0,0 +1,161 @@
1
+package org.saltyrtc.client;
2
+
3
+import android.util.Log;
4
+import org.saltyrtc.client.exceptions.CryptoFailedException;
5
+import org.saltyrtc.client.exceptions.InvalidKeyException;
6
+import org.saltyrtc.client.exceptions.OtherKeyMissingException;
7
+import org.saltyrtc.client.Utils;
8
+
9
+import com.neilalexander.jnacl.NaCl;
10
+
11
+import java.io.UnsupportedEncodingException;
12
+import java.nio.ByteBuffer;
13
+import java.security.SecureRandom;
14
+
15
+/**
16
+ * Handles encrypting and decrypting messages for the peers.
17
+ * Note: This class is thread safe.
18
+ */
19
+public class KeyStore {
20
+    protected static final String name = "KeyStore";
21
+    protected static boolean setupDone = false;
22
+    protected static final byte[] privateKey = new byte[NaCl.SECRETKEYBYTES];
23
+    protected static final byte[] publicKey = new byte[NaCl.PUBLICKEYBYTES];
24
+    protected static byte[] otherKey = null;
25
+    protected static NaCl nacl = null;
26
+    protected static final SecureRandom random = new SecureRandom();
27
+
28
+    public static class Box {
29
+        public final byte[] nonce;
30
+        public final byte[] data;
31
+
32
+        public Box(byte[] nonce, byte[] data) {
33
+            this.nonce = nonce;
34
+            this.data = data;
35
+        }
36
+
37
+        public Box(ByteBuffer buffer) {
38
+            // Unpack nonce
39
+            this.nonce = new byte[NaCl.NONCEBYTES];
40
+            buffer.get(nonce, 0, NaCl.NONCEBYTES);
41
+
42
+            // Unpack data
43
+            this.data = new byte[buffer.remaining()];
44
+            buffer.get(data);
45
+        }
46
+
47
+        public int getSize() {
48
+            return this.nonce.length + this.data.length;
49
+        }
50
+
51
+        public ByteBuffer getBuffer() {
52
+            // Pack data
53
+            // Note: 'allocateDirect' does NOT work, DO NOT CHANGE!
54
+            ByteBuffer box = ByteBuffer.allocate(this.getSize());
55
+            box.put(this.nonce);
56
+            box.put(this.data);
57
+
58
+            // Flip offset and remaining length for reading
59
+            box.flip();
60
+
61
+            // Return box as byte buffer
62
+            return box;
63
+        }
64
+    }
65
+
66
+    public synchronized static String getPublicKey() {
67
+        return NaCl.asHex(publicKey);
68
+    }
69
+
70
+    public synchronized static void setOtherKey(String otherKey) throws InvalidKeyException {
71
+        // Start setup if required
72
+        setup();
73
+        // Store binary key
74
+        KeyStore.otherKey = NaCl.getBinary(otherKey);
75
+        // Create getNaCl for encryption and decryption
76
+        try {
77
+            nacl = new NaCl(privateKey, KeyStore.otherKey);
78
+        } catch (Error e) {
79
+            throw new InvalidKeyException(e.toString());
80
+        }
81
+    }
82
+
83
+    public synchronized static NaCl getNaCl() throws OtherKeyMissingException {
84
+        if (nacl == null) {
85
+            throw new OtherKeyMissingException();
86
+        }
87
+        return nacl;
88
+    }
89
+
90
+    public synchronized static void setup() {
91
+        // Skip if already set up
92
+        if (setupDone) {
93
+            return;
94
+        }
95
+
96
+        // Generate key pair
97
+        Log.d(name, "Generating new key pair");
98
+        NaCl.genkeypair(KeyStore.publicKey, KeyStore.privateKey);
99
+        Log.d(name, "Private key: " + NaCl.asHex(KeyStore.privateKey));
100
+        Log.d(name, "Public key: " + NaCl.asHex(KeyStore.publicKey));
101
+        setupDone = true;
102
+
103
+        // Make sure encryption and decryption works properly
104
+        // TODO: Self-test
105
+        NaCl nacl = KeyStore.nacl;
106
+        KeyStore.nacl = new NaCl(privateKey, publicKey);
107
+        String expected = Utils.getRandomString();
108
+        try {
109
+            if (!decrypt(encrypt(expected)).equals(expected)) {
110
+                throw new AssertionError("Self-test failed");
111
+            }
112
+        } catch (Exception e) {
113
+            Log.e(name, "Self-test failed");
114
+            e.printStackTrace();
115
+        }
116
+        Log.d(name, "Self-test passed");
117
+        KeyStore.nacl = nacl;
118
+    }
119
+
120
+    public static Box encrypt(String message) throws
121
+            OtherKeyMissingException, UnsupportedEncodingException,
122
+            CryptoFailedException {
123
+        // Convert string to bytes
124
+        byte[] data = message.getBytes("UTF-8");
125
+
126
+        // Generate random nonce
127
+        byte[] nonce = new byte[NaCl.NONCEBYTES];
128
+        random.nextBytes(nonce);
129
+
130
+        // Encrypt data with keys and nonce
131
+        try {
132
+            data = getNaCl().encrypt(data, nonce);
133
+        } catch (Error e) {
134
+            throw new CryptoFailedException(e.toString());
135
+        }
136
+        if (data == null) {
137
+            throw new CryptoFailedException("Encrypted data is null");
138
+        }
139
+
140
+        // Return box
141
+        return new Box(nonce, data);
142
+    }
143
+
144
+    public static String decrypt(Box box) throws
145
+            OtherKeyMissingException, UnsupportedEncodingException,
146
+            CryptoFailedException {
147
+        // Decrypt data
148
+        byte[] data;
149
+        try {
150
+            data = getNaCl().decrypt(box.data, box.nonce);
151
+        } catch (Error e) {
152
+            throw new CryptoFailedException(e.toString());
153
+        }
154
+        if (data == null) {
155
+            throw new CryptoFailedException("Decrypted data is null");
156
+        }
157
+
158
+        // Return data as string
159
+        return new String(data, "UTF-8");
160
+    }
161
+}

+ 20
- 0
src/main/java/org/saltyrtc/client/MessageDispatcher.java View File

@@ -0,0 +1,20 @@
1
+package org.saltyrtc.client;
2
+
3
+public class MessageDispatcher<ML> {
4
+    protected ML listener = null;
5
+
6
+    protected ML getListener() {
7
+        return listener;
8
+    }
9
+
10
+    // Ex protected
11
+    public void setListener(ML messageListener) {
12
+        this.listener = messageListener;
13
+    }
14
+
15
+    public void removeListener(ML messageListener) {
16
+        if (this.listener == messageListener) {
17
+            this.listener = null;
18
+        }
19
+    }
20
+}

+ 443
- 0
src/main/java/org/saltyrtc/client/PeerConnection.java View File

@@ -0,0 +1,443 @@
1
+package org.saltyrtc.client;
2
+
3
+import android.content.Context;
4
+import android.util.Log;
5
+import org.webrtc.*;
6
+import org.webrtc.PeerConnection.*;
7
+
8
+import java.util.ArrayList;
9
+import java.util.LinkedList;
10
+
11
+/**
12
+ * The connection between the peers (obviously). Creates the data channel and handles connection
13
+ * events.
14
+ * Public methods can be used safely from any thread.
15
+ */
16
+public class PeerConnection {
17
+    protected static final String NAME = "PeerConnection";
18
+    protected PeerConnectionFactory factory;
19
+    protected String state = null;
20
+    protected org.webrtc.PeerConnection pc;
21
+    protected final org.saltyrtc.client.DataChannel dc;
22
+    protected MediaConstraints constraints = new MediaConstraints();
23
+    protected LinkedList<IceServer> iceServers = new LinkedList<>();
24
+    protected boolean descriptionsExchanged;
25
+    protected ArrayList<IceCandidate> localCandidates;
26
+    protected ArrayList<IceCandidate> remoteCandidates;
27
+    protected Events events;
28
+    protected LocalDescriptionEvents localDescriptionEvents;
29
+    protected RemoteDescriptionEvents remoteDescriptionEvents;
30
+    // Ex protected
31
+    public final StateDispatcher stateDispatcher = new StateDispatcher();
32
+    // Ex protected
33
+    public final PeerConnectionMessageDispatcher messageDispatcher = new PeerConnectionMessageDispatcher();
34
+
35
+    /**
36
+     * Listener for message (answer and candidate) dispatch request events.
37
+     */
38
+    public interface MessageListener {
39
+        void onAnswer(SessionDescription description);
40
+        void onCandidate(IceCandidate candidate);
41
+    }
42
+
43
+    /**
44
+     * Handles signaling server events and dispatches messages.
45
+     */
46
+    protected class Events implements Observer {
47
+        private volatile boolean stopped = false;
48
+
49
+        public void stop() {
50
+            this.stopped = true;
51
+        }
52
+
53
+        @Override
54
+        public void onRenegotiationNeeded() {
55
+            if (!this.stopped) {
56
+                Log.w(NAME, "Ignored renegotiation request");
57
+            }
58
+        }
59
+
60
+        @Override
61
+        public void onIceCandidate(final IceCandidate iceCandidate) {
62
+            // Note: This check might not be necessary but the browser does it as well
63
+            if (iceCandidate != null) {
64
+                Handler.post(new Runnable() {
65
+                    @Override
66
+                    public void run() {
67
+                        if (!stopped) {
68
+                            sendCandidate(iceCandidate);
69
+                        }
70
+                    }
71
+                });
72
+            }
73
+        }
74
+
75
+        @Override
76
+        public void onSignalingChange(SignalingState signalingState) {
77
+            if (!this.stopped) {
78
+                Log.d(NAME, "Ignored signaling state change to: " + signalingState.toString());
79
+            }
80
+        }
81
+
82
+        @Override
83
+        public void onAddStream(MediaStream mediaStream) {
84
+            if (!this.stopped) {
85
+                Log.w(NAME, "Ignored incoming media stream");
86
+            }
87
+        }
88
+
89
+        @Override
90
+        public void onRemoveStream(MediaStream mediaStream) {
91
+            if (!this.stopped) {
92
+                Log.w(NAME, "Ignored media stream removal");
93
+            }
94
+        }
95
+
96
+        @Override
97
+        public void onIceConnectionChange(IceConnectionState iceConnectionState) {
98
+            final String state = iceConnectionState.toString().toLowerCase();
99
+            // Set state
100
+            Handler.post(new Runnable() {
101
+                @Override
102
+                public void run() {
103
+                    if (!stopped) {
104
+                        setState(state);
105
+                    }
106
+                }
107
+            });
108
+        }
109
+
110
+        @Override
111
+        public void onIceGatheringChange(IceGatheringState iceGatheringState) {
112
+            if (this.stopped) {
113
+                return;
114
+            }
115
+            Log.d(NAME, "Ignored ICE gathering state change to: " + iceGatheringState.toString());
116
+        }
117
+
118
+        @Override
119
+        public void onDataChannel(final org.webrtc.DataChannel incomingDc) {
120
+            // Validate label
121
+            if (incomingDc.label().equals(org.saltyrtc.client.DataChannel.LABEL)) {
122
+                // Set incoming data channel instance
123
+                Handler.post(new Runnable() {
124
+                    @Override
125
+                    public void run() {
126
+                        if (stopped) {
127
+                            return;
128
+                        }
129
+                        Log.i(NAME, "Received channel");
130
+                        dc.setInstance(incomingDc);
131
+                    }
132
+                });
133
+            } else {
134
+                Log.w(NAME, "Ignored channel with label: " + incomingDc.label());
135
+            }
136
+        }
137
+    }
138
+
139
+    /**
140
+     * Handles session description events that have been emitted by the local side.
141
+     */
142
+    protected class LocalDescriptionEvents implements SdpObserver {
143
+        private volatile boolean stopped = false;
144
+
145
+        public void stop() {
146
+            this.stopped = true;
147
+        }
148
+
149
+        @Override
150
+        public void onCreateSuccess(final SessionDescription originalDescription) {
151
+            // Set local description after creation
152
+            Handler.post(new Runnable() {
153
+                @Override
154
+                public void run() {
155
+                    if (stopped) {
156
+                        return;
157
+                    }
158
+                    // Dirty hack to increase the application specific bandwidth
159
+                    // Note: This setting exists because the current SCTP over DTLS implementation
160
+                    //       lacks a flow control mechanism. However, 30 kbps is really not what
161
+                    //       we want here, so we increase the allowed bandwidth.
162
+                    final SessionDescription modifiedDescription;
163
+                    SessionDescription.Type type = originalDescription.type;
164
+                    String[] parts = originalDescription.description.split("b=AS:30");
165
+                    if (parts.length == 2) {
166
+                        Log.d(NAME, "Overriding bandwidth setting to 100 Mbps");
167
+                        modifiedDescription = new SessionDescription(
168
+                                type, (parts[0] + "b=AS:102400" + parts[1]));
169
+                    } else {
170
+                        Log.w(NAME, "Couldn't override bandwidth setting");
171
+                        modifiedDescription = originalDescription;
172
+                    }
173
+                    pc.setLocalDescription(localDescriptionEvents, modifiedDescription);
174
+                }
175
+            });
176
+        }
177
+
178
+        @Override
179
+        public void onSetSuccess() {
180
+            Log.d(NAME, "Local description set");
181
+            Handler.post(new Runnable() {
182
+                @Override
183
+                public void run() {
184
+                    if (stopped) {
185
+                        return;
186
+                    }
187
+                    // Answer created
188
+                    SessionDescription description = pc.getLocalDescription();
189
+                    // Note: So this thing can be null for some idiotic reason...
190
+                    if (description == null) {
191
+                        stateDispatcher.error("local", "Local description was null");
192
+                        return;
193
+                    }
194
+                    // Send the answer
195
+                    messageDispatcher.answer(description);
196
+                    // Send the local candidates and set the remote candidates
197
+                    handleCachedCandidates();
198
+                }
199
+            });
200
+        }
201
+
202
+        @Override
203
+        public void onCreateFailure(String error) {
204
+            if (this.stopped) {
205
+                return;
206
+            }
207
+            Log.e(NAME, "Creating answer failed: " + error);
208
+            stateDispatcher.error("create", error);
209
+        }
210
+
211
+        @Override
212
+        public void onSetFailure(String error) {
213
+            if (this.stopped) {
214
+                return;
215
+            }
216
+            Log.e(NAME, "Setting local description failed: " + error);
217
+            stateDispatcher.error("local", error);
218
+        }
219
+    }
220
+
221
+    /**
222
+     * Handles session description events that have been emitted by the remote side.
223
+     */
224
+    protected class RemoteDescriptionEvents implements SdpObserver {
225
+        private volatile boolean stopped = false;
226
+
227
+        public void stop() {
228
+            this.stopped = true;
229
+        }
230
+
231
+        @Override
232
+        public void onCreateSuccess(SessionDescription description) {
233
+            if (this.stopped) {
234
+                return;
235
+            }
236
+            // Note: Not used, should never trigger
237
+            Log.w(NAME, "Ignored remote description create event");
238
+        }
239
+
240
+        @Override
241
+        public void onSetSuccess() {
242
+            Log.d(NAME, "Remote description set");
243
+            Handler.post(new Runnable() {
244
+                @Override
245
+                public void run() {
246
+                    if (stopped) {
247
+                        return;
248
+                    }
249
+                    // Offer received: Send answer
250
+                    sendAnswer();
251
+                }
252
+            });
253
+        }
254
+
255
+        @Override
256
+        public void onCreateFailure(String error) {
257
+            if (this.stopped) {
258
+                return;
259
+            }
260
+            // Note: Not used, should never trigger
261
+            Log.w(NAME, "Ignored remote description creation failure: " + error);
262
+        }
263
+
264
+        @Override
265
+        public void onSetFailure(String error) {
266
+            if (this.stopped) {
267
+                return;
268
+            }
269
+            Log.e(NAME, "Setting remote description failed: " + error);
270
+            stateDispatcher.error("remote", error);
271
+        }
272
+    }
273
+
274
+    /**
275
+     * TODO: Description
276
+     *
277
+     * @param dc The used data channel (wrapper) instance.
278
+     * @param context Required because WebRTC init stuff... dunno, just don't ask
279
+     */
280
+    public PeerConnection(org.saltyrtc.client.DataChannel dc, Context context) {
281
+        this.dc = dc;
282
+
283
+        // Set initial state
284
+        this.setState("unknown");
285
+
286
+        // Some init stuff... no idea why it's required and what it's doing exactly but otherwise
287
+        // the app crashes...
288
+        PeerConnectionFactory.initializeFieldTrials(null);
289
+
290
+        // For some shitty reason we need to initialise audio here...
291
+        // See: https://code.google.com/p/webrtc/issues/detail?id=3416
292
+        if (!PeerConnectionFactory.initializeAndroidGlobals(
293
+                context, true, false, false, null
294
+        )) {
295
+            Log.e(NAME, "Initialising Android globals failed!");
296
+            this.stateDispatcher.error("init", "Initialising Android globals failed");
297
+            return;
298
+        }
299
+
300
+        // Now we can safely create the factory... hopefully...
301
+        this.factory = new PeerConnectionFactory();
302
+
303
+        // Session description constraints
304
+        this.constraints.mandatory.add(new MediaConstraints.KeyValuePair(
305
+                "OfferToReceiveVideo", "false"
306
+        ));
307
+        this.constraints.mandatory.add(new MediaConstraints.KeyValuePair(
308
+                "OfferToReceiveAudio", "false"
309
+        ));
310
+
311
+        // Set ice servers
312
+        this.iceServers.add(new IceServer(
313
+                "turn:example.org",
314
+                "user",
315
+                "pass"
316
+        ));
317
+    }
318
+
319
+    protected void setState(String state) {
320
+        // Ignore repeated state changes
321
+        if (state.equals(this.state)) {
322
+            Log.d(NAME, "Ignoring repeated state: " + state);
323
+            return;
324
+        }
325
+
326
+        // Update state and notify listeners
327
+        this.state = state;
328
+        this.stateDispatcher.state(state);
329
+    }
330
+
331
+    // Ex protected
332
+    public void reset() {
333
+        this.setState("unknown");
334
+
335
+        // Close and reset event instances
336
+        if (this.events != null) {
337
+            this.events.stop();
338
+            this.localDescriptionEvents.stop();
339
+            this.remoteDescriptionEvents.stop();
340
+        }
341
+        this.events = new Events();
342
+        this.localDescriptionEvents = new LocalDescriptionEvents();
343
+        this.remoteDescriptionEvents = new RemoteDescriptionEvents();
344
+
345
+        // Close peer connection instance
346
+        if (this.pc != null) {
347
+            Log.d(NAME, "Closing");
348
+            this.pc.close();
349
+            this.pc.dispose();
350
+            this.pc = null;
351
+        }
352
+        this.descriptionsExchanged = false;
353
+
354
+        // Cached ICE candidates
355
+        this.localCandidates = new ArrayList<>();
356
+        this.remoteCandidates = new ArrayList<>();
357
+    }
358
+
359
+    // Ex protected
360
+    public void create() {
361
+        this.create(null, null);
362
+    }
363
+
364
+    protected void create(MediaConstraints constraints, LinkedList<IceServer> iceServers) {
365
+        // Override defaults
366
+        if (constraints != null) {
367
+            this.constraints = constraints;
368
+        }
369
+        if (iceServers != null) {
370
+            this.iceServers = iceServers;
371
+        }
372
+
373
+        // Enable data channel communication with Firefox and Chromium
374
+        // Note: This shouldn't be necessary anymore but it doesn't do any harm either
375
+        MediaConstraints peerConstraints = new MediaConstraints();
376
+        peerConstraints.optional.add(new MediaConstraints.KeyValuePair(
377
+                "DtlsSrtpKeyAgreement", "true"
378
+        ));
379
+
380
+        // Create peer connection
381
+        this.setState("init");
382
+        this.pc = this.factory.createPeerConnection(
383
+                this.iceServers, peerConstraints, this.events
384
+        );
385
+        Log.d(NAME, "Peer Connection created");
386
+    }
387
+
388
+    // Ex protected
389
+    public void receiveOffer(SessionDescription description) {
390
+        Log.i(NAME, "Received offer");
391
+        this.pc.setRemoteDescription(this.remoteDescriptionEvents, description);
392
+    }
393
+
394
+    protected void sendAnswer() {
395
+        Log.i(NAME, "Creating answer");
396
+        this.pc.createAnswer(this.localDescriptionEvents, this.constraints);
397
+    }
398
+
399
+    protected void sendCandidate(IceCandidate candidate) {
400
+        if (this.descriptionsExchanged) {
401
+            // Send candidate
402
+            Log.d(NAME, "Broadcasting candidate");
403
+            this.messageDispatcher.candidate(candidate);
404
+        } else {
405
+            // Cache candidates if no answer has been received yet
406
+            this.localCandidates.add(candidate);
407
+        }
408
+    }
409
+
410
+    // Ex protected
411
+    public void receiveCandidate(IceCandidate candidate) {
412
+        Log.d(NAME, "Received candidate");
413
+        if (!this.descriptionsExchanged) {
414
+            // Queue candidates if not connected
415
+            // Note: This is required because the app will crash if a candidate is added
416
+            //       before the local description has been set.
417
+            Log.d(NAME, "Delaying setting remote candidate until descriptions have been exchanged");
418
+            this.remoteCandidates.add(candidate);
419
+        } else {
420
+            // Note: A weird freeze occurred here... if this happens again, you're fucked!
421
+            this.pc.addIceCandidate(candidate);
422
+            Log.d(NAME, "Candidate set");
423
+        }
424
+    }
425
+
426
+    protected void handleCachedCandidates() {
427
+        this.descriptionsExchanged = true;
428
+
429
+        // Send cached local candidates
430
+        Log.d(NAME, "Sending " + this.localCandidates.size() + " delayed local candidates");
431
+        for (IceCandidate candidate : this.localCandidates) {
432
+            this.sendCandidate(candidate);
433
+        }
434
+        this.localCandidates.clear();
435
+
436
+        // Set cached remote candidates
437
+        Log.d(NAME, "Setting " + this.remoteCandidates.size() + " delayed remote candidates");
438
+        for (IceCandidate candidate : this.remoteCandidates) {
439
+            this.pc.addIceCandidate(candidate);
440
+        }
441
+        this.remoteCandidates.clear();
442
+    }
443
+}

+ 28
- 0
src/main/java/org/saltyrtc/client/PeerConnectionMessageDispatcher.java View File

@@ -0,0 +1,28 @@
1
+package org.saltyrtc.client;
2
+
3
+import org.webrtc.IceCandidate;
4
+import org.webrtc.SessionDescription;
5
+
6
+public class PeerConnectionMessageDispatcher extends MessageDispatcher<PeerConnection.MessageListener> {
7
+    protected void answer(final SessionDescription description) {
8
+        Handler.post(new Runnable() {
9
+            @Override
10
+            public void run() {
11
+                if (listener != null) {
12
+                    listener.onAnswer(description);
13
+                }
14
+            }
15
+        });
16
+    }
17
+
18
+    protected void candidate(final IceCandidate candidate) {
19
+        Handler.post(new Runnable() {
20
+            @Override
21
+            public void run() {
22
+                if (listener != null) {
23
+                    listener.onCandidate(candidate);
24
+                }
25
+            }
26
+        });
27
+    }
28
+}

+ 31
- 0
src/main/java/org/saltyrtc/client/Session.java View File

@@ -0,0 +1,31 @@
1
+package org.saltyrtc.client;
2
+
3
+import android.util.Log;
4
+import org.saltyrtc.client.exceptions.SessionUnavailableException;
5
+
6
+public class Session {
7
+    protected static final String NAME = "Session";
8
+    protected static String id = null;
9
+
10
+    protected synchronized static boolean equals(String otherId) {
11
+        return id != null && id.equals(otherId);
12
+    }
13
+
14
+    protected synchronized static String get() throws SessionUnavailableException {
15
+        if (id == null) {
16
+            throw new SessionUnavailableException();
17
+        }
18
+        return id;
19
+    }
20
+
21
+    protected synchronized static void set(String id) {
22
+        Log.d(NAME, "New: " + id);
23
+        Session.id = id;
24
+    }
25
+
26
+    // Ex protected
27
+    public synchronized static void reset() {
28
+        id = null;
29
+        Log.d(NAME, "Reset");
30
+    }
31
+}

+ 513
- 0
src/main/java/org/saltyrtc/client/Signaling.java View File

@@ -0,0 +1,513 @@
1
+package org.saltyrtc.client;
2
+
3
+
4
+import android.util.Log;
5
+
6
+import org.saltyrtc.client.exceptions.CryptoException;
7
+import org.saltyrtc.client.exceptions.SessionUnavailableException;
8
+import de.tavendo.autobahn.WebSocketConnection;
9
+import de.tavendo.autobahn.WebSocketException;
10
+import de.tavendo.autobahn.WebSocketHandler;
11
+import de.tavendo.autobahn.WebSocketOptions;
12
+import org.json.JSONException;
13
+import org.json.JSONObject;
14
+import org.webrtc.IceCandidate;
15
+import org.webrtc.SessionDescription;
16
+
17
+import java.nio.ByteBuffer;
18
+import java.util.ArrayList;
19
+
20
+/**
21
+ * The signaling channel used to exchange metadata of the peers.
22
+ * Note: Public methods can be used safely from any thread.
23
+ */
24
+public class Signaling extends EncryptedChannel {
25
+    protected static final String NAME = "Signaling";
26
+    protected static final String DEFAULT_URL = "ws://127.0.0.1:8765/";
27
+    protected static final int CONNECT_MAX_RETRIES = 10;
28
+    protected static final int CONNECT_RETRY_INTERVAL = 10000;
29
+    protected String path;
30
+    protected String url;
31
+    protected String state;
32
+    protected final WebSocketOptions options;
33
+    protected WebSocketConnection ws = null;
34
+    protected int connectTries;
35
+    protected ArrayList<CachedItem> cached;
36
+    protected Events events;
37
+    // Ex protected
38
+    public final StateDispatcher stateDispatcher = new StateDispatcher();
39
+    // Ex protected
40
+    public final SignalingMessageDispatcher messageDispatcher = new SignalingMessageDispatcher();
41
+    protected final ConnectTimer connectTimer = new ConnectTimer();
42
+
43
+    public Signaling() {
44
+        // Set timeout option
45
+        this.options = new WebSocketOptions();
46
+        this.options.setSocketConnectTimeout(CONNECT_RETRY_INTERVAL);
47
+
48
+        // Store own public key for announcement
49
+        this.reset(true);
50
+    }
51
+
52
+    /**
53
+     * Listener for message (answer and candidate) dispatch request events.
54
+     */
55
+    public interface MessageListener {
56
+        void onReset();
57
+        void onSendError();
58
+        void onOffer(SessionDescription description);
59
+        void onCandidate(IceCandidate candidate);
60
+    }
61
+
62
+    protected class CachedItem {
63
+        private final JSONObject message;
64
+        private final boolean encrypt;
65
+
66
+        public CachedItem(JSONObject message, boolean encrypt) {
67
+            this.message = message;
68
+            this.encrypt = encrypt;
69
+        }
70
+    }
71
+
72
+    protected class ConnectTimer implements Runnable {
73
+        @Override
74
+        public void run() {
75
+            connectTries += 1;
76
+            Log.w(NAME, "Connect timeout, retry " +
77
+                    connectTries + "/" + CONNECT_MAX_RETRIES);
78
+            connect(path, url);
79
+        }
80
+    }
81
+
82
+    /**
83
+     * Handles signaling events and dispatches messages.
84
+     * Note: It is vital that messages are always processed with the same amount of post calls
85
+     * to the event loop. This will avoid message reordering while processing.
86
+     */
87
+    protected class Events extends WebSocketHandler {
88
+        private volatile boolean stopped = false;
89
+
90
+        public void stop() {
91
+            this.stopped = true;
92
+        }
93
+
94
+        @Override
95
+        public void onOpen() {
96
+            // Web socket connection is ready for sending and receiving
97
+            Handler.post(new Runnable() {
98
+                @Override
99
+                public void run() {
100
+                    if (!stopped) {
101
+                        setState("open");
102
+                    }
103
+                }
104
+            });
105
+        }
106
+
107
+        @Override
108
+        public void onClose(final int code, String reason) {
109
+            Log.d(NAME, "Connection closed with code: " + code + ", reason: " + reason);
110
+            // Web Socket connection has been closed
111
+            Handler.post(new Runnable() {
112
+                @Override
113
+                public void run() {
114
+                    if (stopped) {
115
+                        return;
116
+                    }
117
+                    // Note: We don't need a timer like in the browser version here
118
+                    if (code == CLOSE_CANNOT_CONNECT) {
119
+                        reconnect(0);
120
+                    } else {
121
+                        setState("closed");
122
+                    }
123
+                }
124
+            });
125
+        }
126
+
127
+        @Override
128
+        public void onTextMessage(final String payload) {
129
+            // A message has been received
130
+            Handler.post(new Runnable() {
131
+                @Override
132
+                public void run() {
133
+                    if (!stopped) {
134
+                        receiveText(payload);
135
+                    }
136
+                }
137
+            });
138
+        }
139
+
140
+        @Override
141
+        public void onRawTextMessage(byte[] payload) {
142
+            if (this.stopped) {
143
+                return;
144
+            }
145
+            Log.w(NAME, "Ignored raw text message");
146
+        }
147
+
148
+        @Override
149
+        public void onBinaryMessage(final byte[] payload) {
150
+            if (this.stopped) {
151
+                return;
152
+            }
153
+
154
+            // Note: Bytes need to be received directly as they might be disposed after return
155
+            final String data;
156
+            try {
157
+                data = decrypt(new KeyStore.Box(ByteBuffer.wrap(payload)));
158
+            } catch (CryptoException e) {
159
+                stateDispatcher.error(e.getState(), e.getError());
160
+                return;
161
+            }
162
+
163
+            // Now that the bytes have been fetched from the buffer, we can safely dispatch the
164
+            // data (if it could be decrypted)
165
+            Handler.post(new Runnable() {
166
+                @Override
167
+                public void run() {
168
+                    if (stopped) {
169
+                        return;
170
+                    }
171
+                    receiveBinary(data);
172
+                }
173
+            });
174
+        }
175
+    }
176
+
177
+    protected void setState(String state) {
178
+        // Ignore repeated state changes
179
+        if (state.equals(this.state)) {
180
+            Log.d(NAME, "Ignoring repeated state: " + state);
181
+            return;
182
+        }
183
+
184
+        // Update state and notify listeners
185
+        this.state = state;
186
+        this.stateDispatcher.state(state);
187
+
188
+        // Open?
189
+        if (state.equals("open")) {
190
+            // Reset connect counter
191
+            this.connectTries = 0;
192
+        }
193
+    }
194
+
195
+    public synchronized void reset() {
196
+        this.reset(false);
197
+    }
198
+
199
+    // Ex protected
200
+    public synchronized void reset(boolean hard) {
201
+        this.setState("unknown");
202
+
203
+        // Close and reset event instance
204
+        if (this.events != null) {
205
+            this.events.stop();
206
+        }
207
+        this.events = new Events();
208
+
209
+        // Close web socket instance
210
+        if (this.ws != null && this.ws.isConnected()) {
211
+            Log.d(NAME, "Disconnecting");
212
+            this.ws.disconnect();
213
+        }
214
+        // Note: This is required because the web socket can't disconnect reliably, thus
215
+        //       a new instance is required.
216
+        this.ws = null;
217
+
218
+        // Hard reset?
219
+        if (!hard) {
220
+            return;
221
+        }
222
+
223
+        // Reset connect counter
224
+        this.connectTries = 0;
225
+
226
+        // Clear cached messages
227
+        this.clear();
228
+    }
229
+
230
+    // Ex protected
231
+    public void clear() {
232
+        this.cached = new ArrayList<>();
233
+    }
234
+
235
+    // Ex protected
236
+    public void connect(String path) {
237
+        this.connect(path, DEFAULT_URL);
238
+    }
239
+
240
+    protected void connect(String path, String url) { // TODO: Use WSS
241
+        // Store path and URL
242
+        this.path = path;
243
+        this.url = url;
244
+
245
+        // Give up?
246
+        if (this.connectTries == CONNECT_MAX_RETRIES) {
247
+            this.connectTries = 0;
248
+            Log.e(NAME, "Connecting failed");
249
+            this.setState("failed");
250
+            return;
251
+        }
252
+
253
+        // Reset and create web socket instance
254
+        this.reset();
255
+        this.ws = new WebSocketConnection();
256
+        Log.d(NAME, "Created");
257
+        this.setState("connecting");
258
+
259
+        // Connect
260
+        Log.d(NAME, "Connecting to path: " + path);
261
+        try {
262
+            this.ws.connect(url + path, this.events, this.options);
263
+        } catch (WebSocketException e) {
264
+            Log.e(NAME, "Connect error: " + e.toString());
265
+            this.stateDispatcher.error("connect", e.toString());
266
+        }
267
+    }
268
+
269
+    public void reconnect() {
270
+        this.reconnect(CONNECT_RETRY_INTERVAL);
271
+    }
272
+
273
+    public void reconnect(int delay) {
274
+        this.restartConnectTimer(delay);
275
+    }
276
+
277
+    public void sendHello() {
278
+        Log.d(NAME, "Sending hello");
279
+
280
+        // Build JSON
281
+        String type = "hello-server";
282
+        JSONObject message = new JSONObject();
283
+        try {
284
+            // Prepare data
285
+            message.put("type", type);
286
+            message.put("key", KeyStore.getPublicKey());
287
+        } catch (JSONException e) {
288
+            Log.e(NAME, "Hello encode error: " + e.toString());
289
+            this.stateDispatcher.error("encode", e.toString());
290
+            return;
291
+        }
292
+
293
+        // Send hello
294
+        this.send(message, false);
295
+    }
296
+
297
+    // Ex protected
298
+    public void sendReset() {
299
+        Log.d(NAME, "Sending reset");
300
+
301
+        // Build JSON
302
+        String type = "reset";
303
+        JSONObject message = new JSONObject();
304
+        try {
305
+            // Prepare data
306
+            message.put("type", type);
307
+        } catch (JSONException e) {
308
+            Log.e(NAME, "Reset encode error: " + e.toString());
309
+            e.printStackTrace();
310
+            this.stateDispatcher.error("encode", e.toString());
311
+            return;
312
+        }
313
+
314
+        // Send reset
315
+        this.send(message, false);
316
+    }
317
+
318
+    protected void receiveReset() {
319
+        Log.d(NAME, "Broadcasting reset");
320
+        this.messageDispatcher.reset();
321
+    }
322
+
323
+    protected void receiveSendError() {
324
+        Log.d(NAME, "Broadcasting send error");
325
+        this.messageDispatcher.sendError();
326
+    }
327
+
328
+    protected void receiveOffer(SessionDescription offer, String session) {
329
+        Log.d(NAME, "Broadcasting offer");
330
+        this.messageDispatcher.offer(offer, session);
331
+    }
332
+
333
+    // Ex protected
334
+    public void sendAnswer(SessionDescription description) {
335
+        Log.d(NAME, "Sending answer");
336
+
337
+        // Build JSON
338
+        String type = "answer";
339
+        JSONObject message = new JSONObject();
340
+        JSONObject payload = new JSONObject();
341
+        try {
342
+            // Prepare payload
343
+            payload.put("type", type);
344
+            payload.put("sdp", description.description);
345
+
346
+            // Prepare data
347
+            message.put("type", type);
348
+            message.put("session", Session.get());
349
+            message.put("data", payload);
350
+        } catch (JSONException e) {
351
+            Log.e(NAME, "Answer encode error: " + e.toString());
352
+            e.printStackTrace();
353
+            this.stateDispatcher.error("encode", e.toString());
354
+            return;
355
+        } catch (SessionUnavailableException e) {
356
+            Log.e(NAME, "Session unavailable error: " + e.toString());
357
+            e.printStackTrace();
358
+            this.stateDispatcher.error("session", e.toString());
359
+            return;
360
+        }
361
+
362
+        // Send answer
363
+        this.send(message);
364
+    }
365
+
366
+    // Ex protected
367
+    public void sendCandidate(IceCandidate candidate) {
368
+        Log.d(NAME, "Sending candidate");
369
+
370
+        // Build JSON
371
+        String type = "candidate";
372
+        JSONObject message = new JSONObject();
373
+        JSONObject payload = new JSONObject();
374
+        try {
375
+            // Prepare payload
376
+            payload.put("type", type);
377
+            payload.put("sdpMLineIndex", candidate.sdpMLineIndex);
378
+            payload.put("sdpMid", candidate.sdpMid);
379
+            payload.put("candidate", candidate.sdp);
380
+
381
+            // Prepare data
382
+            message.put("type", type);
383
+            message.put("session", Session.get());
384
+            message.put("data", payload);
385
+        } catch (JSONException e) {
386
+            Log.e(NAME, "Candidate encode error: " + e.toString());
387
+            e.printStackTrace();
388
+            this.stateDispatcher.error("encode", e.toString());
389
+            return;
390
+        } catch (SessionUnavailableException e) {
391
+            Log.e(NAME, "Session unavailable error: " + e.toString());
392
+            e.printStackTrace();
393
+            this.stateDispatcher.error("session", e.toString());
394
+            return;
395
+        }
396
+
397
+        // Send candidate
398
+        this.send(message);
399
+    }
400
+
401
+    protected void receiveCandidate(IceCandidate candidate) {
402
+        Log.d(NAME, "Broadcasting candidate");
403
+        this.messageDispatcher.candidate(candidate);
404
+    }
405
+
406
+    protected void startConnectTimer(int delay) {
407
+        Handler.postDelayed(this.connectTimer, delay);
408
+    }
409
+
410
+    protected void restartConnectTimer(int delay) {
411
+        this.cancelConnectTimer();
412
+        this.startConnectTimer(delay);
413
+    }
414
+
415
+    protected void cancelConnectTimer() {
416
+        Handler.removeCallbacks(this.connectTimer);
417
+    }
418
+
419
+    public void sendCached() {
420
+        Log.d(NAME, "Sending " + this.cached.size() + " delayed messages");
421
+        for (CachedItem item : this.cached) {
422
+            this.send(item.message, item.encrypt);
423
+        }
424
+        this.cached.clear();
425
+    }
426
+
427
+    protected void send(JSONObject message) {
428
+        this.send(message, true);
429
+    }
430
+
431
+    protected void send(JSONObject message, boolean encrypt) {
432
+        // Delay sending until connected
433
+        if (this.ws != null && this.ws.isConnected()) {
434
+            Log.d(NAME, "Sending message (encrypted: " + encrypt + "): " + message);
435
+            if (encrypt) {
436
+                KeyStore.Box box;
437
+                try {
438
+                    // Encrypt data
439
+                    box = this.encrypt(message.toString());
440
+                } catch (CryptoException e) {
441
+                    this.stateDispatcher.error(e.getState(), e.getError());
442
+                    return;
443
+                }
444
+
445
+                // Send buffer content as byte array
446
+                this.ws.sendBinaryMessage(box.getBuffer().array());
447
+            } else {
448
+                this.ws.sendTextMessage(message.toString());
449
+            }
450
+        } else {
451
+            Log.d(NAME, "Delaying message until WebSocket is open");
452
+            this.cached.add(new CachedItem(message, encrypt));
453
+        }
454
+    }
455
+
456
+    protected void receiveText(String data) {
457
+        try {
458
+            // Decode data
459
+            Log.d(NAME, "Received text message: " + data);
460
+            JSONObject message = new JSONObject(data);
461
+            String type = message.getString("type");
462
+
463
+            // Relay message
464
+            //noinspection IfCanBeSwitch
465
+            if (type.equals("reset")) {
466
+                this.receiveReset();
467
+            } else if (type.equals("send-error")) {
468
+                this.receiveSendError();
469
+            } else {
470
+                Log.w(NAME, "Ignored text message: " + data);
471
+            }
472
+        } catch (JSONException e) {
473
+            Log.w(NAME, "Ignored invalid text message: " + data);
474
+        }
475
+    }
476
+
477
+    protected void receiveBinary(String data) {
478
+        try {
479
+            // Decode data
480
+            Log.d(NAME, "Received encrypted message: " + data);
481
+            JSONObject message = new JSONObject(data);
482
+            String type = message.getString("type");
483
+
484
+            // Check session
485
+            String session = message.getString("session");
486
+            if (!type.equals("offer") && !equals(session)) {
487
+                Log.w(NAME, "Ignored message from another session: " + session);
488
+                return;
489
+            }
490
+
491
+            // Relay message
492
+            //noinspection IfCanBeSwitch
493
+            if (type.equals("offer")) {
494
+                JSONObject payload = message.getJSONObject("data");
495
+                this.receiveOffer(new SessionDescription(
496
+                        SessionDescription.Type.fromCanonicalForm(type),
497
+                        payload.getString("sdp")
498
+                ), session);
499
+            } else if (type.equals("candidate")) {
500
+                JSONObject payload = message.getJSONObject("data");
501
+                this.receiveCandidate(new IceCandidate(
502
+                        payload.getString("sdpMid"),
503
+                        payload.getInt("sdpMLineIndex"),
504
+                        payload.getString("candidate")
505
+                ));
506
+            } else {
507
+                Log.w(NAME, "Ignored encrypted message: " + data);
508
+            }
509
+        } catch (JSONException e) {
510
+            Log.w(NAME, "Ignored invalid encrypted message: " + data);
511
+        }
512
+    }
513
+}

+ 56
- 0
src/main/java/org/saltyrtc/client/SignalingMessageDispatcher.java View File

@@ -0,0 +1,56 @@
1
+package org.saltyrtc.client;
2
+
3
+import org.webrtc.IceCandidate;
4
+import org.webrtc.SessionDescription;
5
+
6
+public class SignalingMessageDispatcher extends MessageDispatcher<Signaling.MessageListener> {
7
+    protected void reset() {
8
+        Handler.post(new Runnable() {
9
+            @Override
10
+            public void run() {
11
+                if (listener != null) {
12
+                    listener.onReset();
13
+                }
14
+            }
15
+        });
16
+    }
17
+
18
+    protected void sendError() {
19
+        Handler.post(new Runnable() {
20
+            @Override
21
+            public void run() {
22
+                if (listener != null) {
23
+                    listener.onSendError();
24
+                }
25
+            }
26
+        });
27
+    }
28
+
29
+    protected void offer(
30
+            final SessionDescription description,
31
+            final String offer
32
+    ) {
33
+        Handler.post(new Runnable() {
34
+            @Override
35
+            public void run() {
36
+                if (listener != null) {
37
+                    // Note: This is a workaround to prevent message reordering. Do not move the next
38
+                    //       line of code somewhere else unless you know EXACTLY what you are doing.
39
+                    Session.set(offer);
40
+                    listener.onOffer(description);
41
+                }
42
+            }
43
+        });
44
+    }
45
+
46
+    protected void candidate(final IceCandidate candidate) {
47
+        Handler.post(new Runnable() {
48
+            @Override
49
+            public void run() {
50
+                if (listener != null) {
51
+                    listener.onCandidate(candidate);
52
+                }
53
+            }
54
+        });
55
+    }
56
+}

+ 65
- 0
src/main/java/org/saltyrtc/client/State.java View File

@@ -0,0 +1,65 @@
1
+package org.saltyrtc.client;
2
+
3
+import java.util.HashSet;
4
+
5
+/**
6
+ * States have a name, type and value. This class will also notify state listeners.
7
+ * Note: Public methods can be used safely from any thread.
8
+ */
9
+public class State {
10
+    public final String name;
11
+    protected int type;
12
+    protected String value;
13
+    protected final HashSet<StateListener> listeners = new HashSet<>();
14
+
15
+    public State(String name) {
16
+        this.name = name;
17
+    }
18
+
19
+    // Ex protected
20
+    public void addListener(StateListener listener) {
21
+        this.listeners.add(listener);
22
+        // Send initial values (if any)
23
+        this.notifyListener(listener);
24
+    }
25
+
26
+    // Ex protected
27
+    public void removeListener(StateListener listener) {
28
+        this.listeners.remove(listener);
29
+    }
30
+
31
+    // Ex protected
32
+    public void notifyListener(final StateListener listener) {
33
+       this.notify(listener);
34
+    }
35
+
36
+    // Ex protected
37
+    public void notifyListeners() {
38
+        this.notify(null);
39
+    }
40
+
41
+    protected void notify(final StateListener listener_) {
42
+        if (this.value == null) {
43
+            return;
44
+        }
45
+
46
+        final int type = this.type;
47
+        final String value = this.value;
48
+
49
+        // Broadcast
50
+        Handler.post(new Runnable() {
51
+            @Override
52
+            public void run() {
53
+                if (listener_ == null) {
54
+                    // Notify all listeners
55
+                    for (StateListener listener : listeners) {
56
+                        listener.onState(type, value);
57
+                    }
58
+                } else {
59
+                    // Notify a single listener
60
+                    listener_.onState(type, value);
61
+                }
62
+            }
63
+        });
64
+    }
65
+}

+ 39
- 0
src/main/java/org/saltyrtc/client/StateDispatcher.java View File

@@ -0,0 +1,39 @@
1
+package org.saltyrtc.client;
2
+
3
+import java.util.HashSet;
4
+
5
+public class StateDispatcher {
6
+    protected final HashSet<InternalStateListener> listeners = new HashSet<>();
7
+
8
+    protected void state(final String state) {
9
+        Handler.post(new Runnable() {
10
+            @Override
11
+            public void run() {
12
+                for (InternalStateListener listener : listeners) {
13
+                    listener.handleState(state);
14
+                }
15
+            }
16
+        });
17
+    }
18
+
19
+    protected void error(final String state, final String error) {
20
+        Handler.post(new Runnable() {
21
+            @Override
22
+            public void run() {
23
+                for (InternalStateListener listener : listeners) {
24
+                    listener.handleError(state, error);
25
+                }
26
+            }
27
+        });
28
+    }
29
+
30
+    // Ex protected
31
+    public void addListener(InternalStateListener listener) {
32
+        this.listeners.add(listener);
33
+    }
34
+
35
+    // Ex protected
36
+    public void removeListener(InternalStateListener listener) {
37
+        this.listeners.remove(listener);
38
+    }
39
+}

+ 5
- 0
src/main/java/org/saltyrtc/client/StateHandler.java View File

@@ -0,0 +1,5 @@
1
+package org.saltyrtc.client;
2
+
3
+public interface StateHandler {
4
+    void handle();
5
+}

+ 8
- 0
src/main/java/org/saltyrtc/client/StateListener.java View File

@@ -0,0 +1,8 @@
1
+package org.saltyrtc.client;
2
+
3
+/**
4
+ * Listener for state change events of an abstract state instance.
5
+ */
6
+public interface StateListener {
7
+    void onState(int type, String value);
8
+}

+ 10
- 0
src/main/java/org/saltyrtc/client/StateType.java View File

@@ -0,0 +1,10 @@
1
+package org.saltyrtc.client;
2
+
3
+/**
4
+ * Defines valid state types and their weight.
5
+ */
6
+public class StateType {
7
+    public static final int DANGER = 100;
8
+    public static final int WARNING = 10;
9
+    public static final int SUCCESS = 1;
10
+}

+ 101
- 0
src/main/java/org/saltyrtc/client/States.java View File

@@ -0,0 +1,101 @@
1
+package org.saltyrtc.client;
2
+
3
+import java.util.HashMap;
4
+
5
+/**
6
+ * Contains state instances for signaling, peer connection, data channel and the
7
+ * summarising client state.
8
+ * Note: Public methods can be used safely from any thread.
9
+ */
10
+public class States {
11
+    protected final ClientState client;
12
+    protected final HashMap<String, InternalState> states = new HashMap<>();
13
+
14
+    public States() {
15
+        // Setup internal states
16
+        InternalState signaling = new InternalState("signaling");
17
+        InternalState pc = new InternalState("pc");
18
+        InternalState dc = new InternalState("dc");
19
+        this.states.put("signaling", signaling);
20
+        this.states.put("pc", pc);
21
+        this.states.put("dc", dc);
22
+
23
+        // Signaling state rules
24
+        this.setupRules(signaling, StateType.DANGER,
25
+                new String[]{"unknown", "failed"});
26
+        this.setupRules(signaling, StateType.WARNING,
27
+                new String[]{"connecting", "closing", "closed"});
28
+        this.setupRules(signaling, StateType.SUCCESS,
29
+                new String[]{"open"});
30
+
31
+        // Peer Connection state rules
32
+        this.setupRules(pc, StateType.DANGER,
33
+                new String[]{"unknown", "init", "new", "failed", "closed"});
34
+        this.setupRules(pc, StateType.WARNING,
35
+                new String[]{"checking", "disconnected"});
36
+        this.setupRules(pc, StateType.SUCCESS,
37
+                new String[]{"connected", "completed"});
38
+
39
+        // Data Channel state rules
40
+        this.setupRules(dc, StateType.DANGER,
41
+                new String[]{"unknown", "init", "closed"});
42
+        this.setupRules(dc, StateType.WARNING,
43
+                new String[]{"connecting", "closing"});
44
+        this.setupRules(dc, StateType.SUCCESS,
45
+                new String[]{"open"});
46
+
47
+        // Setup client state
48
+        this.client = new ClientState("client");
49
+        this.client.update(this.states);
50
+    }
51
+
52
+    public ClientState getClient() {
53
+        return this.client;
54
+    }
55
+
56
+    public InternalState getSignaling() {
57
+        return this.states.get("signaling");
58
+    }
59
+
60
+    public InternalState getPeerConnection() {
61
+        return this.states.get("pc");
62
+    }
63
+
64
+    public InternalState getDataChannel() {
65
+        return this.states.get("dc");
66
+    }
67
+
68
+    protected void setupRules(InternalState state, int type, String[] values) {
69
+        HashMap<String, Integer> rules = new HashMap<>();
70
+        for (String value : values) {
71
+            rules.put(value, type);
72
+        }
73
+        state.addRules(rules);
74
+    }
75
+
76
+    // Ex protected
77
+    public void reset() {
78
+        this.getSignaling().reset();
79
+        this.getPeerConnection().reset();
80
+        this.getDataChannel().reset();
81
+        this.client.update(this.states);
82
+    }
83
+
84
+    // Ex protected
85
+    public void updateSignaling(String value) {
86
+        this.getSignaling().update(value);
87
+        this.client.update(this.states);
88
+    }
89
+
90
+    // Ex protected
91
+    public void updatePeerConnection(String value) {
92
+        this.getPeerConnection().update(value);
93