├── .gitignore ├── CHANGES.md ├── LICENSE ├── README.md ├── WebTestBot └── index.html ├── build.gradle ├── ci-test.sh ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── respoke-sdk-android.iml ├── respokeSDK ├── build.gradle ├── gradle.properties ├── proguard-rules.pro ├── respoke-sdk.iml └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── digium │ │ └── respokesdk │ │ ├── Respoke.java │ │ ├── RespokeCall.java │ │ ├── RespokeClient.java │ │ ├── RespokeConnection.java │ │ ├── RespokeConversationReadStatus.java │ │ ├── RespokeDirectConnection.java │ │ ├── RespokeEndpoint.java │ │ ├── RespokeGroup.java │ │ ├── RespokeGroupMessage.java │ │ ├── RespokeSignalingChannel.java │ │ ├── RespokeWorkerThread.java │ │ └── RestAPI │ │ ├── APIDoOpen.java │ │ ├── APIGetToken.java │ │ └── APITransaction.java │ └── res │ ├── drawable-hdpi │ └── ic_launcher.png │ ├── drawable-mdpi │ └── ic_launcher.png │ ├── drawable-xhdpi │ └── ic_launcher.png │ ├── drawable-xxhdpi │ └── ic_launcher.png │ └── values │ └── strings.xml ├── respokeSDKTest ├── android-wait-for-emulator ├── build.gradle ├── proguard-rules.pro ├── respokeSDKTest.iml ├── runtests.sh └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── digium │ │ └── respokesdktest │ │ ├── RespokeActivityTestCase.java │ │ ├── RespokeTestCase.java │ │ ├── functional │ │ ├── CallingTests.java │ │ ├── ConnectionTests.java │ │ ├── DirectConnectionTests.java │ │ ├── GroupTests.java │ │ ├── HangupTests.java │ │ ├── MessagingTests.java │ │ └── PresenceTests.java │ │ └── unit │ │ ├── RespokeCallTests.java │ │ ├── RespokeClientTests.java │ │ ├── RespokeEndpointTests.java │ │ ├── RespokeGroupTests.java │ │ └── RespokeTests.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── digium │ │ └── respokesdktest │ │ └── MainActivity.java │ └── res │ ├── layout │ └── activity_main.xml │ ├── menu │ └── menu_main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | respokeSDK/build 2 | respokeSDK/respokeSDK.iml 3 | .DS_Store 4 | .gradle/ 5 | respokeSDKTest/build 6 | respokeSDK/gradle.properties 7 | build 8 | .idea 9 | local.properties 10 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | Respoke Android SDK Change Log 2 | ============================== 3 | 4 | v1.5.0 5 | ------ 6 | * Moved RespokeClient.getGroupHistories() to a new route from a deprecated one. 7 | * Adds RespokeClient.getConversations() and .setConversationsRead() for server-side message history support. 8 | 9 | v1.4.1 10 | ------ 11 | * Updated AndroidAsync dependency to 2.1.7 12 | 13 | v1.4.0 14 | ------ 15 | 16 | * Added support for retrieving group message history, and marking a message to be persisted 17 | to the history. See commit 9a3f4e6 for details. 18 | 19 | v1.3.3 20 | ------ 21 | 22 | * Removed unnecessary inclusion of Google Play Services. The SDK doesn't actually use it. Client 23 | apps should refer to the demo app for examples of how to use GCM (which relies on Google Play 24 | Services). 25 | 26 | v1.3.2 27 | ------ 28 | 29 | * Fixed a crash that can occur intermittently during call setup 30 | 31 | v1.3.1 32 | ------ 33 | 34 | * Fixed a crash that can occur if a hangup signal is received during key moments of call set up 35 | 36 | v1.3.0 37 | ------ 38 | 39 | * Improved ice candidates signaling, resulting in faster call setup and inter-op support for 40 | clients that do not support trickle ice, such as chan_respoke. 41 | 42 | v1.2.0 43 | ------ 44 | 45 | * Added Respoke-SDK header to websocket & http[s] requests, which includes the sdk version 46 | and the Android OS version making the request. 47 | 48 | v1.1.2 49 | ------ 50 | 51 | * Fixed a packaging problem that was preventing the SDK from being installed from Maven 52 | 53 | v1.1.1 54 | ------ 55 | 56 | * Added missing in-line documentation to the public interfaces used by client applications. 57 | 58 | * Rearranged internally used methods for clarity 59 | 60 | v1.1.0 61 | ------ 62 | 63 | * This version of the Android SDK now uses a customized version of the AndroidAsync library 64 | dependency as a submodule rather than has a Gradle dependency. If you are working with the SDK 65 | code directly, note that you now need to use the `--recursive` flag when cloning the repository as 66 | described in the README. 67 | 68 | * ***breaking*** Changed the `RespokeEndpoint.sendMessage()` method to now have 4 params, 69 | where the 3rd param is now `Boolean ccSelf`. This param indicates to the Respoke service whether the 70 | message should be sent to other connections of the sender's endpointId or not. 71 | 72 | * ***breaking*** Changed the `onMessage()` method of the `RespokeClient.Listener` interface to now require 73 | a 5th param, `Boolean didSend`. This param indicates to the listener whether the message received 74 | was *from* the localEndpoint *sent to* the specified endpoint (didSend=false), or was *from* the 75 | specified endpoint *sent to* the localEndpoint (didSend=true). This change was required to support 76 | messages that could be coming from another connection of the same endpointID that the current client 77 | is connected as (ccSelf). 78 | 79 | * ***breaking*** Changed the `onMessage()` method of the `RespokeEndpoint.Listener` interface to now 80 | require a 4th param, `Boolean didSend`. This param has the same meaning as the one added to 81 | `RespokeClient.Listener#onMessage`. 82 | 83 | * Added conference call support. Call the new `RespokeClient.joinConference()` method to create a 84 | RespokeCall instance that will join the multiparty audio conference specified by the conferenceID 85 | parameter. 86 | 87 | * Declining an incoming call by calling the `RespokeCall.hangup()` method in the Android SDK will now also decline that call on any other clients that have logged in to Respoke using the same endpoint ID (Android or otherwise) 88 | 89 | * Fixed a bug that could prevent the SDK from registering for push notifications. 90 | 91 | * Fixed several intermittent crashes related to hanging up a call. 92 | 93 | * Fixed a crash that can occur when an application using the Respoke SDK is pushed into or restored 94 | from the background. 95 | 96 | v1.0.4 97 | ------ 98 | 99 | * Added a new "push" parameter to `RespokeGroup.sendMessage()` and `RespokeEndpoint.sendMessage()` 100 | that allows you to indicate if a specific message should be considered (true) or ignored (false) by 101 | the push notification service. Use this to explicitly ignore certain messages (like meta data) that 102 | should never be sent to mobile devices as a push notification. 103 | 104 | * Added a new `Respoke.unregisterPushServices()` method that allows you to explicitly unregister a 105 | device from receiving push notifications in the future. 106 | 107 | * Updated the AndroidAsync, Google Play Services, and App Support dependency libraries to the latest 108 | available versions 109 | 110 | * Improved the instructions in the README on how to run the SDK test cases 111 | 112 | * Miscellaneous test case fixes 113 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015, Digium, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Respoke SDK for Android 2 | ======================= 3 | 4 | The Respoke SDK for Android makes it easy to add live voice, video, text, and data features to your mobile app. For information on how to use the SDK, take a look at our developer documentation and sample apps here: 5 | 6 | [https://docs.respoke.io/](https://docs.respoke.io/) 7 | 8 | Installing the SDK 9 | ============= 10 | 11 | The Respoke Android SDK is available to install via the Maven Central repository. 12 | You can [download the latest JAR directly](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.digium.respoke%22%20AND%20a%3A%22respoke-sdk%22), or install via Maven: 13 | 14 | ``` 15 | 16 | com.digium.respoke 17 | respoke-sdk 18 | (insert latest version) 19 | 20 | ``` 21 | 22 | Gradle: 23 | 24 | ``` 25 | dependencies { 26 | compile 'com.digium.respoke:respoke-sdk:1.+' 27 | } 28 | ``` 29 | Building the SDK from source 30 | ============================ 31 | 32 | You must use JDK 1.8. 33 | 34 | From the root directory: 35 | ``` 36 | ./gradlew build 37 | ``` 38 | 39 | NOTE: The javadoc generation task will complain with errors, but it will still generate the javadoc jar file. 40 | 41 | Contributing 42 | ============ 43 | 44 | We welcome pull requests to improve the SDK for everyone. When submitting changes, please make sure you have run the SDK test cases before submitting and added/modified any tests that are affected by your improvements. 45 | 46 | Running the SDK test cases 47 | ========================== 48 | 49 | The SDK test cases require UI elements and so are contained in a test application named "RespokeSDKTest" in this repo. To run the test cases, do the following: 50 | 51 | 52 | 1) Create a Respoke developer account and define a Respoke application in the [developer console](https://portal.respoke.io/#/signup). Make a note of the **application ID** for the Respoke Application you created. 53 | 54 | 2) Clone this repo onto your development machine. 55 | 56 | 3) Open Android Studio, choose "Import Project" and select the root directory of the repo. 57 | 58 | 4) Open RespokeTestCase.java and change the value of the static string `TEST_APP_ID` with the Respoke application ID you received in step 1. 59 | 60 | 5) Start the web TestBot in either Chrome or Firefox as described in the section "Starting the Web TestBot" below, passing your Respoke application ID as a parameter on the URL. 61 | 62 | 6) In Android Studio, open the 'Run' menu and select "Edit Configurations". In the upper left corner of the dialog that appears, press the '+' button to create a new run configuration and select 'Android Tests'. 63 | 64 | 7) Name the new test configuration "All Tests", select the "respokeSDKTest" module from the drop-down box, and choose the settings in the "Target Device" box that make the most sense for your test set up. save the new configuration. You only have to do this once, in the future you can just select the existing configuration to run the tests. 65 | 66 | 8) Open the "Run" menu again, and choose "debug…". Select the "All Tests" configuration to run the test cases, displaying the results inside of Android Studio. You will also see debug messages and video displayed in the web browser running the TestBot. 67 | 68 | Starting the Web TestBot 69 | ======================== 70 | 71 | The functional test cases that use RespokeCall require a specific Web application based on Respoke.js that is set up to automatically respond to certain actions that the SDK test cases perform. Because the web application will use audio and video, it requires special user permissions from browsers that support WebRTC and typically requires user interaction. Therefore it must run from either the context of a web server, or by loading the html file from the file system with specific command line parameters for Chrome. 72 | 73 | Additionally, the Android Studio test project has been set up to expect that the web application will connect to Respoke with a specific endpoint ID in the following format: 74 | 75 | testbot-username 76 | 77 | This username is the user that you are logged into your development computer with when you run the tests. This is done to avoid conflicts that can occur when multiple developers are running multiple instances of the test web application simultaneously. 78 | 79 | To set up your system to perform these tests, do one of the following: 80 | 81 | #### A) Load the html from a file with Chrome. 82 | 83 | 84 | 1) You can use command line parameters to load the test bot with Chrome tell it to use a fake audio and video source during testing. On Mac OS, the command would look like this: 85 | 86 | $ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \ 87 | --use-fake-ui-for-media-stream \ 88 | --use-fake-device-for-media-stream \ 89 | --allow-file-access-from-files \ 90 | ./WebTestBot/index.html & 91 | 92 | 2) Once the file has loaded, append your local username and Respoke application ID to the URL to match what Android Studio will search for as the tests run: 93 | 94 | file:///respoke-android-sdk/WebTestBot/index.html#?un=mymacusername&app_id=my-respoke-app-id 95 | 96 | 3) Run the SDK test cases 97 | 98 | 99 | 100 | #### B) Run with a local web server. 101 | 102 | 103 | 1) Install http-server 104 | 105 | $ sudo npm i -g http-server 106 | 107 | 2) Start http-server from the testbot directory: 108 | 109 | $ cd WebTestBot/ 110 | $ http-server 111 | 112 | 3) Start Chrome using command line parameters to use fake audio/video and auto accept media permissions so that no human interaction is required: 113 | 114 | $ /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --use-fake-ui-for-media-stream --use-fake-device-for-media-stream 115 | 116 | This can alternately be done with Firefox by navigating to "about:config" and then setting the "media.navigator.permission.disabled" option to TRUE 117 | 118 | 4) Open the TestBot in a Chrome tab by loading http://localhost:8080/#?un=mymacusername&app_id=my-respoke-app-id 119 | 120 | 5) Run the SDK test cases 121 | 122 | 123 | 124 | License 125 | ======= 126 | 127 | The Respoke SDK and demo applications are licensed under the MIT license. Please see the [LICENSE](LICENSE) file for details. 128 | -------------------------------------------------------------------------------- /WebTestBot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 33 | 34 | 177 | 178 | 179 | 180 |
181 | 182 |

183 | Connected as {{username}}. Waiting for test users. 184 |

185 | 186 |
187 | Connecting the SDK autotest bot, please wait... 188 |
189 | 190 |
191 |
192 | 193 | 198 |
199 | 200 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.2.2' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ci-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Test runner for the CI server 5 | # 6 | 7 | trap "on_exit" EXIT 8 | 9 | # Ensure cleanup runs on exit 10 | function on_exit() 11 | { 12 | if test -f chrome.pid; then 13 | kill $(cat chrome.pid) 14 | rm -f chrome.pid 15 | fi 16 | } 17 | 18 | set -ex 19 | 20 | # Kill lingering instances of Google Chrome 21 | killall -9 "Google Chrome" || true # ignore errors 22 | while test $(ps aux | grep ^bamboo | grep -i [c]hrome | wc -l) -ne 0; do 23 | sleep 1 24 | done 25 | 26 | # List the connected Android devices for debugging purposes 27 | adb devices 28 | 29 | # Launch Google Chrome 30 | "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \ 31 | --use-fake-ui-for-media-stream \ 32 | --use-fake-device-for-media-stream \ 33 | --allow-file-access-from-files \ 34 | ./WebTestBot/index.html & 35 | echo $! > chrome.pid 36 | 37 | # Run the tests 38 | ./gradlew -p respokeSDKTest --stacktrace --info clean connectedAndroidTest 39 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Settings specified in this file will override any Gradle settings 5 | # configured through the IDE. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/respoke/respoke-sdk-android/34a15f0558d29b1f1bc8481bbc5c505e855e05ef/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Dec 01 14:46:15 CST 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.14.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 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 "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 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 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 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 Windowz 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 91 | -------------------------------------------------------------------------------- /respoke-sdk-android.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /respokeSDK/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.2.2' 9 | } 10 | } 11 | 12 | apply plugin: 'com.android.library' 13 | 14 | repositories { 15 | mavenCentral() 16 | jcenter() 17 | } 18 | 19 | android { 20 | compileSdkVersion 23 21 | buildToolsVersion '23.0.3' 22 | 23 | configurations { 24 | javadocDeps 25 | } 26 | 27 | defaultConfig { 28 | minSdkVersion 15 29 | targetSdkVersion 23 30 | } 31 | 32 | buildTypes { 33 | release { 34 | minifyEnabled false 35 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 36 | } 37 | } 38 | } 39 | 40 | dependencies { 41 | compile fileTree(dir: 'libs', include: ['*.jar']) 42 | compile 'com.android.support:appcompat-v7:23.2.0' 43 | compile 'io.pristine:libjingle:9340@aar' 44 | compile 'com.digium.respoke:AndroidAsync:2.1.7' 45 | } 46 | 47 | apply plugin: 'maven' 48 | apply plugin: 'signing' 49 | 50 | task androidJavadocs(type: Javadoc) { 51 | options.addBooleanOption('Xdoclint:none', true) 52 | source = android.sourceSets.main.java.source 53 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 54 | options.linkSource true 55 | destinationDir = file("../javadoc/") 56 | failOnError false 57 | } 58 | 59 | task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { 60 | classifier = 'javadoc' 61 | from androidJavadocs.destinationDir 62 | } 63 | 64 | task androidSourcesJar(type: Jar) { 65 | classifier = 'sources' 66 | from android.sourceSets.main.java.source 67 | } 68 | 69 | artifacts { 70 | archives androidSourcesJar 71 | archives androidJavadocsJar 72 | } 73 | 74 | signing { 75 | required { gradle.taskGraph.hasTask("uploadArchives") } 76 | sign configurations.archives 77 | } 78 | 79 | 80 | group = "com.digium.respoke" 81 | version = "1.5.0" 82 | 83 | uploadArchives { 84 | repositories { 85 | mavenDeployer { 86 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } 87 | 88 | repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { 89 | authentication(userName: ossrhUsername, password: ossrhPassword) 90 | } 91 | 92 | snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") { 93 | authentication(userName: ossrhUsername, password: ossrhPassword) 94 | } 95 | 96 | pom.project { 97 | name 'Respoke Android SDK' 98 | artifactId 'respoke-sdk' 99 | packaging 'jar' 100 | description 'Add live voice, video, text and data features to your mobile app' 101 | url 'https://www.respoke.io/' 102 | 103 | scm { 104 | connection 'scm:git@github.com:respoke/respoke-sdk-android.git' 105 | developerConnection 'scm:git@github.com:respoke/respoke-sdk-android.git' 106 | url 'https://github.com/respoke/respoke-sdk-android' 107 | } 108 | 109 | licenses { 110 | license { 111 | name 'MIT License' 112 | url 'https://github.com/respoke/respoke-sdk-android/blob/master/LICENSE' 113 | } 114 | } 115 | 116 | developers { 117 | developer { 118 | id 'jpadams' 119 | name 'Respoke.io' 120 | email 'info@respoke.io' 121 | } 122 | } 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /respokeSDK/gradle.properties: -------------------------------------------------------------------------------- 1 | signing.keyId=keyID 2 | signing.password=keyPassword 3 | signing.secretKeyRingFile=~/.gnupg/secring.gpg 4 | 5 | ossrhUsername=username 6 | ossrhPassword=password -------------------------------------------------------------------------------- /respokeSDK/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Applications/Android Studio.app/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | -keep class * extends java.util.ListResourceBundle { 20 | protected Object[][] getContents(); 21 | } 22 | 23 | -keep public class com.google.android.gms.common.internal.safeparcel.SafeParcelable { 24 | public static final *** NULL; 25 | } 26 | 27 | -keepnames @com.google.android.gms.common.annotation.KeepName class * 28 | -keepclassmembernames class * { 29 | @com.google.android.gms.common.annotation.KeepName *; 30 | } 31 | 32 | -keepnames class * implements android.os.Parcelable { 33 | public static final ** CREATOR; 34 | } -------------------------------------------------------------------------------- /respokeSDK/respoke-sdk.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /respokeSDK/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /respokeSDK/src/main/java/com/digium/respokesdk/Respoke.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015, Digium, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under The MIT License found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | * For all details and documentation: https://www.respoke.io 9 | */ 10 | 11 | package com.digium.respokesdk; 12 | 13 | import android.content.Context; 14 | import android.os.Handler; 15 | import android.os.Looper; 16 | 17 | import org.webrtc.PeerConnectionFactory; 18 | import org.webrtc.VideoRendererGui; 19 | 20 | import java.util.ArrayList; 21 | 22 | /** 23 | * A global static class which provides access to the Respoke functionality. 24 | */ 25 | public class Respoke { 26 | 27 | public final static int GUID_STRING_LENGTH = 36; // The length of GUID strings 28 | 29 | private static Respoke _instance; 30 | private static boolean factoryStaticInitialized; 31 | private String pushToken; 32 | private ArrayList instances; 33 | private Context context; 34 | 35 | 36 | /** 37 | * A listener interface to allow the receiver to be notified of the success or failure of an asynchronous operation 38 | */ 39 | public interface TaskCompletionListener { 40 | 41 | 42 | /** 43 | * Receive a notification that the asynchronous operation completed successfully 44 | */ 45 | void onSuccess(); 46 | 47 | 48 | /** 49 | * Receive a notification that the asynchronous operation failed 50 | * 51 | * @param errorMessage A human-readable description of the error that was encountered 52 | */ 53 | void onError(String errorMessage); 54 | 55 | } 56 | 57 | 58 | /** 59 | * A helper function to post success to a TaskCompletionListener on the UI thread 60 | * 61 | * @param completionListener The TaskCompletionListener to notify 62 | */ 63 | public static void postTaskSuccess(final TaskCompletionListener completionListener) { 64 | new Handler(Looper.getMainLooper()).post(new Runnable() { 65 | @Override 66 | public void run() { 67 | if (null != completionListener) { 68 | completionListener.onSuccess(); 69 | } 70 | } 71 | }); 72 | } 73 | 74 | 75 | /** 76 | * A helper function to post an error message to a TaskCompletionListener on the UI thread 77 | * 78 | * @param completionListener The TaskCompletionListener to notify 79 | * @param errorMessage The error message to post 80 | */ 81 | public static void postTaskError(final TaskCompletionListener completionListener, final String errorMessage) { 82 | new Handler(Looper.getMainLooper()).post(new Runnable() { 83 | @Override 84 | public void run() { 85 | if (null != completionListener) { 86 | completionListener.onError(errorMessage); 87 | } 88 | } 89 | }); 90 | } 91 | 92 | 93 | /** 94 | * The private constructor for the Respoke singleton 95 | */ 96 | private Respoke() 97 | { 98 | instances = new ArrayList(); 99 | } 100 | 101 | 102 | /** 103 | * Retrieve the globally shared instance of the Respoke SDK 104 | * 105 | * @return Respoke SDK instance 106 | */ 107 | public static Respoke sharedInstance() 108 | { 109 | if (_instance == null) 110 | { 111 | _instance = new Respoke(); 112 | } 113 | 114 | return _instance; 115 | } 116 | 117 | 118 | /** 119 | * This is one of two possible entry points for interacting with the library. This method creates a new Client object 120 | * which represents your app's connection to the cloud infrastructure. This method does NOT automatically call the 121 | * client.connect() method after the client is created, so your app will need to call it when it is ready to 122 | * connect. 123 | * @param appContext The Android application Context 124 | * @return A Respoke Client instance 125 | */ 126 | public RespokeClient createClient(Context appContext) 127 | { 128 | context = appContext; 129 | 130 | RespokeClient newClient = new RespokeClient(); 131 | instances.add(newClient); 132 | 133 | return newClient; 134 | } 135 | 136 | 137 | /** 138 | * Unregister a client that is no longer active so that it's resources can be 139 | * deallocated 140 | * 141 | * @param client The client to unregister 142 | */ 143 | public void unregisterClient(RespokeClient client) { 144 | instances.remove(client); 145 | } 146 | 147 | 148 | /** 149 | * Create a globally unique identifier for naming instances 150 | * 151 | * @return New globally unique identifier 152 | */ 153 | public static String makeGUID() { 154 | String uuid = ""; 155 | String chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; 156 | int rnd = 0; 157 | int r; 158 | 159 | for (int i = 0; i < GUID_STRING_LENGTH; i += 1) { 160 | if (i == 8 || i == 13 || i == 18 || i == 23) { 161 | uuid = uuid + "-"; 162 | } else if (i == 14) { 163 | uuid = uuid + "4"; 164 | } else { 165 | if (rnd <= 0x02) { 166 | rnd = (int) (0x2000000 + Math.round(java.lang.Math.random() * 0x1000000)); 167 | } 168 | r = rnd & 0xf; 169 | rnd = rnd >> 4; 170 | 171 | if (i == 19) { 172 | uuid = uuid + chars.charAt((r & 0x3) | 0x8); 173 | } else { 174 | uuid = uuid + chars.charAt(r); 175 | } 176 | } 177 | } 178 | return uuid; 179 | } 180 | 181 | 182 | /** 183 | * Notify the Respoke SDK that this device should register itself for push notifications 184 | * 185 | * @param token The token that identifies the device to GCMS. 186 | */ 187 | public void registerPushToken(String token) { 188 | pushToken = token; 189 | 190 | if (instances.size() > 0) { 191 | registerPushServices(); 192 | } 193 | } 194 | 195 | 196 | /** 197 | * Unregister this device from the Respoke push notification service and stop any future notifications until it is re-registered 198 | * 199 | * @param completionListener A listener to be notified of the success of the asynchronous unregistration operation 200 | */ 201 | public void unregisterPushServices(TaskCompletionListener completionListener) { 202 | RespokeClient activeInstance = null; 203 | 204 | // If there are already client instances running, check if any of them have already connected 205 | for (RespokeClient eachInstance : instances) { 206 | if (eachInstance.isConnected()) { 207 | // The push service only supports one endpoint per device, so the token only needs to be registered for the first active client (if there is more than one) 208 | activeInstance = eachInstance; 209 | break; 210 | } 211 | } 212 | 213 | if (null != activeInstance) { 214 | activeInstance.unregisterFromPushServices(completionListener); 215 | } else { 216 | postTaskError(completionListener, "There is no active client to unregister"); 217 | } 218 | } 219 | 220 | 221 | /** 222 | * Notify the shared SDK instance that the specified client has connected. This is for internal use only, and should never be called by your client application. 223 | * 224 | * @param client The client that just connected 225 | */ 226 | public void clientConnected(RespokeClient client) { 227 | if (null != pushToken) { 228 | registerPushServices(); 229 | } 230 | 231 | if (!factoryStaticInitialized) { 232 | // Perform a one-time WebRTC global initialization 233 | PeerConnectionFactory.initializeAndroidGlobals(context, true, true, true, VideoRendererGui.getEGLContext()); 234 | factoryStaticInitialized = true; 235 | } 236 | } 237 | 238 | 239 | //** Private methods 240 | 241 | 242 | /** 243 | * Attempt to register push services for this device 244 | */ 245 | private void registerPushServices() { 246 | RespokeClient activeInstance = null; 247 | 248 | // If there are already client instances running, check if any of them have already connected 249 | for (RespokeClient eachInstance : instances) { 250 | if (eachInstance.isConnected()) { 251 | // The push service only supports one endpoint per device, so the token only needs to be registered for the first active client (if there is more than one) 252 | activeInstance = eachInstance; 253 | } 254 | } 255 | 256 | if (null != activeInstance) { 257 | // Notify the Respoke servers that this device is eligible to receive notifications directed at this endpointID 258 | activeInstance.registerPushServicesWithToken(pushToken); 259 | } 260 | } 261 | 262 | } 263 | -------------------------------------------------------------------------------- /respokeSDK/src/main/java/com/digium/respokesdk/RespokeConnection.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015, Digium, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under The MIT License found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | * For all details and documentation: https://www.respoke.io 9 | */ 10 | 11 | package com.digium.respokesdk; 12 | 13 | import java.lang.ref.WeakReference; 14 | 15 | /** 16 | * Represents remote Connections which belong to an Endpoint. An Endpoint can be authenticated from multiple devices, 17 | * browsers, or tabs. Each of these separate authentications is a Connection. The client can interact 18 | * with connections by calling them or sending them messages. 19 | */ 20 | public class RespokeConnection { 21 | 22 | public String connectionID; 23 | private WeakReference endpointReference; 24 | public Object presence; 25 | 26 | 27 | /** 28 | * The constructor for this class 29 | * 30 | * @param newConnectionID The ID for this connection 31 | * @param newEndpoint The endpoint to which this connection belongs 32 | */ 33 | public RespokeConnection(String newConnectionID, RespokeEndpoint newEndpoint) { 34 | connectionID = newConnectionID; 35 | endpointReference = new WeakReference(newEndpoint); 36 | } 37 | 38 | 39 | /** 40 | * Get the endpoint to which this connection belongs 41 | * 42 | * @return The endpoint instance 43 | */ 44 | public RespokeEndpoint getEndpoint() { 45 | return endpointReference.get(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /respokeSDK/src/main/java/com/digium/respokesdk/RespokeConversationReadStatus.java: -------------------------------------------------------------------------------- 1 | package com.digium.respokesdk; 2 | 3 | import java.util.Date; 4 | 5 | import static android.R.id.message; 6 | 7 | /** 8 | * 9 | */ 10 | 11 | public class RespokeConversationReadStatus { 12 | public String groupId; 13 | public Date timestamp; 14 | 15 | public RespokeConversationReadStatus(String groupId, Date timestamp) { 16 | this.groupId = groupId; 17 | this.timestamp = timestamp; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /respokeSDK/src/main/java/com/digium/respokesdk/RespokeDirectConnection.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015, Digium, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under The MIT License found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | * For all details and documentation: https://www.respoke.io 9 | */ 10 | 11 | package com.digium.respokesdk; 12 | 13 | import android.content.Context; 14 | import android.os.Handler; 15 | import android.os.Looper; 16 | 17 | import org.json.JSONException; 18 | import org.json.JSONObject; 19 | import org.webrtc.DataChannel; 20 | import org.webrtc.PeerConnection; 21 | 22 | import java.io.UnsupportedEncodingException; 23 | import java.lang.ref.WeakReference; 24 | import java.nio.ByteBuffer; 25 | import java.nio.CharBuffer; 26 | import java.nio.charset.CharacterCodingException; 27 | import java.nio.charset.Charset; 28 | import java.nio.charset.CharsetDecoder; 29 | 30 | /** 31 | * A direct connection via RTCDataChannel, including state and path negotation. 32 | */ 33 | public class RespokeDirectConnection implements org.webrtc.DataChannel.Observer { 34 | 35 | private WeakReference listenerReference; 36 | private WeakReference callReference; 37 | private DataChannel dataChannel; 38 | 39 | 40 | /** 41 | * A listener interface to notify the receiver of events occurring with the direct connection 42 | */ 43 | public interface Listener { 44 | 45 | /** 46 | * The direct connection setup has begun. This does NOT mean it's ready to send messages yet. Listen to 47 | * onOpen for that notification. 48 | * 49 | * @param sender The direct connection for which the event occurred 50 | */ 51 | public void onStart(RespokeDirectConnection sender); 52 | 53 | /** 54 | * Called when the direct connection is opened. 55 | * 56 | * @param sender The direct connection for which the event occurred 57 | */ 58 | public void onOpen(RespokeDirectConnection sender); 59 | 60 | /** 61 | * Called when the direct connection is closed. 62 | * 63 | * @param sender The direct connection for which the event occurred 64 | */ 65 | public void onClose(RespokeDirectConnection sender); 66 | 67 | /** 68 | * Called when a message is received over the direct connection. 69 | * @param message The message received. 70 | * @param sender The direct connection for which the event occurred 71 | */ 72 | public void onMessage(String message, RespokeDirectConnection sender); 73 | 74 | } 75 | 76 | 77 | /** 78 | * The constructor for this class 79 | * 80 | * @param call The call instance with which this direct connection is associated 81 | */ 82 | public RespokeDirectConnection(RespokeCall call) { 83 | callReference = new WeakReference(call); 84 | } 85 | 86 | 87 | /** 88 | * Set a receiver for the Listener interface 89 | * 90 | * @param listener The new receiver for events from the Listener interface for this instance 91 | */ 92 | public void setListener(Listener listener) { 93 | if (null != listener) { 94 | listenerReference = new WeakReference(listener); 95 | } else { 96 | listenerReference = null; 97 | } 98 | } 99 | 100 | 101 | /** 102 | * Accept the direct connection and start the process of obtaining media. 103 | * 104 | * @param context An application context with which to access system resources 105 | */ 106 | public void accept(Context context) { 107 | if (null != callReference) { 108 | RespokeCall call = callReference.get(); 109 | if (null != call) { 110 | call.directConnectionDidAccept(context); 111 | } 112 | } 113 | } 114 | 115 | 116 | /** 117 | * Indicate whether a datachannel is being setup or is in progress. 118 | * 119 | * @return True the direct connection is active, false otherwise 120 | */ 121 | public boolean isActive() { 122 | return ((null != dataChannel) && (dataChannel.state() == DataChannel.State.OPEN)); 123 | } 124 | 125 | 126 | /** 127 | * Get the call object associated with this direct connection 128 | * 129 | * @return The call instance 130 | */ 131 | public RespokeCall getCall() { 132 | if (null != callReference) { 133 | return callReference.get(); 134 | } else { 135 | return null; 136 | } 137 | } 138 | 139 | 140 | /** 141 | * Send a message to the remote client through the direct connection. 142 | * 143 | * @param message The message to send 144 | * @param completionListener A listener to receive a notification on the success of the asynchronous operation 145 | */ 146 | public void sendMessage(String message, final Respoke.TaskCompletionListener completionListener) { 147 | if (isActive()) { 148 | JSONObject jsonMessage = new JSONObject(); 149 | try { 150 | jsonMessage.put("message", message); 151 | byte[] rawMessage = jsonMessage.toString().getBytes(Charset.forName("UTF-8")); 152 | ByteBuffer directData = ByteBuffer.allocateDirect(rawMessage.length); 153 | directData.put(rawMessage); 154 | directData.flip(); 155 | DataChannel.Buffer data = new DataChannel.Buffer(directData, false); 156 | 157 | if (dataChannel.send(data)) { 158 | Respoke.postTaskSuccess(completionListener); 159 | } else { 160 | Respoke.postTaskError(completionListener, "Error sending message"); 161 | } 162 | } catch (JSONException e) { 163 | Respoke.postTaskError(completionListener, "Unable to encode message to JSON"); 164 | } 165 | } else { 166 | Respoke.postTaskError(completionListener, "DataChannel not in an open state"); 167 | } 168 | } 169 | 170 | 171 | /** 172 | * Establish a new direct connection instance with the peer connection for the call. This is used internally to the SDK and should not be called directly by your client application. 173 | */ 174 | public void createDataChannel() { 175 | if (null != callReference) { 176 | RespokeCall call = callReference.get(); 177 | if (null != call) { 178 | PeerConnection peerConnection = call.getPeerConnection(); 179 | dataChannel = peerConnection.createDataChannel("respokeDataChannel", new DataChannel.Init()); 180 | dataChannel.registerObserver(this); 181 | } 182 | } 183 | } 184 | 185 | 186 | /** 187 | * Notify the direct connection instance that the peer connection has opened the specified data channel 188 | * 189 | * @param newDataChannel The DataChannel that has opened 190 | */ 191 | public void peerConnectionDidOpenDataChannel(DataChannel newDataChannel) { 192 | if (null != dataChannel) { 193 | // Replacing the previous connection, so disable observer messages from the old instance 194 | dataChannel.unregisterObserver(); 195 | } else { 196 | new Handler(Looper.getMainLooper()).post(new Runnable() { 197 | public void run() { 198 | if (null != listenerReference) { 199 | Listener listener = listenerReference.get(); 200 | if (null != listener) { 201 | listener.onStart(RespokeDirectConnection.this); 202 | } 203 | } 204 | } 205 | }); 206 | } 207 | 208 | dataChannel = newDataChannel; 209 | newDataChannel.registerObserver(this); 210 | } 211 | 212 | 213 | // org.webrtc.DataChannel.Observer methods 214 | 215 | 216 | public void onStateChange() { 217 | switch (dataChannel.state()) { 218 | case CONNECTING: 219 | break; 220 | 221 | case OPEN: { 222 | if (null != callReference) { 223 | RespokeCall call = callReference.get(); 224 | if (null != call) { 225 | call.directConnectionDidOpen(this); 226 | } 227 | } 228 | 229 | new Handler(Looper.getMainLooper()).post(new Runnable() { 230 | public void run() { 231 | if (null != listenerReference) { 232 | Listener listener = listenerReference.get(); 233 | if (null != listener) { 234 | listener.onOpen(RespokeDirectConnection.this); 235 | } 236 | } 237 | } 238 | }); 239 | } 240 | break; 241 | 242 | case CLOSING: 243 | break; 244 | 245 | case CLOSED: { 246 | if (null != callReference) { 247 | RespokeCall call = callReference.get(); 248 | if (null != call) { 249 | call.directConnectionDidClose(this); 250 | } 251 | } 252 | 253 | new Handler(Looper.getMainLooper()).post(new Runnable() { 254 | public void run() { 255 | if (null != listenerReference) { 256 | Listener listener = listenerReference.get(); 257 | if (null != listener) { 258 | listener.onClose(RespokeDirectConnection.this); 259 | } 260 | } 261 | } 262 | }); 263 | } 264 | break; 265 | } 266 | } 267 | 268 | 269 | public void onMessage(org.webrtc.DataChannel.Buffer buffer) { 270 | if (buffer.binary) { 271 | // TODO 272 | } else { 273 | Charset charset = Charset.forName("UTF-8"); 274 | CharsetDecoder decoder = charset.newDecoder(); 275 | try { 276 | String message = decoder.decode( buffer.data ).toString(); 277 | 278 | try { 279 | JSONObject jsonMessage = new JSONObject(message); 280 | final String messageText = jsonMessage.getString("message"); 281 | 282 | if (null != messageText) { 283 | new Handler(Looper.getMainLooper()).post(new Runnable() { 284 | public void run() { 285 | if (null != listenerReference) { 286 | Listener listener = listenerReference.get(); 287 | if (null != listener) { 288 | listener.onMessage(messageText, RespokeDirectConnection.this); 289 | } 290 | } 291 | } 292 | }); 293 | } 294 | } catch (JSONException e) { 295 | // If it is not valid json, ignore the message 296 | } 297 | } catch (CharacterCodingException e) { 298 | // If the message can not be decoded, ignore it 299 | } 300 | } 301 | } 302 | 303 | 304 | } 305 | -------------------------------------------------------------------------------- /respokeSDK/src/main/java/com/digium/respokesdk/RespokeEndpoint.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015, Digium, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under The MIT License found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | * For all details and documentation: https://www.respoke.io 9 | */ 10 | 11 | package com.digium.respokesdk; 12 | 13 | import android.content.Context; 14 | import android.opengl.GLSurfaceView; 15 | import android.os.Handler; 16 | import android.os.Looper; 17 | import android.util.Log; 18 | 19 | import org.json.JSONArray; 20 | import org.json.JSONException; 21 | import org.json.JSONObject; 22 | 23 | import java.lang.ref.WeakReference; 24 | import java.util.ArrayList; 25 | import java.util.Date; 26 | import java.util.Iterator; 27 | 28 | /** 29 | * Represents remote Endpoints. Endpoints are users of this application that are not the one logged into this 30 | * instance of the application. An Endpoint could be logged in from multiple other instances of this app, each of 31 | * which is represented by a Connection. The client can interact with endpoints by calling them or 32 | * sending them messages. An endpoint can be a person using an app from a browser or a script using the APIs on 33 | * a server. 34 | */ 35 | public class RespokeEndpoint { 36 | 37 | private WeakReference listenerReference; 38 | private String endpointID; 39 | public ArrayList connections; 40 | private RespokeSignalingChannel signalingChannel; 41 | public Object presence; 42 | private WeakReference directConnectionReference; 43 | private WeakReference clientReference; 44 | 45 | 46 | /** 47 | * A listener interface to notify the receiver of events occurring with the endpoint 48 | */ 49 | public interface Listener { 50 | 51 | /** 52 | * Handle messages sent to the logged-in user from this one Endpoint. 53 | * 54 | * @param message The message 55 | * @param timestamp The timestamp of the message 56 | * @param endpoint The remote endpoint that sent the message 57 | * @param didSend True if the specified endpoint sent the message, False if it received the message 58 | */ 59 | public void onMessage(String message, Date timestamp, RespokeEndpoint endpoint, boolean didSend); 60 | 61 | 62 | /** 63 | * A notification that the presence for an endpoint has changed 64 | * 65 | * @param presence The new presence 66 | * @param sender The endpoint 67 | */ 68 | public void onPresence(Object presence, RespokeEndpoint sender); 69 | 70 | } 71 | 72 | 73 | /** 74 | * The constructor for this class 75 | * 76 | * @param channel The signaling channel managing communications with this endpoint 77 | * @param newEndpointID The ID for this endpoint 78 | * @param client The client to which this endpoint instance belongs 79 | */ 80 | public RespokeEndpoint(RespokeSignalingChannel channel, String newEndpointID, RespokeClient client) { 81 | endpointID = newEndpointID; 82 | signalingChannel = channel; 83 | connections = new ArrayList(); 84 | clientReference = new WeakReference(client); 85 | } 86 | 87 | 88 | /** 89 | * Set a receiver for the Listener interface 90 | * 91 | * @param listener The new receiver for events from the Listener interface for this endpoint instance 92 | */ 93 | public void setListener(Listener listener) { 94 | listenerReference = new WeakReference(listener); 95 | } 96 | 97 | 98 | /** 99 | * Send a message to the endpoint through the infrastructure. 100 | * 101 | * @param message The message to send 102 | * @param push A flag indicating if a push notification should be sent for this message 103 | * @param ccSelf A flag indicating if the message should be copied to other devices the client might be logged into 104 | * @param completionListener A listener to receive a notification on the success of the asynchronous operation 105 | */ 106 | public void sendMessage(String message, boolean push, boolean ccSelf, final Respoke.TaskCompletionListener completionListener) { 107 | if ((null != signalingChannel) && (signalingChannel.connected)) { 108 | try { 109 | JSONObject data = new JSONObject(); 110 | data.put("to", endpointID); 111 | data.put("message", message); 112 | data.put("push", push); 113 | data.put("ccSelf", ccSelf); 114 | 115 | signalingChannel.sendRESTMessage("post", "/v1/messages", data, new RespokeSignalingChannel.RESTListener() { 116 | @Override 117 | public void onSuccess(Object response) { 118 | Respoke.postTaskSuccess(completionListener); 119 | } 120 | 121 | @Override 122 | public void onError(final String errorMessage) { 123 | Respoke.postTaskError(completionListener, errorMessage); 124 | } 125 | }); 126 | } catch (JSONException e) { 127 | Respoke.postTaskError(completionListener, "Error encoding message"); 128 | } 129 | } else { 130 | Respoke.postTaskError(completionListener, "Can't complete request when not connected. Please reconnect!"); 131 | } 132 | } 133 | 134 | 135 | /** 136 | * Create a new call with audio and optionally video. 137 | * 138 | * @param callListener A listener to receive notifications of call related events 139 | * @param context An application context with which to access system resources 140 | * @param glView A GLSurfaceView into which video from the call should be rendered, or null if the call is audio only 141 | * @param audioOnly Specify true for an audio-only call 142 | * 143 | * @return A new RespokeCall instance 144 | */ 145 | public RespokeCall startCall(RespokeCall.Listener callListener, Context context, GLSurfaceView glView, boolean audioOnly) { 146 | RespokeCall call = null; 147 | 148 | if ((null != signalingChannel) && (signalingChannel.connected)) { 149 | call = new RespokeCall(signalingChannel, this, false); 150 | call.setListener(callListener); 151 | 152 | call.startCall(context, glView, audioOnly); 153 | } 154 | 155 | return call; 156 | } 157 | 158 | 159 | /** 160 | * Get the endpoint's ID 161 | * 162 | * @return The ID 163 | */ 164 | public String getEndpointID() { 165 | return endpointID; 166 | } 167 | 168 | 169 | /** 170 | * Returns a connection with the specified ID, and optionally creates one if it does not exist 171 | * 172 | * @param connectionID The ID of the connection 173 | * @param skipCreate Whether or not to create a new connection if it is not found 174 | * 175 | * @return The connection that matches the specified ID, or null if not found and skipCreate is true 176 | */ 177 | public RespokeConnection getConnection(String connectionID, boolean skipCreate) { 178 | RespokeConnection connection = null; 179 | 180 | for (RespokeConnection eachConnection : connections) { 181 | if (eachConnection.connectionID.equals(connectionID)) { 182 | connection = eachConnection; 183 | break; 184 | } 185 | } 186 | 187 | if ((null == connection) && !skipCreate) { 188 | connection = new RespokeConnection(connectionID, this); 189 | connections.add(connection); 190 | } 191 | 192 | return connection; 193 | } 194 | 195 | 196 | /** 197 | * Get an array of connections associated with this endpoint 198 | * 199 | * @return The array of connections 200 | */ 201 | public ArrayList getConnections() { 202 | return connections; 203 | } 204 | 205 | 206 | /** 207 | * Process a received message. This is used internally to the SDK and should not be called directly by your client application. 208 | * 209 | * @param message The body of the message 210 | * @param timestamp The message timestamp 211 | */ 212 | public void didReceiveMessage(final String message, final Date timestamp) { 213 | new Handler(Looper.getMainLooper()).post(new Runnable() { 214 | @Override 215 | public void run() { 216 | if (null != listenerReference) { 217 | Listener listener = listenerReference.get(); 218 | if (null != listener) { 219 | listener.onMessage(message, timestamp, RespokeEndpoint.this, false); 220 | } 221 | } 222 | } 223 | }); 224 | } 225 | 226 | 227 | /** 228 | * Process a sent message. This is used internally to the SDK and should not be called directly by your client application. 229 | * 230 | * @param message The body of the message 231 | * @param timestamp The message timestamp 232 | */ 233 | public void didSendMessage(final String message, final Date timestamp) { 234 | new Handler(Looper.getMainLooper()).post(new Runnable() { 235 | @Override 236 | public void run() { 237 | if (null != listenerReference) { 238 | Listener listener = listenerReference.get(); 239 | if (null != listener) { 240 | listener.onMessage(message, timestamp, RespokeEndpoint.this, true); 241 | } 242 | } 243 | } 244 | }); 245 | } 246 | 247 | 248 | /** 249 | * Find the presence out of all known connections with the highest priority (most availability) 250 | * and set it as the endpoint's resolved presence. 251 | */ 252 | public void resolvePresence() { 253 | ArrayList list = new ArrayList(); 254 | 255 | for (RespokeConnection eachConnection : connections) { 256 | Object connectionPresence = eachConnection.presence; 257 | 258 | if (null != connectionPresence) { 259 | list.add(connectionPresence); 260 | } 261 | } 262 | 263 | RespokeClient client = null; 264 | RespokeClient.ResolvePresenceListener resolveListener = null; 265 | if (null != clientReference) { 266 | client = clientReference.get(); 267 | 268 | if (client != null) { 269 | resolveListener = client.getResolvePresenceListener(); 270 | } 271 | } 272 | 273 | if (null != resolveListener) { 274 | presence = resolveListener.resolvePresence(list); 275 | } else { 276 | ArrayList options = new ArrayList(); 277 | options.add("chat"); 278 | options.add("available"); 279 | options.add("away"); 280 | options.add("dnd"); 281 | options.add("xa"); 282 | options.add("unavailable"); 283 | 284 | String newPresence = null; 285 | for (String eachOption : options) { 286 | for (Object eachObject : list) { 287 | if (eachObject instanceof String) { 288 | String eachObjectString = (String) eachObject; 289 | 290 | if (eachObjectString.toLowerCase().equals(eachOption)) { 291 | newPresence = eachOption; 292 | } 293 | } 294 | } 295 | 296 | if (null != newPresence) { 297 | break; 298 | } 299 | } 300 | 301 | if (null == newPresence) { 302 | newPresence = "unavailable"; 303 | } 304 | 305 | presence = newPresence; 306 | } 307 | 308 | new Handler(Looper.getMainLooper()).post(new Runnable() { 309 | @Override 310 | public void run() { 311 | if (null != listenerReference) { 312 | Listener listener = listenerReference.get(); 313 | if (null != listener) { 314 | listener.onPresence(presence, RespokeEndpoint.this); 315 | } 316 | } 317 | } 318 | }); 319 | } 320 | 321 | 322 | /** 323 | * Get the active direct connection with this endpoint (if any) 324 | * 325 | * @return The active direct connection instance, or null otherwise 326 | */ 327 | public RespokeDirectConnection directConnection() { 328 | if (null != directConnectionReference) { 329 | return directConnectionReference.get(); 330 | } else { 331 | return null; 332 | } 333 | } 334 | 335 | 336 | /** 337 | * Associate a direct connection object with this endpoint. This method is used internally by the SDK should not be called by your client application. 338 | * 339 | * @param newDirectConnection The direct connection to associate 340 | */ 341 | public void setDirectConnection(RespokeDirectConnection newDirectConnection) { 342 | if (null != newDirectConnection) { 343 | directConnectionReference = new WeakReference(newDirectConnection); 344 | } else { 345 | directConnectionReference = null; 346 | } 347 | } 348 | 349 | 350 | /** 351 | * Create a new DirectConnection. This method creates a new Call as well, attaching this DirectConnection to 352 | * it for the purposes of creating a peer-to-peer link for sending data such as messages to the other endpoint. 353 | * Information sent through a DirectConnection is not handled by the cloud infrastructure. 354 | * 355 | * @return The DirectConnection which can be used to send data and messages directly to the other endpoint. 356 | */ 357 | public RespokeDirectConnection startDirectConnection() { 358 | // The constructor will call the setDirectConnection method on this endpoint instance with a reference to the new RespokeDirectConnection object 359 | RespokeCall call = new RespokeCall(signalingChannel, this, true); 360 | call.startCall(null, null, false); 361 | 362 | return directConnection(); 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /respokeSDK/src/main/java/com/digium/respokesdk/RespokeGroup.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015, Digium, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under The MIT License found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | * For all details and documentation: https://www.respoke.io 9 | */ 10 | 11 | package com.digium.respokesdk; 12 | 13 | import android.os.Handler; 14 | import android.os.Looper; 15 | 16 | import org.json.JSONArray; 17 | import org.json.JSONException; 18 | import org.json.JSONObject; 19 | 20 | import java.lang.ref.WeakReference; 21 | import java.util.ArrayList; 22 | import java.util.Date; 23 | 24 | 25 | /** 26 | * A group, representing a collection of connections and the method by which to communicate with them. 27 | */ 28 | public class RespokeGroup { 29 | 30 | private WeakReference listenerReference; 31 | private String groupID; ///< The ID of this group 32 | private WeakReference clientReference; ///< The client managing this group 33 | private RespokeSignalingChannel signalingChannel; ///< The signaling channel to use 34 | private ArrayList members; ///< An array of the members of this group 35 | private boolean joined; ///< Indicates if the client is a member of this group 36 | 37 | 38 | /** 39 | * A listener interface to notify the receiver of events occurring with the group 40 | */ 41 | public interface Listener { 42 | 43 | 44 | /** 45 | * Receive a notification that an connection has joined this group. 46 | * 47 | * @param connection The RespokeConnection that joined the group 48 | * @param sender The RespokeGroup that the connection has joined 49 | */ 50 | void onJoin(RespokeConnection connection, RespokeGroup sender); 51 | 52 | 53 | /** 54 | * Receive a notification that an connection has left this group. 55 | * 56 | * @param connection The RespokeConnection that left the group 57 | * @param sender The RespokeGroup that the connection has left 58 | */ 59 | void onLeave(RespokeConnection connection, RespokeGroup sender); 60 | 61 | 62 | /** 63 | * Receive a notification that a group message has been received 64 | * 65 | * @param message The body of the message 66 | * @param endpoint The endpoint that sent the message 67 | * @param sender The group that received the message 68 | * @param timestamp The timestamp of when the message was sent. 69 | */ 70 | void onGroupMessage(String message, RespokeEndpoint endpoint, RespokeGroup sender, Date timestamp); 71 | 72 | 73 | } 74 | 75 | 76 | /** 77 | * A listener interface to receive a notification that the task to get the list of group members has completed 78 | */ 79 | public interface GetGroupMembersCompletionListener { 80 | 81 | /** 82 | * Receive an array of the group members asynchronously 83 | * 84 | * @param memberArray An array of the connections that are a member of this group 85 | */ 86 | void onSuccess(ArrayList memberArray); 87 | 88 | 89 | /** 90 | * Receive a notification that the asynchronous operation failed 91 | * 92 | * @param errorMessage A human-readable description of the error that was encountered 93 | */ 94 | void onError(String errorMessage); 95 | } 96 | 97 | /** 98 | * The constructor for this class 99 | * 100 | * @param newGroupID The ID for this group 101 | * @param channel The signaling channel managing communications with this group 102 | * @param newClient The client to which this group instance belongs 103 | * @param isJoined Whether the group has already been joined 104 | */ 105 | public RespokeGroup(String newGroupID, RespokeSignalingChannel channel, RespokeClient newClient, 106 | Boolean isJoined) { 107 | groupID = newGroupID; 108 | signalingChannel = channel; 109 | clientReference = new WeakReference(newClient); 110 | members = new ArrayList(); 111 | joined = isJoined; 112 | } 113 | 114 | public RespokeGroup(String newGroupID, RespokeSignalingChannel channel, RespokeClient newClient) { 115 | this(newGroupID, channel, newClient, true); 116 | } 117 | 118 | /** 119 | * Set a receiver for the Listener interface 120 | * 121 | * @param listener The new receiver for events from the Listener interface for this group instance 122 | */ 123 | public void setListener(Listener listener) { 124 | listenerReference = new WeakReference(listener); 125 | } 126 | 127 | 128 | /** 129 | * Get an array containing the members of the group. 130 | * 131 | * @param completionListener A listener to receive a notification on the success of the asynchronous operation 132 | **/ 133 | public void getMembers(final GetGroupMembersCompletionListener completionListener) { 134 | if (isJoined()) { 135 | if ((null != groupID) && (groupID.length() > 0)) { 136 | String urlEndpoint = "/v1/channels/" + groupID + "/subscribers/"; 137 | 138 | signalingChannel.sendRESTMessage("get", urlEndpoint, null, new RespokeSignalingChannel.RESTListener() { 139 | @Override 140 | public void onSuccess(Object response) { 141 | JSONArray responseArray = null; 142 | 143 | if (response != null) { 144 | if (response instanceof JSONArray) { 145 | responseArray = (JSONArray) response; 146 | } else if (response instanceof String) { 147 | try { 148 | responseArray = new JSONArray((String) response); 149 | } catch (JSONException e) { 150 | // An exception will trigger the error handler 151 | } 152 | } 153 | } 154 | 155 | if (null != responseArray) { 156 | final ArrayList nameList = new ArrayList(); 157 | RespokeClient client = clientReference.get(); 158 | if (null != client) { 159 | for (int ii = 0; ii < responseArray.length(); ii++) { 160 | try { 161 | JSONObject eachEntry = (JSONObject) responseArray.get(ii); 162 | String newEndpointID = eachEntry.getString("endpointId"); 163 | String newConnectionID = eachEntry.getString("connectionId"); 164 | 165 | // Do not include ourselves in this list 166 | if (!newEndpointID.equals(client.getEndpointID())) { 167 | // Get the existing instance for this connection, or create a new one if necessary 168 | RespokeConnection connection = client.getConnection(newConnectionID, newEndpointID, false); 169 | 170 | if (null != connection) { 171 | nameList.add(connection); 172 | } 173 | } 174 | } catch (JSONException e) { 175 | // Skip unintelligible records 176 | } 177 | } 178 | } 179 | 180 | // If certain connections present in the members array prior to this method are somehow no longer in the list received from the server, it's assumed a pending onLeave message will handle flushing it out of the client cache after this method completes 181 | members.clear(); 182 | members.addAll(nameList); 183 | 184 | new Handler(Looper.getMainLooper()).post(new Runnable() { 185 | @Override 186 | public void run() { 187 | if (null != completionListener) { 188 | completionListener.onSuccess(nameList); 189 | } 190 | } 191 | }); 192 | } else { 193 | postGetGroupMembersError(completionListener, "Invalid response from server"); 194 | } 195 | } 196 | 197 | @Override 198 | public void onError(final String errorMessage) { 199 | postGetGroupMembersError(completionListener, errorMessage); 200 | } 201 | }); 202 | } else { 203 | postGetGroupMembersError(completionListener, "Group name must be specified"); 204 | } 205 | } else { 206 | postGetGroupMembersError(completionListener, "Not a member of this group anymore."); 207 | } 208 | } 209 | 210 | 211 | /** 212 | * Join this group 213 | * 214 | * @param completionListener A listener to receive a notification upon completion of this 215 | * async operation 216 | */ 217 | public void join(final Respoke.TaskCompletionListener completionListener) { 218 | if (!isConnected()) { 219 | Respoke.postTaskError(completionListener, "Can't complete request when not connected. " + 220 | "Please reconnect!"); 221 | return; 222 | } 223 | 224 | if ((groupID == null) || (groupID.length() == 0)) { 225 | Respoke.postTaskError(completionListener, "Group name must be specified"); 226 | return; 227 | } 228 | 229 | String urlEndpoint = String.format("/v1/groups/%s", groupID); 230 | signalingChannel.sendRESTMessage("post", urlEndpoint, null, new RespokeSignalingChannel.RESTListener() { 231 | @Override 232 | public void onSuccess(Object response) { 233 | joined = true; 234 | Respoke.postTaskSuccess(completionListener); 235 | } 236 | 237 | @Override 238 | public void onError(final String errorMessage) { 239 | Respoke.postTaskError(completionListener, errorMessage); 240 | } 241 | }); 242 | } 243 | 244 | 245 | /** 246 | * Leave this group 247 | * 248 | * @param completionListener A listener to receive a notification on the success of the asynchronous operation 249 | **/ 250 | public void leave(final Respoke.TaskCompletionListener completionListener) { 251 | if (isJoined()) { 252 | if ((null != groupID) && (groupID.length() > 0)) { 253 | String urlEndpoint = "/v1/groups"; 254 | 255 | JSONArray groupList = new JSONArray(); 256 | groupList.put(groupID); 257 | 258 | JSONObject data = new JSONObject(); 259 | try { 260 | data.put("groups", groupList); 261 | 262 | signalingChannel.sendRESTMessage("delete", urlEndpoint, data, new RespokeSignalingChannel.RESTListener() { 263 | @Override 264 | public void onSuccess(Object response) { 265 | joined = false; 266 | 267 | Respoke.postTaskSuccess(completionListener); 268 | } 269 | 270 | @Override 271 | public void onError(final String errorMessage) { 272 | Respoke.postTaskError(completionListener, errorMessage); 273 | } 274 | }); 275 | } catch (JSONException e) { 276 | Respoke.postTaskError(completionListener, "Error encoding group list to json"); 277 | } 278 | } else { 279 | Respoke.postTaskError(completionListener, "Group name must be specified"); 280 | } 281 | } else { 282 | Respoke.postTaskError(completionListener, "Not a member of this group anymore."); 283 | } 284 | } 285 | 286 | 287 | /** 288 | * Return true if the local client is a member of this group and false if not. 289 | * 290 | * @return The membership status 291 | */ 292 | public boolean isJoined() { 293 | return joined && isConnected(); 294 | } 295 | 296 | public boolean isConnected() { 297 | return (null != signalingChannel) && signalingChannel.connected; 298 | } 299 | 300 | /** 301 | * Get the ID for this group 302 | * 303 | * @return The group's ID 304 | */ 305 | public String getGroupID() { 306 | return groupID; 307 | } 308 | 309 | 310 | /** 311 | * Send a message to the entire group. 312 | * 313 | * @param message The message to send 314 | * @param push A flag indicating if a push notification should be sent for this message 315 | * @param persist A flag indicating if history should be maintained for this message. 316 | * @param completionListener A listener to receive a notification on the success of the asynchronous operation 317 | **/ 318 | public void sendMessage(String message, boolean push, boolean persist, 319 | final Respoke.TaskCompletionListener completionListener) { 320 | if (isJoined()) { 321 | if ((null != groupID) && (groupID.length() > 0)) { 322 | RespokeClient client = clientReference.get(); 323 | if (null != client) { 324 | 325 | JSONObject data = new JSONObject(); 326 | 327 | try { 328 | data.put("endpointId", client.getEndpointID()); 329 | data.put("message", message); 330 | data.put("push", push); 331 | data.put("persist", persist); 332 | } catch (JSONException e) { 333 | Respoke.postTaskError(completionListener, "Unable to encode message"); 334 | return; 335 | } 336 | 337 | String urlEndpoint = "/v1/channels/" + groupID + "/publish/"; 338 | signalingChannel.sendRESTMessage("post", urlEndpoint, data, new RespokeSignalingChannel.RESTListener() { 339 | @Override 340 | public void onSuccess(Object response) { 341 | Respoke.postTaskSuccess(completionListener); 342 | } 343 | 344 | @Override 345 | public void onError(final String errorMessage) { 346 | Respoke.postTaskError(completionListener, errorMessage); 347 | } 348 | }); 349 | } else { 350 | Respoke.postTaskError(completionListener, "There was an internal error processing this request."); 351 | } 352 | } else { 353 | Respoke.postTaskError(completionListener, "Group name must be specified"); 354 | } 355 | } else { 356 | Respoke.postTaskError(completionListener, "Not a member of this group anymore."); 357 | } 358 | } 359 | 360 | public void sendMessage(String message, boolean push, final Respoke.TaskCompletionListener completionListener) { 361 | sendMessage(message, push, false, completionListener); 362 | } 363 | 364 | 365 | /** 366 | * Notify the group that a connection has joined. This is used internally to the SDK and should not be called directly by your client application. 367 | * 368 | * @param connection The connection that has joined the group 369 | */ 370 | public void connectionDidJoin(final RespokeConnection connection) { 371 | members.add(connection); 372 | 373 | new Handler(Looper.getMainLooper()).post(new Runnable() { 374 | @Override 375 | public void run() { 376 | Listener listener = listenerReference.get(); 377 | if (null != listener) { 378 | listener.onJoin(connection, RespokeGroup.this); 379 | } 380 | } 381 | }); 382 | } 383 | 384 | 385 | /** 386 | * Notify the group that a connection has left. This is used internally to the SDK and should not be called directly by your client application. 387 | * 388 | * @param connection The connection that has left the group 389 | */ 390 | public void connectionDidLeave(final RespokeConnection connection) { 391 | members.remove(connection); 392 | 393 | new Handler(Looper.getMainLooper()).post(new Runnable() { 394 | @Override 395 | public void run() { 396 | Listener listener = listenerReference.get(); 397 | if (null != listener) { 398 | listener.onLeave(connection, RespokeGroup.this); 399 | } 400 | } 401 | }); 402 | } 403 | 404 | 405 | /** 406 | * Notify the group that a group message was received. This is used internally to the SDK and should not be called directly by your client application. 407 | * 408 | * @param message The body of the message 409 | * @param endpoint The endpoint that sent the message 410 | * @param timestamp The message timestamp 411 | */ 412 | public void didReceiveMessage(final String message, final RespokeEndpoint endpoint, final Date timestamp) { 413 | new Handler(Looper.getMainLooper()).post(new Runnable() { 414 | @Override 415 | public void run() { 416 | Listener listener = listenerReference.get(); 417 | if (null != listener) { 418 | listener.onGroupMessage(message, endpoint, RespokeGroup.this, timestamp); 419 | } 420 | } 421 | }); 422 | } 423 | 424 | 425 | //** Private methods 426 | 427 | 428 | /** 429 | * A convenience method for posting errors to a GetGroupMembersCompletionListener 430 | * 431 | * @param completionListener The listener to notify 432 | * @param errorMessage The human-readable error message that occurred 433 | */ 434 | private void postGetGroupMembersError(final GetGroupMembersCompletionListener completionListener, final String errorMessage) { 435 | new Handler(Looper.getMainLooper()).post(new Runnable() { 436 | @Override 437 | public void run() { 438 | if (null != completionListener) { 439 | completionListener.onError(errorMessage); 440 | } 441 | } 442 | }); 443 | } 444 | 445 | 446 | } 447 | -------------------------------------------------------------------------------- /respokeSDK/src/main/java/com/digium/respokesdk/RespokeGroupMessage.java: -------------------------------------------------------------------------------- 1 | package com.digium.respokesdk; 2 | 3 | import java.util.Date; 4 | 5 | public class RespokeGroupMessage { 6 | public String message; 7 | public RespokeGroup group; 8 | public RespokeEndpoint endpoint; 9 | public Date timestamp; 10 | 11 | public RespokeGroupMessage(String message, RespokeGroup group, RespokeEndpoint endpoint, 12 | Date timestamp) { 13 | this.message = message; 14 | this.endpoint = endpoint; 15 | this.group = group; 16 | this.timestamp = timestamp; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /respokeSDK/src/main/java/com/digium/respokesdk/RespokeWorkerThread.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015, Digium, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under The MIT License found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | * For all details and documentation: https://www.respoke.io 9 | */ 10 | 11 | package com.digium.respokesdk; 12 | 13 | import android.os.Handler; 14 | import android.os.HandlerThread; 15 | 16 | /** 17 | * Implements a worker thread for queueing and processing socket transactions with the Respoke service 18 | */ 19 | public class RespokeWorkerThread extends HandlerThread { 20 | 21 | private Handler workerHandler; 22 | 23 | 24 | public RespokeWorkerThread(String name) { 25 | super(name); 26 | } 27 | 28 | 29 | public void postTask(Runnable task){ 30 | workerHandler.post(task); 31 | } 32 | 33 | 34 | public void postTaskDelayed(Runnable task, long delayMillis){ 35 | workerHandler.postDelayed(task, delayMillis); 36 | } 37 | 38 | 39 | public void prepareHandler(){ 40 | workerHandler = new Handler(getLooper()); 41 | } 42 | 43 | 44 | public void cancelAllTasks() { 45 | // Cancel all pending tasks and callbacks, but leave the thread ready to run new tasks 46 | workerHandler.removeCallbacksAndMessages(null); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /respokeSDK/src/main/java/com/digium/respokesdk/RestAPI/APIDoOpen.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015, Digium, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under The MIT License found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | * For all details and documentation: https://www.respoke.io 9 | */ 10 | 11 | package com.digium.respokesdk.RestAPI; 12 | 13 | import android.content.Context; 14 | 15 | import org.json.JSONException; 16 | 17 | 18 | public class APIDoOpen extends APITransaction { 19 | 20 | public String tokenID; 21 | public String appToken; 22 | 23 | public APIDoOpen(Context context, String baseURL) { 24 | super(context, baseURL + "/v1/session-tokens"); 25 | } 26 | 27 | 28 | public void go() { 29 | params = "tokenId=" + tokenID; 30 | 31 | super.go(); 32 | } 33 | 34 | 35 | @Override 36 | public void transactionComplete() { 37 | if (success) { 38 | try { 39 | appToken = jsonResult.getString("token"); 40 | } 41 | catch (JSONException e) { 42 | success = false; 43 | errorMessage = "Unexpected response from server"; 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /respokeSDK/src/main/java/com/digium/respokesdk/RestAPI/APIGetToken.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015, Digium, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under The MIT License found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | * For all details and documentation: https://www.respoke.io 9 | */ 10 | 11 | package com.digium.respokesdk.RestAPI; 12 | 13 | import android.content.Context; 14 | 15 | import org.json.JSONException; 16 | 17 | 18 | public class APIGetToken extends APITransaction { 19 | 20 | private static final String DEFAULT_TTL = "21600"; 21 | 22 | public String appID; 23 | public String endpointID; 24 | public String token; 25 | 26 | public APIGetToken(Context context, String baseURL) { 27 | super(context, baseURL + "/v1/tokens"); 28 | } 29 | 30 | 31 | public void go() { 32 | params = "appId=" + appID + "&endpointId=" + endpointID + "&ttl=" + DEFAULT_TTL; 33 | 34 | super.go(); 35 | } 36 | 37 | 38 | @Override 39 | public void transactionComplete() { 40 | if (success) { 41 | try { 42 | token = jsonResult.getString("tokenId"); 43 | } 44 | catch (JSONException e) { 45 | success = false; 46 | errorMessage = "Unexpected response from server"; 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /respokeSDK/src/main/java/com/digium/respokesdk/RestAPI/APITransaction.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015, Digium, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under The MIT License found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | * For all details and documentation: https://www.respoke.io 9 | */ 10 | 11 | package com.digium.respokesdk.RestAPI; 12 | 13 | import java.io.BufferedReader; 14 | import java.io.DataOutputStream; 15 | import java.io.IOException; 16 | import java.io.InputStream; 17 | import java.io.InputStreamReader; 18 | import java.io.UnsupportedEncodingException; 19 | import java.net.CookieHandler; 20 | import java.net.CookieManager; 21 | import java.net.CookiePolicy; 22 | import java.net.HttpURLConnection; 23 | import java.net.URI; 24 | import java.net.URISyntaxException; 25 | import java.net.URL; 26 | 27 | import org.json.JSONException; 28 | import org.json.JSONObject; 29 | 30 | import android.content.Context; 31 | import android.os.AsyncTask; 32 | import android.util.Log; 33 | 34 | import com.digium.respokesdk.BuildConfig; 35 | 36 | 37 | public class APITransaction { 38 | 39 | private static final String TAG = "ApiTransaction"; 40 | public static final String RESPOKE_BASE_URL = "https://api.respoke.io"; 41 | 42 | /** 43 | * Rejects a message if the body size is greater than this. It is enforced server side, so changing this 44 | * won't make the bodySizeLimit any bigger, this just gives you a sensible error if it's too big. 45 | */ 46 | public static final long bodySizeLimit = 20000; 47 | 48 | public boolean abort; 49 | public boolean success; 50 | public String errorMessage; 51 | public JSONObject jsonResult; 52 | public String baseURL; 53 | public String contentType; 54 | Context context; 55 | 56 | private HttpURLConnection connection; 57 | protected String httpMethod; 58 | protected String params; 59 | protected int serverResponseCode; 60 | private AsyncTransaction asyncTrans; 61 | 62 | public static String getSDKHeader() { 63 | String sdkTitle = "Respoke-Android"; 64 | String sdkVersion = BuildConfig.VERSION_NAME; 65 | String androidVersion = android.os.Build.VERSION.RELEASE; 66 | 67 | if (!sdkVersion.equals("")) { 68 | sdkTitle = sdkTitle + "/" + sdkVersion; 69 | } 70 | 71 | return String.format("%s (Android %s)", sdkTitle, androidVersion); 72 | } 73 | 74 | public APITransaction(Context context, String baseURL) { 75 | this.context = context; 76 | abort = false; 77 | httpMethod = "POST"; 78 | params = null; 79 | this.baseURL = baseURL; 80 | contentType = "application/x-www-form-urlencoded"; 81 | } 82 | 83 | 84 | public void go() { 85 | asyncTrans = new AsyncTransaction(); 86 | asyncTrans.execute(this.httpMethod); 87 | } 88 | 89 | 90 | public void transactionComplete() { 91 | // This method is overridden by child classes 92 | } 93 | 94 | 95 | public void cancel() { 96 | abort = true; 97 | if (asyncTrans != null) { 98 | asyncTrans.cancel(); 99 | } 100 | } 101 | 102 | 103 | private class AsyncTransaction extends AsyncTask { 104 | 105 | public void cancel() { 106 | if (connection != null) { 107 | Log.e(TAG, "connection cancelled!"); 108 | connection.disconnect(); 109 | } 110 | this.cancel(true); 111 | } 112 | 113 | @Override 114 | protected Object doInBackground(String... args) { 115 | 116 | String httpMethod = args[0]; 117 | 118 | // clear any previous received data 119 | jsonResult = null; 120 | 121 | try { 122 | if (params.getBytes("UTF-8").length <= bodySizeLimit) { 123 | try { 124 | //accept no cookies 125 | CookieManager cookieManager = new CookieManager(); 126 | cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_NONE); 127 | CookieHandler.setDefault(cookieManager); 128 | 129 | System.setProperty("http.keepAlive", "false"); 130 | 131 | URI uri = new URI(baseURL.replace(" ", "%20")); 132 | URL url = new URL(uri.toASCIIString()); 133 | connection = (HttpURLConnection) url.openConnection(); 134 | 135 | // Allow Inputs & Outputs 136 | connection.setRequestMethod(httpMethod); 137 | connection.setDoInput(true); 138 | connection.setUseCaches(false); 139 | if (httpMethod.equals("POST")) { 140 | connection.setDoOutput(true); 141 | } 142 | 143 | //Headers 144 | connection.setRequestProperty("Content-Type", contentType); 145 | connection.setRequestProperty("Accept", "application/xml"); 146 | connection.setRequestProperty("Respoke-SDK", getSDKHeader()); 147 | 148 | if (httpMethod.equals("POST")) { 149 | //open stream and start writing 150 | DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream()); 151 | outputStream.writeBytes(params); 152 | outputStream.flush(); 153 | outputStream.close(); 154 | } 155 | 156 | serverResponseCode = connection.getResponseCode(); 157 | 158 | if (serverResponseCode == 200) { 159 | success = true; 160 | 161 | // Parse the response data into a string 162 | String receivedString = readResponse(connection.getInputStream()); 163 | 164 | if (receivedString != null) { 165 | try { 166 | // Parse the response string into JSON objects 167 | jsonResult = new JSONObject(receivedString); 168 | } catch (JSONException e) { 169 | errorMessage = "Error deserializing response"; 170 | success = false; 171 | } 172 | } 173 | } else { 174 | throw new IOException(Integer.toString(serverResponseCode)); 175 | } 176 | 177 | } catch (IOException e) { 178 | success = false; 179 | errorMessage = e.getLocalizedMessage(); 180 | 181 | try { 182 | Log.e(TAG, "serverResponseCode = " + connection.getResponseCode()); 183 | Log.e(TAG, "serverResponseMessage = " + connection.getResponseMessage()); 184 | serverResponseCode = connection.getResponseCode(); 185 | String serverResponseMessage = connection.getResponseMessage(); 186 | 187 | if (serverResponseCode == 401) { 188 | errorMessage = "API authentication error"; 189 | } else if (serverResponseCode == 429) { 190 | errorMessage = "API rate limit was exceeded"; 191 | } else if (serverResponseCode == 503) { 192 | errorMessage = "Server is down for maintenance"; 193 | } else if (serverResponseCode >= 400) { 194 | errorMessage = "Failed with server error = " + serverResponseCode + " message = " + serverResponseMessage; 195 | } else { 196 | errorMessage = "Unknown Error. http status = " + serverResponseCode + " message = " + serverResponseMessage; 197 | } 198 | } catch (IOException ioe) { 199 | ioe.printStackTrace(); 200 | } 201 | } catch (URISyntaxException e) { 202 | Log.e(TAG, "Bad URI!"); 203 | errorMessage = "An invalid server URL was specified"; 204 | success = false; 205 | } catch (Exception e) { 206 | Log.e(TAG, "Unknown exception"); 207 | errorMessage = "An unknown problem occurred"; 208 | success = false; 209 | } finally { 210 | if (connection != null) { 211 | connection.disconnect(); 212 | } 213 | } 214 | } else { 215 | errorMessage = "Request body is too big"; 216 | success = false; 217 | } 218 | } catch (UnsupportedEncodingException e) { 219 | errorMessage = "Unable to encode message"; 220 | success = false; 221 | } 222 | 223 | return jsonResult; 224 | } 225 | 226 | 227 | //Process after transaction is complete 228 | @Override 229 | protected void onPostExecute(Object result) 230 | { 231 | transactionComplete(); 232 | } 233 | 234 | 235 | private String readResponse(InputStream stream) { 236 | String receivedString = null; 237 | 238 | try { 239 | BufferedReader br = new BufferedReader(new InputStreamReader(stream)); 240 | StringBuilder sb = new StringBuilder(); 241 | String line; 242 | 243 | while ((line = br.readLine()) != null) { 244 | sb.append(line).append("\n"); 245 | } 246 | 247 | br.close(); 248 | receivedString = sb.toString(); 249 | } catch (IOException ioe) { 250 | // Do nothing 251 | } 252 | 253 | return receivedString; 254 | } 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /respokeSDK/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/respoke/respoke-sdk-android/34a15f0558d29b1f1bc8481bbc5c505e855e05ef/respokeSDK/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /respokeSDK/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/respoke/respoke-sdk-android/34a15f0558d29b1f1bc8481bbc5c505e855e05ef/respokeSDK/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /respokeSDK/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/respoke/respoke-sdk-android/34a15f0558d29b1f1bc8481bbc5c505e855e05ef/respokeSDK/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /respokeSDK/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/respoke/respoke-sdk-android/34a15f0558d29b1f1bc8481bbc5c505e855e05ef/respokeSDK/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /respokeSDK/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Respoke SDK 3 | 4 | -------------------------------------------------------------------------------- /respokeSDKTest/android-wait-for-emulator: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Originally written by Ralf Kistner , but placed in the public domain 4 | 5 | set +e 6 | 7 | # If ANDROID_HOME is specified, use that adb; otherwise, search the path 8 | if test ${ANDROID_HOME}; then 9 | : ${ADB:=${ANDROID_HOME}/platform-tools/adb} 10 | else 11 | : ${ADB:=adb} 12 | fi 13 | 14 | bootanim="" 15 | failcounter=0 16 | timeout_in_sec=120 17 | 18 | until [[ "$bootanim" =~ "stopped" ]]; do 19 | bootanim=`${ADB} -e shell getprop init.svc.bootanim 2>&1 &` 20 | if [[ "$bootanim" =~ "device not found" || "$bootanim" =~ "device offline" 21 | || "$bootanim" =~ "running" ]]; then 22 | let "failcounter += 1" 23 | echo "Waiting for emulator to start" 24 | if [[ $failcounter -gt timeout_in_sec ]]; then 25 | echo "Timeout ($timeout_in_sec seconds) reached; failed to start emulator" 26 | exit 1 27 | fi 28 | fi 29 | sleep 1 30 | done 31 | 32 | echo "Emulator is ready" 33 | -------------------------------------------------------------------------------- /respokeSDKTest/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion '23.0.3' 6 | 7 | defaultConfig { 8 | applicationId "com.digium.respokesdktest" 9 | minSdkVersion 15 10 | targetSdkVersion 23 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | resValue "string", "TEST_BOT_SUFFIX", System.getenv("USER") 20 | } 21 | debug{ 22 | resValue "string", "TEST_BOT_SUFFIX", System.getenv("USER") 23 | } 24 | } 25 | } 26 | 27 | dependencies { 28 | compile fileTree(dir: 'libs', include: ['*.jar']) 29 | compile 'com.android.support:appcompat-v7:23.2.0' 30 | compile project(':respokeSDK') 31 | } 32 | -------------------------------------------------------------------------------- /respokeSDKTest/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/jasonadams/android_sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /respokeSDKTest/respokeSDKTest.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /respokeSDKTest/runtests.sh: -------------------------------------------------------------------------------- 1 | # !/bin/bash 2 | 3 | #emulator @Nexus_One_API_19 & 4 | 5 | ./gradlew clean connectedAndroidTest --stacktrace 6 | -------------------------------------------------------------------------------- /respokeSDKTest/src/androidTest/java/com/digium/respokesdktest/RespokeActivityTestCase.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015, Digium, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under The MIT License found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | * For all details and documentation: https://www.respoke.io 9 | */ 10 | 11 | package com.digium.respokesdktest; 12 | 13 | import android.content.Context; 14 | import android.test.ActivityInstrumentationTestCase2; 15 | 16 | import com.digium.respokesdk.Respoke; 17 | import com.digium.respokesdk.RespokeClient; 18 | 19 | import java.util.concurrent.CountDownLatch; 20 | import java.util.concurrent.TimeUnit; 21 | 22 | 23 | public abstract class RespokeActivityTestCase extends ActivityInstrumentationTestCase2 { 24 | 25 | public CountDownLatch asyncTaskSignal = new CountDownLatch(1); 26 | 27 | 28 | public RespokeActivityTestCase(Class activityClass) { 29 | super(activityClass); 30 | } 31 | 32 | 33 | public RespokeClient createTestClient(final String endpointID, RespokeClient.Listener listener, final Context context) { 34 | final RespokeClient client = Respoke.sharedInstance().createClient(context); 35 | assertNotNull("Should create test client", client); 36 | client.baseURL = RespokeTestCase.TEST_RESPOKE_BASE_URL; 37 | 38 | asyncTaskSignal = new CountDownLatch(1); // Reset the countdown signal 39 | client.setListener(listener); 40 | try { 41 | runTestOnUiThread(new Runnable() { 42 | @Override 43 | public void run() { 44 | client.connect(endpointID, RespokeTestCase.TEST_APP_ID, true, null, context, new RespokeClient.ConnectCompletionListener() { 45 | @Override 46 | public void onError(String errorMessage) { 47 | assertTrue("Should successfully connect. error: " + errorMessage, false); 48 | asyncTaskSignal.countDown(); 49 | } 50 | }); 51 | } 52 | }); 53 | 54 | assertTrue("Client connect timed out", asyncTaskSignal.await(RespokeTestCase.TEST_TIMEOUT, TimeUnit.SECONDS)); 55 | assertTrue("Test client should connect", client.isConnected()); 56 | 57 | } catch (Throwable throwable) { 58 | assertTrue("Exception encountered while creating test client", false); 59 | } 60 | 61 | return client; 62 | } 63 | 64 | 65 | } 66 | -------------------------------------------------------------------------------- /respokeSDKTest/src/androidTest/java/com/digium/respokesdktest/RespokeTestCase.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015, Digium, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under The MIT License found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | * For all details and documentation: https://www.respoke.io 9 | */ 10 | 11 | package com.digium.respokesdktest; 12 | 13 | import android.content.Context; 14 | import android.os.Looper; 15 | import android.test.AndroidTestCase; 16 | import android.util.Log; 17 | 18 | import com.digium.respokesdk.Respoke; 19 | import com.digium.respokesdk.RespokeClient; 20 | 21 | /** 22 | * A test case base class to provide commonly used methods 23 | */ 24 | public abstract class RespokeTestCase extends AndroidTestCase { 25 | 26 | public final static String TEST_RESPOKE_BASE_URL = "https://api.respoke.io"; 27 | public final static String TEST_APP_ID = "REPLACE_ME_WITH_YOUR_APP_ID"; 28 | public static final String TEST_MESSAGE = "This is a test message!"; 29 | public static int TEST_TIMEOUT = 60; // Timeout in seconds 30 | public static int CALL_TEST_TIMEOUT = 60; // Timeout in seconds for calling tests (which take longer to setup) 31 | public boolean asyncTaskDone; 32 | 33 | private static final String TAG = "RespokeTestCase"; 34 | 35 | 36 | public static String getTestBotEndpointId(Context context) { 37 | return "testbot-" + context.getResources().getText(R.string.TEST_BOT_SUFFIX); 38 | } 39 | 40 | 41 | public static String generateTestEndpointID() { 42 | String uuid = ""; 43 | String chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; 44 | int rnd = 0; 45 | int r; 46 | 47 | for (int i = 0; i < 6; i += 1) { 48 | if (rnd <= 0x02) { 49 | rnd = (int) (0x2000000 + Math.round(java.lang.Math.random() * 0x1000000)); 50 | } 51 | r = rnd & 0xf; 52 | rnd = rnd >> 4; 53 | uuid = uuid + chars.charAt(r); 54 | } 55 | return "test_user_" + uuid; 56 | } 57 | 58 | 59 | public boolean waitForCompletion(long timeoutSecs) { 60 | long timeoutDate = System.currentTimeMillis() + (timeoutSecs * 1000); 61 | 62 | try { 63 | do { 64 | if (System.currentTimeMillis() > timeoutDate) { 65 | Log.e(TAG, "network timeout"); 66 | Log.e(TAG, "inside TapInspectTestCase : class = " + this.getClass().getSimpleName()); 67 | break; 68 | } 69 | synchronized (this) { 70 | this.wait(100); 71 | } 72 | } while (!asyncTaskDone); 73 | } catch (InterruptedException e) { 74 | e.printStackTrace(); 75 | return false; 76 | } 77 | 78 | return asyncTaskDone; 79 | } 80 | 81 | 82 | public RespokeClient createTestClient(String endpointID, RespokeClient.Listener listener) { 83 | final RespokeClient client = Respoke.sharedInstance().createClient(getContext()); 84 | assertNotNull("Should create test client", client); 85 | client.baseURL = TEST_RESPOKE_BASE_URL; 86 | 87 | asyncTaskDone = false; 88 | client.setListener(listener); 89 | client.connect(endpointID, RespokeTestCase.TEST_APP_ID, true, null, getContext(), new RespokeClient.ConnectCompletionListener() { 90 | @Override 91 | public void onError(String errorMessage) { 92 | assertTrue("Should successfully connect", false); 93 | asyncTaskDone = true; 94 | } 95 | }); 96 | 97 | assertTrue("Client connect timed out", waitForCompletion(RespokeTestCase.TEST_TIMEOUT)); 98 | assertTrue("Test client should connect", client.isConnected()); 99 | 100 | return client; 101 | } 102 | 103 | 104 | public static boolean currentlyOnUIThread() { 105 | // Returns true if the current thread is the UI thread 106 | return Looper.myLooper() == Looper.getMainLooper(); 107 | } 108 | 109 | 110 | } 111 | -------------------------------------------------------------------------------- /respokeSDKTest/src/androidTest/java/com/digium/respokesdktest/functional/ConnectionTests.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015, Digium, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under The MIT License found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | * For all details and documentation: https://www.respoke.io 9 | */ 10 | 11 | package com.digium.respokesdktest.functional; 12 | 13 | import android.util.Log; 14 | 15 | import com.digium.respokesdk.Respoke; 16 | import com.digium.respokesdk.RespokeCall; 17 | import com.digium.respokesdk.RespokeClient; 18 | import com.digium.respokesdk.RespokeDirectConnection; 19 | import com.digium.respokesdk.RespokeEndpoint; 20 | import com.digium.respokesdk.RespokeGroup; 21 | import com.digium.respokesdktest.RespokeTestCase; 22 | 23 | import java.util.Date; 24 | 25 | 26 | public class ConnectionTests extends RespokeTestCase implements RespokeClient.Listener { 27 | 28 | private boolean rateLimitHit; 29 | RespokeEndpoint endpoint; 30 | 31 | // Test temporarily disabled until socket library issues are resolved 32 | /* 33 | public void testRateLimiting() { 34 | // Create a client to test with 35 | final String testEndpointID = generateTestEndpointID(); 36 | final RespokeClient client = createTestClient(testEndpointID, this); 37 | 38 | endpoint = client.getEndpoint(testEndpointID, false); 39 | assertNotNull("Should create endpoint instance", endpoint); 40 | 41 | asyncTaskDone = false; 42 | 43 | // Launch 5 simultaneous cascades of presence registration calls to the server, in an effort to hit the rate limit 44 | sendLoop(1); 45 | sendLoop(1); 46 | sendLoop(1); 47 | sendLoop(1); 48 | sendLoop(1); 49 | 50 | assertTrue("Test timed out", waitForCompletion(RespokeTestCase.TEST_TIMEOUT)); 51 | assertTrue("Should eventually hit a rate limit error", rateLimitHit); 52 | } 53 | 54 | 55 | public void sendLoop(final Integer attempt) { 56 | // Generally hits the rate limit around 20 attempts 57 | if (!rateLimitHit && (attempt < 30)) { 58 | endpoint.registerPresence(new Respoke.TaskCompletionListener() { 59 | @Override 60 | public void onSuccess() { 61 | assertTrue("Should be called in UI thread", RespokeTestCase.currentlyOnUIThread()); 62 | sendLoop(attempt + 1); 63 | } 64 | 65 | @Override 66 | public void onError(String errorMessage) { 67 | if (!rateLimitHit) { 68 | assertTrue("Should indicate a rate limit error. received: " + errorMessage, errorMessage.equals("API rate limit was exceeded")); 69 | rateLimitHit = true; 70 | asyncTaskDone = true; 71 | } 72 | } 73 | }); 74 | } else { 75 | asyncTaskDone = true; 76 | } 77 | } 78 | */ 79 | 80 | // RespokeClient.Listener methods 81 | 82 | 83 | public void onConnect(RespokeClient sender) { 84 | asyncTaskDone = true; 85 | } 86 | 87 | 88 | public void onDisconnect(RespokeClient sender, boolean reconnecting) { 89 | assertTrue("Should not disconnect during testing", false); 90 | asyncTaskDone = true; 91 | } 92 | 93 | 94 | public void onError(RespokeClient sender, String errorMessage) { 95 | assertTrue("Should not produce any client errors during endpoint testing", false); 96 | asyncTaskDone = true; 97 | } 98 | 99 | 100 | public void onCall(RespokeClient sender, RespokeCall call) { 101 | // Not under test 102 | } 103 | 104 | 105 | public void onIncomingDirectConnection(RespokeDirectConnection directConnection, RespokeEndpoint endpoint) { 106 | // Not under test 107 | } 108 | 109 | 110 | public void onMessage(String message, RespokeEndpoint endpoint, RespokeGroup group, Date timestamp, Boolean didSend) { 111 | // Not under test 112 | } 113 | 114 | 115 | } 116 | -------------------------------------------------------------------------------- /respokeSDKTest/src/androidTest/java/com/digium/respokesdktest/functional/DirectConnectionTests.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015, Digium, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under The MIT License found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | * For all details and documentation: https://www.respoke.io 9 | */ 10 | 11 | package com.digium.respokesdktest.functional; 12 | 13 | import com.digium.respokesdk.Respoke; 14 | import com.digium.respokesdk.RespokeCall; 15 | import com.digium.respokesdk.RespokeClient; 16 | import com.digium.respokesdk.RespokeDirectConnection; 17 | import com.digium.respokesdk.RespokeEndpoint; 18 | import com.digium.respokesdk.RespokeGroup; 19 | import com.digium.respokesdktest.RespokeTestCase; 20 | 21 | import java.util.Date; 22 | 23 | 24 | public class DirectConnectionTests extends RespokeTestCase implements RespokeClient.Listener, RespokeEndpoint.Listener, RespokeCall.Listener, RespokeDirectConnection.Listener { 25 | 26 | private boolean callbackDidSucceed; 27 | private boolean messageReceived; 28 | private boolean didConnect; 29 | private boolean didHangup; 30 | private boolean didGetIncomingDirectConnection; 31 | private boolean didGetCallerOnOpen; 32 | private boolean didGetCalleeOnOpen; 33 | private boolean didGetCallerOnClose; 34 | private boolean didGetCalleeOnClose; 35 | private RespokeEndpoint firstEndpoint; 36 | private RespokeEndpoint secondEndpoint; 37 | private RespokeDirectConnection callerDirectConnection; 38 | private RespokeDirectConnection calleeDirectConnection; 39 | private Object receivedMessageObject; 40 | 41 | 42 | public void testDirectConnection() { 43 | // Create a client to test with 44 | final String testEndpointID = generateTestEndpointID(); 45 | final RespokeClient firstClient = createTestClient(testEndpointID, this); 46 | 47 | // Create a second client to test with 48 | final String secondTestEndpointID = generateTestEndpointID(); 49 | final RespokeClient secondClient = createTestClient(secondTestEndpointID, this); 50 | 51 | // Build references to each of the endpoints 52 | firstEndpoint = secondClient.getEndpoint(testEndpointID, false); 53 | assertNotNull("Should create endpoint instance", firstEndpoint); 54 | firstEndpoint.setListener(this); 55 | 56 | secondEndpoint = firstClient.getEndpoint(secondTestEndpointID, false); 57 | assertNotNull("Should create endpoint instance", secondEndpoint); 58 | secondEndpoint.setListener(this); 59 | 60 | // Start a direct connection between the two endpoints, calling from firstEndpoint 61 | asyncTaskDone = false; 62 | didGetIncomingDirectConnection = false; 63 | didGetCallerOnOpen = false; 64 | didGetCalleeOnOpen = false; 65 | callerDirectConnection = secondEndpoint.startDirectConnection(); 66 | callerDirectConnection.setListener(this); 67 | 68 | RespokeCall call = callerDirectConnection.getCall(); 69 | call.setListener(this); 70 | 71 | assertTrue("Test timed out", waitForCompletion(RespokeTestCase.CALL_TEST_TIMEOUT)); 72 | assertTrue("Call should be established", didConnect); 73 | assertTrue("Callee client should have received an incoming direct connection notification", didGetIncomingDirectConnection); 74 | assertTrue("Caller should have received an onOpen notification", didGetCallerOnOpen); 75 | assertTrue("Callee should have received an onOpen notification", didGetCalleeOnOpen); 76 | assertTrue("Call should indicate that it is the caller", call.isCaller()); 77 | assertTrue("Should indicate call is with the endpoint that the call was started from", secondEndpoint == call.endpoint); 78 | assertNotNull("Callee should have been notified about the incoming direct connection", calleeDirectConnection); 79 | 80 | // Test sending a text message over the direct connection 81 | 82 | asyncTaskDone = false; 83 | callbackDidSucceed = false; 84 | messageReceived = false; 85 | callerDirectConnection.sendMessage(TEST_MESSAGE, new Respoke.TaskCompletionListener() { 86 | @Override 87 | public void onSuccess() { 88 | assertTrue("Should be called in UI thread", RespokeTestCase.currentlyOnUIThread()); 89 | callbackDidSucceed = true; 90 | asyncTaskDone = messageReceived; 91 | } 92 | 93 | @Override 94 | public void onError(String errorMessage) { 95 | assertTrue("Should not encounter an error when sending a message over a direct connection. Error: %@" + errorMessage, false); 96 | asyncTaskDone = true; 97 | } 98 | }); 99 | 100 | assertTrue("Test timed out", waitForCompletion(RespokeTestCase.TEST_TIMEOUT)); 101 | assertTrue("sendMessage should have called the successHandler", callbackDidSucceed); 102 | assertTrue("Received message should be a string", receivedMessageObject instanceof String); 103 | assertTrue("Should have received correct message", receivedMessageObject.equals(TEST_MESSAGE)); 104 | 105 | asyncTaskDone = false; 106 | didGetCallerOnClose = false; 107 | didGetCalleeOnClose = false; 108 | didHangup = false; 109 | call.hangup(true); 110 | 111 | assertTrue("Test timed out", waitForCompletion(RespokeTestCase.TEST_TIMEOUT)); 112 | assertTrue("Callee should have received onClose notification", didGetCalleeOnClose); 113 | assertTrue("Caller should have received onClose notification", didGetCallerOnClose); 114 | } 115 | 116 | 117 | // RespokeClient.Listener methods 118 | 119 | 120 | public void onConnect(RespokeClient sender) { 121 | asyncTaskDone = true; 122 | } 123 | 124 | 125 | public void onDisconnect(RespokeClient sender, boolean reconnecting) { 126 | // Not under test 127 | } 128 | 129 | 130 | public void onError(RespokeClient sender, String errorMessage) { 131 | assertTrue("Should not produce any client errors during testing", false); 132 | asyncTaskDone = true; 133 | } 134 | 135 | 136 | public void onCall(RespokeClient sender, RespokeCall call) { 137 | // Not under test 138 | } 139 | 140 | 141 | public void onIncomingDirectConnection(RespokeDirectConnection directConnection, RespokeEndpoint endpoint) { 142 | assertTrue("Should be called in UI thread", RespokeTestCase.currentlyOnUIThread()); 143 | assertTrue("Should originate from the first Endpoint", endpoint == firstEndpoint); 144 | assertNotNull("DirectConnection object should not be nil", directConnection); 145 | calleeDirectConnection = directConnection; 146 | calleeDirectConnection.setListener(this); 147 | didGetIncomingDirectConnection = true; 148 | 149 | // Accept the call to continue the connection process 150 | directConnection.accept(getContext()); 151 | 152 | asyncTaskDone = didConnect && didGetCallerOnOpen && didGetCalleeOnOpen; 153 | } 154 | 155 | 156 | public void onMessage(String message, RespokeEndpoint endpoint, RespokeGroup group, Date timestamp, Boolean didSend) { 157 | // Not under test 158 | } 159 | 160 | 161 | // RespokeEndpoint.Listener methods 162 | 163 | 164 | public void onMessage(String message, Date timestamp, RespokeEndpoint endpoint, boolean didSend) { 165 | assertTrue("No messages should have been received through the Respoke service", false); 166 | } 167 | 168 | 169 | public void onPresence(Object presence, RespokeEndpoint sender) { 170 | // Not under test 171 | } 172 | 173 | 174 | // RespokeCall.Listener methods 175 | 176 | 177 | public void onError(String errorMessage, RespokeCall sender) { 178 | assertTrue("Should be called in UI thread", RespokeTestCase.currentlyOnUIThread()); 179 | assertTrue("Should perform a call without any errors. Error: " + errorMessage, false); 180 | asyncTaskDone = true; 181 | } 182 | 183 | 184 | public void onHangup(RespokeCall sender) { 185 | assertTrue("Should be called in UI thread", RespokeTestCase.currentlyOnUIThread()); 186 | didHangup = true; 187 | asyncTaskDone = didGetCallerOnClose && didGetCalleeOnClose; 188 | } 189 | 190 | 191 | public void onConnected(RespokeCall sender) { 192 | assertTrue("Should be called in UI thread", RespokeTestCase.currentlyOnUIThread()); 193 | didConnect = true; 194 | asyncTaskDone = didGetIncomingDirectConnection && didGetCallerOnOpen && didGetCalleeOnOpen; 195 | } 196 | 197 | 198 | public void directConnectionAvailable(RespokeDirectConnection directConnection, RespokeEndpoint endpoint) { 199 | assertTrue("Should be called in UI thread", RespokeTestCase.currentlyOnUIThread()); 200 | assertTrue("Should reference correct direct connection object", directConnection == callerDirectConnection); 201 | assertTrue("Should reference correct remote endpoint", endpoint == secondEndpoint); 202 | } 203 | 204 | 205 | // RespokeDirectConnection.Listener methods 206 | 207 | 208 | public void onStart(RespokeDirectConnection sender) { 209 | // This callback will not be called in this test. It is only triggered when adding a directConnection to an existing call, which is currently not supported. 210 | } 211 | 212 | 213 | public void onOpen(RespokeDirectConnection sender) { 214 | assertTrue("Should be called in UI thread", RespokeTestCase.currentlyOnUIThread()); 215 | if (sender == callerDirectConnection) { 216 | didGetCallerOnOpen = true; 217 | } else if (sender == calleeDirectConnection) { 218 | didGetCalleeOnOpen = true; 219 | } else { 220 | assertTrue("Should reference the correct direct connection object", false); 221 | } 222 | 223 | asyncTaskDone = didGetIncomingDirectConnection && didConnect && didGetCallerOnOpen && didGetCalleeOnOpen; 224 | } 225 | 226 | 227 | public void onClose(RespokeDirectConnection sender){ 228 | assertTrue("Should be called in UI thread", RespokeTestCase.currentlyOnUIThread()); 229 | if (sender == callerDirectConnection) { 230 | didGetCallerOnClose = true; 231 | } else if (sender == calleeDirectConnection) { 232 | didGetCalleeOnClose = true; 233 | } else { 234 | assertTrue("Should reference the correct direct connection object", false); 235 | } 236 | 237 | asyncTaskDone = didHangup && didGetCallerOnClose && didGetCalleeOnClose; 238 | } 239 | 240 | 241 | public void onMessage(String message, RespokeDirectConnection sender) 242 | { 243 | assertTrue("Should be called in UI thread", RespokeTestCase.currentlyOnUIThread()); 244 | assertTrue("Should reference the correct direct connection object", sender == calleeDirectConnection); 245 | assertNotNull("message should not be null", message); 246 | receivedMessageObject = message; 247 | messageReceived = true; 248 | asyncTaskDone = callbackDidSucceed; 249 | } 250 | 251 | } 252 | -------------------------------------------------------------------------------- /respokeSDKTest/src/androidTest/java/com/digium/respokesdktest/functional/HangupTests.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015, Digium, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under The MIT License found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | * For all details and documentation: https://www.respoke.io 9 | */ 10 | 11 | package com.digium.respokesdktest.functional; 12 | 13 | import com.digium.respokesdk.Respoke; 14 | import com.digium.respokesdk.RespokeCall; 15 | import com.digium.respokesdk.RespokeClient; 16 | import com.digium.respokesdk.RespokeDirectConnection; 17 | import com.digium.respokesdk.RespokeEndpoint; 18 | import com.digium.respokesdk.RespokeGroup; 19 | import com.digium.respokesdktest.MainActivity; 20 | import com.digium.respokesdktest.RespokeActivityTestCase; 21 | import com.digium.respokesdktest.RespokeTestCase; 22 | 23 | import java.util.Date; 24 | import java.util.concurrent.CountDownLatch; 25 | import java.util.concurrent.TimeUnit; 26 | 27 | public class HangupTests extends RespokeActivityTestCase implements RespokeClient.Listener, RespokeEndpoint.Listener, RespokeCall.Listener { 28 | 29 | private boolean callbackDidSucceed; 30 | private boolean firstCallDidHangup; 31 | private boolean secondCallDidHangup; 32 | private RespokeCall firstIncomingCall; 33 | private RespokeCall secondIncomingCall; 34 | private RespokeEndpoint testbotEndpoint; 35 | private RespokeClient firstClient; 36 | private RespokeClient secondClient; 37 | 38 | 39 | public HangupTests() { 40 | super(MainActivity.class); 41 | } 42 | 43 | 44 | public void testCallDeclineCCSelf() throws Throwable { 45 | // Create a client to test with 46 | final String testEndpointID = RespokeTestCase.generateTestEndpointID(); 47 | firstClient = createTestClient(testEndpointID, this, getActivity()); 48 | secondClient = createTestClient(testEndpointID, this, getActivity()); 49 | 50 | // If things went well, there should be a web page open on the test host running a Transporter app that is logged in as testbot. It is set up to automatically initiate a call when asked via Respoke message 51 | 52 | testbotEndpoint = firstClient.getEndpoint(RespokeTestCase.getTestBotEndpointId(getActivity()), false); 53 | assertNotNull("Should create endpoint instance", testbotEndpoint); 54 | testbotEndpoint.setListener(this); 55 | 56 | // Send a quick message to make sure the test UI is running and produce a meaningful test error message 57 | asyncTaskSignal = new CountDownLatch(1); // Reset the countdown signal 58 | callbackDidSucceed = false; 59 | runTestOnUiThread(new Runnable() { 60 | @Override 61 | public void run() { 62 | testbotEndpoint.sendMessage(CallingTests.TEST_BOT_CALL_ME_MESSAGE, false, true, new Respoke.TaskCompletionListener() { 63 | @Override 64 | public void onSuccess() { 65 | assertTrue("Should be called in UI thread", RespokeTestCase.currentlyOnUIThread()); 66 | callbackDidSucceed = true; 67 | if ((null != firstIncomingCall) && (null != secondIncomingCall)) { 68 | asyncTaskSignal.countDown(); 69 | } 70 | } 71 | 72 | @Override 73 | public void onError(String errorMessage) { 74 | assertTrue("Should successfully send a message. Error: " + errorMessage, false); 75 | asyncTaskSignal.countDown(); 76 | } 77 | }); 78 | } 79 | }); 80 | 81 | assertTrue("Test timed out", asyncTaskSignal.await(RespokeTestCase.TEST_TIMEOUT, TimeUnit.SECONDS)); 82 | assertTrue("sendMessage should call onSuccess", callbackDidSucceed); 83 | assertNotNull("Should have created a call object to represent the incoming call", firstIncomingCall); 84 | assertTrue("Should be the recipient of the call, not the caller", !firstIncomingCall.isCaller()); 85 | assertTrue("Should indicate call is with the endpoint that the call was started from", testbotEndpoint == firstIncomingCall.endpoint); 86 | assertTrue("Should indicate this is an audio-only call", firstIncomingCall.audioOnly); 87 | assertNotNull("Should have created a call object to represent the incoming call", secondIncomingCall); 88 | assertTrue("Should be the recipient of the call, not the caller", !secondIncomingCall.isCaller()); 89 | assertTrue("Should indicate call is with the endpoint that the call was started from", testbotEndpoint.getEndpointID().equals(secondIncomingCall.endpoint.getEndpointID())); 90 | assertTrue("Should indicate this is an audio-only call", secondIncomingCall.audioOnly); 91 | 92 | // the incoming call has been detected by both clients. Decline the call on the first client, and wait for the hangup signal to be received by the 2nd client 93 | asyncTaskSignal = new CountDownLatch(1); // Reset the countdown signal 94 | 95 | runTestOnUiThread(new Runnable() { 96 | @Override 97 | public void run() { 98 | firstIncomingCall.hangup(true); 99 | } 100 | }); 101 | 102 | assertTrue("Test timed out", asyncTaskSignal.await(RespokeTestCase.TEST_TIMEOUT, TimeUnit.SECONDS)); 103 | assertTrue("First client should have hung up the call", firstCallDidHangup); 104 | assertTrue("Second client should have been notified of the hangup", secondCallDidHangup); 105 | 106 | firstClient.disconnect(); 107 | Respoke.sharedInstance().unregisterClient(firstClient); 108 | secondClient.disconnect(); 109 | Respoke.sharedInstance().unregisterClient(secondClient); 110 | } 111 | 112 | 113 | // RespokeClient.Listener methods 114 | 115 | 116 | public void onConnect(RespokeClient sender) { 117 | asyncTaskSignal.countDown(); 118 | } 119 | 120 | 121 | public void onDisconnect(RespokeClient sender, boolean reconnecting) { 122 | 123 | } 124 | 125 | 126 | public void onError(RespokeClient sender, String errorMessage) { 127 | assertTrue("Should not produce any client errors during endpoint testing", false); 128 | assertTrue("Should be called in UI thread", RespokeTestCase.currentlyOnUIThread()); 129 | asyncTaskSignal.countDown(); 130 | } 131 | 132 | 133 | public void onCall(RespokeClient sender, RespokeCall call) { 134 | assertTrue("Should be called in UI thread", RespokeTestCase.currentlyOnUIThread()); 135 | 136 | if (sender == firstClient) { 137 | firstIncomingCall = call; 138 | firstIncomingCall.setListener(this); 139 | } else if (sender == secondClient) { 140 | secondIncomingCall = call; 141 | secondIncomingCall.setListener(this); 142 | } else { 143 | assertTrue("Unrecognized client received an incoming call", false); 144 | } 145 | 146 | if ((callbackDidSucceed) && (null != firstIncomingCall) && (null != secondIncomingCall)) { 147 | asyncTaskSignal.countDown(); 148 | } 149 | } 150 | 151 | 152 | public void onIncomingDirectConnection(RespokeDirectConnection directConnection, RespokeEndpoint endpoint) { 153 | // Not under test 154 | } 155 | 156 | 157 | public void onMessage(String message, RespokeEndpoint sender, RespokeGroup group, Date timestamp, Boolean didSend) { 158 | // Not under test 159 | } 160 | 161 | 162 | // RespokeEndpoint.Listener methods 163 | 164 | 165 | public void onMessage(String message, Date timestamp, RespokeEndpoint endpoint, boolean didSend) { 166 | // Not under test 167 | } 168 | 169 | 170 | public void onPresence(Object presence, RespokeEndpoint sender) { 171 | // Not under test 172 | } 173 | 174 | 175 | // RespokeCall.Listener methods 176 | 177 | 178 | public void onError(String errorMessage, RespokeCall sender) { 179 | assertTrue("Should perform a call without any errors. Error: " + errorMessage, false); 180 | assertTrue("Should be called in UI thread", RespokeTestCase.currentlyOnUIThread()); 181 | asyncTaskSignal.countDown(); 182 | } 183 | 184 | 185 | public void onHangup(RespokeCall sender) { 186 | assertTrue("Should be called in UI thread", RespokeTestCase.currentlyOnUIThread()); 187 | 188 | if (sender == firstIncomingCall) { 189 | firstCallDidHangup = true; 190 | } else if (sender == secondIncomingCall) { 191 | secondCallDidHangup = true; 192 | } else { 193 | assertTrue("Unrecognized call received a hangup signal", false); 194 | } 195 | 196 | if (firstCallDidHangup && secondCallDidHangup) { 197 | asyncTaskSignal.countDown(); 198 | } 199 | } 200 | 201 | 202 | public void onConnected(RespokeCall sender) { 203 | // Not under test 204 | } 205 | 206 | 207 | public void directConnectionAvailable(RespokeDirectConnection directConnection, RespokeEndpoint endpoint) { 208 | // Not under test 209 | } 210 | 211 | } 212 | -------------------------------------------------------------------------------- /respokeSDKTest/src/androidTest/java/com/digium/respokesdktest/functional/MessagingTests.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015, Digium, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under The MIT License found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | * For all details and documentation: https://www.respoke.io 9 | */ 10 | 11 | package com.digium.respokesdktest.functional; 12 | 13 | import com.digium.respokesdk.Respoke; 14 | import com.digium.respokesdk.RespokeCall; 15 | import com.digium.respokesdk.RespokeClient; 16 | import com.digium.respokesdk.RespokeDirectConnection; 17 | import com.digium.respokesdk.RespokeEndpoint; 18 | import com.digium.respokesdk.RespokeGroup; 19 | import com.digium.respokesdktest.RespokeTestCase; 20 | 21 | import java.util.Date; 22 | 23 | 24 | public class MessagingTests extends RespokeTestCase implements RespokeClient.Listener, RespokeEndpoint.Listener { 25 | 26 | private boolean callbackDidSucceed; 27 | private boolean endpointMessageDelivered; 28 | private boolean endpointMessageCopied; 29 | private boolean doCopySelf; 30 | private boolean clientMessageDelivered; 31 | private boolean clientMessageCopied; 32 | private RespokeEndpoint recipientEndpoint; 33 | private RespokeEndpoint senderEndpoint; 34 | 35 | 36 | public void testEndpointMessaging() { 37 | doCopySelf = false; 38 | runMessagingTest(); 39 | } 40 | 41 | public void testEndpointMessagingCCSelf() { 42 | doCopySelf = true; 43 | runMessagingTest(); 44 | } 45 | 46 | /** 47 | * This test will create two client instances with unique endpoint IDs. It will then send messages between the two to test functionality. 48 | */ 49 | 50 | public void runMessagingTest() { 51 | RespokeClient senderCCClient = null; 52 | RespokeEndpoint recipientCCEndpoint = null; 53 | 54 | // Create a client to test with 55 | final String recipientEndpointID = generateTestEndpointID(); 56 | final RespokeClient recipientClient = createTestClient(recipientEndpointID, this); 57 | 58 | // Create a second client to test with 59 | final String senderEndpointID = generateTestEndpointID(); 60 | final RespokeClient senderClient = createTestClient(senderEndpointID, this); 61 | 62 | // Build references to each of the endpoints 63 | recipientEndpoint = senderClient.getEndpoint(recipientEndpointID, false); 64 | assertNotNull("Should create endpoint instance", recipientEndpoint); 65 | recipientEndpoint.setListener(this); 66 | 67 | senderEndpoint = recipientClient.getEndpoint(senderEndpointID, false); 68 | assertNotNull("Should create endpoint instance", senderEndpoint); 69 | senderEndpoint.setListener(this); 70 | 71 | // Create new client and endpoint for copying self 72 | if (doCopySelf) 73 | { 74 | senderCCClient = createTestClient(senderEndpointID, this); 75 | assertNotNull("Should create sender CC client", senderCCClient); 76 | 77 | recipientCCEndpoint = senderCCClient.getEndpoint(recipientEndpointID, false); 78 | assertNotNull("Should create endpoint instance", recipientCCEndpoint); 79 | recipientCCEndpoint.setListener(this); 80 | } 81 | 82 | endpointMessageCopied = false; 83 | endpointMessageDelivered = false; 84 | clientMessageCopied = false; 85 | clientMessageDelivered = false; 86 | asyncTaskDone = false; 87 | callbackDidSucceed = false; 88 | recipientEndpoint.sendMessage(TEST_MESSAGE, false, doCopySelf, new Respoke.TaskCompletionListener() { 89 | @Override 90 | public void onSuccess() { 91 | assertTrue("Should be called in UI thread", RespokeTestCase.currentlyOnUIThread()); 92 | callbackDidSucceed = true; 93 | tryCompleteTask(); 94 | } 95 | 96 | @Override 97 | public void onError(String errorMessage) { 98 | assertTrue("Should successfully send a message", false); 99 | } 100 | }); 101 | 102 | assertTrue("Test timed out", waitForCompletion(RespokeTestCase.TEST_TIMEOUT)); 103 | assertTrue("sendMessage should call successHandler", callbackDidSucceed); 104 | assertTrue("Should call RespokeEndpoint.onMessage listener when a message is received", endpointMessageDelivered); 105 | assertTrue("Should call RespokeClient.onMessage listener when a message is received", clientMessageDelivered); 106 | 107 | if (doCopySelf) { 108 | assertTrue("Should call RespokeEndpoint.onMessage listener with didSend=false when a message is copied", endpointMessageCopied); 109 | assertTrue("Should call RespokeClient.onMessage listener with didSend=false when a message is copied", clientMessageCopied); 110 | } 111 | } 112 | 113 | public void tryCompleteTask() { 114 | asyncTaskDone = callbackDidSucceed && endpointMessageDelivered && clientMessageDelivered; 115 | if (doCopySelf) { 116 | asyncTaskDone = asyncTaskDone && endpointMessageCopied && clientMessageCopied; 117 | } 118 | } 119 | 120 | // RespokeClient.Listener methods 121 | 122 | 123 | public void onConnect(RespokeClient sender) { 124 | asyncTaskDone = true; 125 | } 126 | 127 | 128 | public void onDisconnect(RespokeClient sender, boolean reconnecting) { 129 | 130 | } 131 | 132 | 133 | public void onError(RespokeClient sender, String errorMessage) { 134 | assertTrue("Should not produce any client errors during endpoint testing", false); 135 | asyncTaskDone = true; 136 | } 137 | 138 | 139 | public void onCall(RespokeClient sender, RespokeCall call) { 140 | // Not under test 141 | } 142 | 143 | 144 | public void onIncomingDirectConnection(RespokeDirectConnection directConnection, RespokeEndpoint endpoint) { 145 | // Not under test 146 | } 147 | 148 | 149 | public void onMessage(String message, RespokeEndpoint endpoint, RespokeGroup group, Date timestamp, Boolean didSend) { 150 | assertTrue("Should be called in UI thread", RespokeTestCase.currentlyOnUIThread()); 151 | assertTrue("Message sent should be the message received", message.equals(TEST_MESSAGE)); 152 | assertNotNull("Should include a timestamp", timestamp); 153 | 154 | if (!doCopySelf) { 155 | assertTrue("Endpoint should always be the sender", didSend); 156 | } 157 | 158 | assertNotNull("Endpoint passed to RespokeClient.onMessage listener should not be null", endpoint); 159 | final String onMessageEndpointID = endpoint.getEndpointID(); 160 | 161 | if (didSend) { 162 | assertTrue("Should indicate correct sender endpointID", onMessageEndpointID.equals(senderEndpoint.getEndpointID())); 163 | clientMessageDelivered = true; 164 | } else { 165 | assertTrue("Should indicate correct sender endpointID", onMessageEndpointID.equals(recipientEndpoint.getEndpointID())); 166 | clientMessageCopied = true; 167 | } 168 | 169 | tryCompleteTask(); 170 | } 171 | 172 | 173 | // RespokeEndpoint.Listener methods 174 | 175 | 176 | public void onMessage(String message, Date timestamp, RespokeEndpoint endpoint, boolean didSend) { 177 | assertTrue("Should be called in UI thread", RespokeTestCase.currentlyOnUIThread()); 178 | assertTrue("Message sent should be the message received", message.equals(TEST_MESSAGE)); 179 | assertNotNull("Should include a timestamp", timestamp); 180 | 181 | if (!doCopySelf) { 182 | assertTrue("Endpoint should always be the sender", didSend); 183 | } 184 | 185 | assertNotNull("Endpoint passed to RespokeEndpoint.onMessage listener should not be null", endpoint); 186 | final String onMessageEndpointID = endpoint.getEndpointID(); 187 | 188 | if (didSend) { 189 | assertTrue("Should indicate correct sender endpointID", onMessageEndpointID.equals(senderEndpoint.getEndpointID())); 190 | endpointMessageDelivered = true; 191 | } else { 192 | assertTrue("Should indicate correct sender endpointID", onMessageEndpointID.equals(recipientEndpoint.getEndpointID())); 193 | endpointMessageCopied = true; 194 | } 195 | 196 | tryCompleteTask(); 197 | } 198 | 199 | 200 | public void onPresence(Object presence, RespokeEndpoint sender) { 201 | // Not under test 202 | } 203 | 204 | 205 | } 206 | -------------------------------------------------------------------------------- /respokeSDKTest/src/androidTest/java/com/digium/respokesdktest/functional/PresenceTests.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015, Digium, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under The MIT License found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | * For all details and documentation: https://www.respoke.io 9 | */ 10 | 11 | package com.digium.respokesdktest.functional; 12 | 13 | import com.digium.respokesdk.Respoke; 14 | import com.digium.respokesdk.RespokeCall; 15 | import com.digium.respokesdk.RespokeClient; 16 | import com.digium.respokesdk.RespokeDirectConnection; 17 | import com.digium.respokesdk.RespokeEndpoint; 18 | import com.digium.respokesdk.RespokeGroup; 19 | import com.digium.respokesdktest.RespokeTestCase; 20 | 21 | import java.util.ArrayList; 22 | import java.util.Date; 23 | import java.util.HashMap; 24 | 25 | 26 | public class PresenceTests extends RespokeTestCase implements RespokeClient.Listener, RespokeClient.ResolvePresenceListener, RespokeEndpoint.Listener { 27 | 28 | private boolean callbackDidSucceed; 29 | private boolean remotePresenceReceived; 30 | private Object customPresenceResolution; 31 | private Integer presenceCallBackCount; 32 | private boolean countingTest; 33 | 34 | 35 | public void testCustomResolveMethod() { 36 | // Create a client to test with 37 | final String testEndpointID = generateTestEndpointID(); 38 | final RespokeClient firstClient = createTestClient(testEndpointID, this); 39 | 40 | // Create a second client to test with 41 | final String secondTestEndpointID = generateTestEndpointID(); 42 | final RespokeClient secondClient = createTestClient(secondTestEndpointID, this); 43 | 44 | // The custom resolve function will always return this random value 45 | customPresenceResolution = Respoke.makeGUID(); 46 | secondClient.setResolvePresenceListener(this); 47 | 48 | // Build references to each of the endpoints 49 | RespokeEndpoint firstEndpoint = secondClient.getEndpoint(testEndpointID, false); 50 | assertNotNull("Should create endpoint instance", firstEndpoint); 51 | firstEndpoint.setListener(this); 52 | 53 | RespokeEndpoint secondEndpoint = firstClient.getEndpoint(secondTestEndpointID, false); 54 | assertNotNull("Should create endpoint instance", secondEndpoint); 55 | 56 | asyncTaskDone = false; 57 | remotePresenceReceived = false; 58 | callbackDidSucceed = false; 59 | Object expectedRemotePresence = new HashMap(); 60 | ((HashMap) expectedRemotePresence).put("presence", "nacho presence2"); 61 | firstClient.setPresence(expectedRemotePresence, new Respoke.TaskCompletionListener() { 62 | @Override 63 | public void onSuccess() { 64 | assertTrue("Should be called in UI thread", RespokeTestCase.currentlyOnUIThread()); 65 | callbackDidSucceed = true; 66 | asyncTaskDone = remotePresenceReceived; 67 | } 68 | 69 | @Override 70 | public void onError(String errorMessage) { 71 | assertTrue("Should successfully set presence updates. Error: " + errorMessage, false); 72 | } 73 | }); 74 | 75 | assertTrue("Test timed out", waitForCompletion(RespokeTestCase.TEST_TIMEOUT)); 76 | 77 | assertTrue("Resolved presence should be a string", firstEndpoint.presence instanceof String); 78 | assertTrue("Resolved presence should be correct", customPresenceResolution.equals(firstEndpoint.presence)); 79 | } 80 | 81 | 82 | public void testOfflineEndpointPresence() { 83 | // Create a client to test with 84 | final String testEndpointID = generateTestEndpointID(); 85 | final RespokeClient client = createTestClient(testEndpointID, this); 86 | 87 | // Create a second random endpoint id to test with 88 | final String secondTestEndpointID = generateTestEndpointID(); 89 | 90 | // Get an endpoint object to represent the second endpoint which is not online 91 | customPresenceResolution = "unavailable"; 92 | RespokeEndpoint endpoint = client.getEndpoint(secondTestEndpointID, false); 93 | endpoint.setListener(this); 94 | 95 | assertNull("Presence should be null if the client has not registered for presence updates yet", endpoint.presence); 96 | 97 | asyncTaskDone = false; 98 | callbackDidSucceed = true; // Tell the onPresence listener method to signal asyncTaskDone = true right away 99 | assertTrue("Test timed out", waitForCompletion(RespokeTestCase.TEST_TIMEOUT)); 100 | 101 | assertNotNull(endpoint); 102 | assertNotNull("Presence should not be null", endpoint.presence); 103 | assertTrue("Presence should be unavailable", endpoint.presence.equals("unavailable")); 104 | } 105 | 106 | 107 | public void testPresenceRegistrationQueueing() { 108 | // Create a client to test with 109 | final String testEndpointID = generateTestEndpointID(); 110 | final RespokeClient client = createTestClient(testEndpointID, this); 111 | 112 | // Create a second random endpoint id to test with 113 | final String secondTestEndpointID = generateTestEndpointID(); 114 | 115 | countingTest = true; 116 | presenceCallBackCount = 0; 117 | RespokeEndpoint endpoint = client.getEndpoint(secondTestEndpointID, false); 118 | endpoint.setListener(this); 119 | 120 | // call getEndpoint several times again 121 | client.getEndpoint(secondTestEndpointID, false); 122 | client.getEndpoint(secondTestEndpointID, false); 123 | client.getEndpoint(secondTestEndpointID, false); 124 | client.getEndpoint(secondTestEndpointID, false); 125 | client.getEndpoint(secondTestEndpointID, false); 126 | client.getEndpoint(secondTestEndpointID, false); 127 | client.getEndpoint(secondTestEndpointID, false); 128 | 129 | // A single presence registration Runnable should have been queued, count the number of times the call back fires over the next few seconds 130 | asyncTaskDone = false; 131 | waitForCompletion(10); 132 | 133 | assertTrue("The presence callback should have occurred less than 3 times, but actually occurred " + presenceCallBackCount + " times.", presenceCallBackCount < 3); 134 | } 135 | 136 | 137 | // RespokeClient.Listener methods 138 | 139 | 140 | public void onConnect(RespokeClient sender) { 141 | asyncTaskDone = true; 142 | } 143 | 144 | 145 | public void onDisconnect(RespokeClient sender, boolean reconnecting) { 146 | 147 | } 148 | 149 | 150 | public void onError(RespokeClient sender, String errorMessage) { 151 | assertTrue("Should not produce any client errors during endpoint testing. Error: " + errorMessage, false); 152 | asyncTaskDone = true; 153 | } 154 | 155 | 156 | public void onCall(RespokeClient sender, RespokeCall call) { 157 | // Not under test 158 | } 159 | 160 | 161 | public void onIncomingDirectConnection(RespokeDirectConnection directConnection, RespokeEndpoint endpoint) { 162 | // Not under test 163 | } 164 | 165 | 166 | public void onMessage(String message, RespokeEndpoint endpoint, RespokeGroup group, Date timestamp, Boolean didSend) { 167 | // Not under test 168 | } 169 | 170 | 171 | // RespokeEndpoint.Listener methods 172 | 173 | 174 | public void onMessage(String message, Date timestamp, RespokeEndpoint endpoint, boolean didSend) { 175 | // Not under test 176 | } 177 | 178 | 179 | public void onPresence(Object presence, RespokeEndpoint sender) { 180 | assertTrue("Should be called in UI thread", RespokeTestCase.currentlyOnUIThread()); 181 | assertNotNull("Remote presence should not be null", presence); 182 | assertTrue("Remote presence should be a string", presence instanceof String); 183 | 184 | if (countingTest) { 185 | presenceCallBackCount++; 186 | } else { 187 | assertTrue("Resolved presence should be correct. Expected " + customPresenceResolution + " but received " + presence, customPresenceResolution.equals((String) presence)); 188 | remotePresenceReceived = true; 189 | asyncTaskDone = callbackDidSucceed; 190 | } 191 | } 192 | 193 | 194 | // RespokeClient.ResolvePresenceListener methods 195 | 196 | 197 | public Object resolvePresence(ArrayList presenceArray) { 198 | assertTrue("presence array should contain the correct number of values", 1 == presenceArray.size()); 199 | return customPresenceResolution; 200 | } 201 | 202 | 203 | } 204 | -------------------------------------------------------------------------------- /respokeSDKTest/src/androidTest/java/com/digium/respokesdktest/unit/RespokeCallTests.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015, Digium, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under The MIT License found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | * For all details and documentation: https://www.respoke.io 9 | */ 10 | 11 | package com.digium.respokesdktest.unit; 12 | 13 | import com.digium.respokesdk.RespokeCall; 14 | import com.digium.respokesdktest.RespokeTestCase; 15 | 16 | import org.json.JSONException; 17 | import org.json.JSONObject; 18 | 19 | 20 | public class RespokeCallTests extends RespokeTestCase { 21 | 22 | 23 | public void testSdpHasVideo() { 24 | try { 25 | JSONObject audioOnlySDP = new JSONObject("{\"type\":\"offer\",\"sdp\":\"v=0\\r\\no=- 6116182927273193777 2 IN IP4 127.0.0.1\\r\\ns=-\\r\\nt=0 0\\r\\na=group:BUNDLE audio\\r\\na=msid-semantic: WMS HxDm7ZdgkDE9aJpjeytJhT3stMgXxapdk8DY\\r\\nm=audio 9 RTP\\/SAVPF 111 103 104 9 0 8 106 105 13 126\\r\\nc=IN IP4 0.0.0.0\\r\\na=rtcp:9 IN IP4 0.0.0.0\\r\\na=ice-ufrag:AdddxlRYoMW8UICE\\r\\na=ice-pwd:BVFvVAKconjagjwsLlc8eoOq\\r\\na=fingerprint:sha-256 21:26:14:08:05:AD:D0:07:2F:8A:CC:D8:40:A9:A5:B9:72:5D:62:6D:83:1A:AF:76:35:F3:EA:4C:12:28:37:13\\r\\na=setup:actpass\\r\\na=mid:audio\\r\\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\\r\\na=extmap:3 http:\\/\\/www.webrtc.org\\/experiments\\/rtp-hdrext\\/abs-send-time\\r\\na=sendrecv\\r\\na=rtcp-mux\\r\\na=rtpmap:111 opus\\/48000\\/2\\r\\na=fmtp:111 minptime=10; useinbandfec=1\\r\\na=rtpmap:103 ISAC\\/16000\\r\\na=rtpmap:104 ISAC\\/32000\\r\\na=rtpmap:9 G722\\/8000\\r\\na=rtpmap:0 PCMU\\/8000\\r\\na=rtpmap:8 PCMA\\/8000\\r\\na=rtpmap:106 CN\\/32000\\r\\na=rtpmap:105 CN\\/16000\\r\\na=rtpmap:13 CN\\/8000\\r\\na=rtpmap:126 telephone-event\\/8000\\r\\na=maxptime:60\\r\\na=ssrc:3260245544 cname:EU2GZuWLO3m0+MJp\\r\\na=ssrc:3260245544 msid:HxDm7ZdgkDE9aJpjeytJhT3stMgXxapdk8DY 4c809bee-6b56-438a-ac90-d7053c0c4770\\r\\na=ssrc:3260245544 mslabel:HxDm7ZdgkDE9aJpjeytJhT3stMgXxapdk8DY\\r\\na=ssrc:3260245544 label:4c809bee-6b56-438a-ac90-d7053c0c4770\\r\\n\"}"); 26 | JSONObject videoSDP = new JSONObject("{\"type\":\"offer\",\"sdp\":\"v=0\\r\\no=- 2269581771901782266 2 IN IP4 127.0.0.1\\r\\ns=-\\r\\nt=0 0\\r\\na=group:BUNDLE audio video\\r\\na=msid-semantic: WMS g7oxcNrUqwuwbUOvVLFMKO5OvWTOi8ee8ND6\\r\\nm=audio 9 RTP\\/SAVPF 111 103 104 9 0 8 106 105 13 126\\r\\nc=IN IP4 0.0.0.0\\r\\na=rtcp:9 IN IP4 0.0.0.0\\r\\na=ice-ufrag:0\\/4ED6Lq6RjLjTek\\r\\na=ice-pwd:BQdH\\/GJRrIOmFnrIEn0NRpzb\\r\\na=fingerprint:sha-256 21:26:14:08:05:AD:D0:07:2F:8A:CC:D8:40:A9:A5:B9:72:5D:62:6D:83:1A:AF:76:35:F3:EA:4C:12:28:37:13\\r\\na=setup:actpass\\r\\na=mid:audio\\r\\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\\r\\na=extmap:3 http:\\/\\/www.webrtc.org\\/experiments\\/rtp-hdrext\\/abs-send-time\\r\\na=sendrecv\\r\\na=rtcp-mux\\r\\na=rtpmap:111 opus\\/48000\\/2\\r\\na=fmtp:111 minptime=10; useinbandfec=1\\r\\na=rtpmap:103 ISAC\\/16000\\r\\na=rtpmap:104 ISAC\\/32000\\r\\na=rtpmap:9 G722\\/8000\\r\\na=rtpmap:0 PCMU\\/8000\\r\\na=rtpmap:8 PCMA\\/8000\\r\\na=rtpmap:106 CN\\/32000\\r\\na=rtpmap:105 CN\\/16000\\r\\na=rtpmap:13 CN\\/8000\\r\\na=rtpmap:126 telephone-event\\/8000\\r\\na=maxptime:60\\r\\na=ssrc:2445183102 cname:QOl3hq3Z4\\/FooJ+H\\r\\na=ssrc:2445183102 msid:g7oxcNrUqwuwbUOvVLFMKO5OvWTOi8ee8ND6 24650e1f-14f9-46ea-ba5a-6f5f86262d31\\r\\na=ssrc:2445183102 mslabel:g7oxcNrUqwuwbUOvVLFMKO5OvWTOi8ee8ND6\\r\\na=ssrc:2445183102 label:24650e1f-14f9-46ea-ba5a-6f5f86262d31\\r\\nm=video 9 RTP\\/SAVPF 100 116 117 96\\r\\nc=IN IP4 0.0.0.0\\r\\na=rtcp:9 IN IP4 0.0.0.0\\r\\na=ice-ufrag:0\\/4ED6Lq6RjLjTek\\r\\na=ice-pwd:BQdH\\/GJRrIOmFnrIEn0NRpzb\\r\\na=fingerprint:sha-256 21:26:14:08:05:AD:D0:07:2F:8A:CC:D8:40:A9:A5:B9:72:5D:62:6D:83:1A:AF:76:35:F3:EA:4C:12:28:37:13\\r\\na=setup:actpass\\r\\na=mid:video\\r\\na=extmap:2 urn:ietf:params:rtp-hdrext:toffset\\r\\na=extmap:3 http:\\/\\/www.webrtc.org\\/experiments\\/rtp-hdrext\\/abs-send-time\\r\\na=sendrecv\\r\\na=rtcp-mux\\r\\na=rtpmap:100 VP8\\/90000\\r\\na=rtcp-fb:100 ccm fir\\r\\na=rtcp-fb:100 nack\\r\\na=rtcp-fb:100 nack pli\\r\\na=rtcp-fb:100 goog-remb\\r\\na=rtpmap:116 red\\/90000\\r\\na=rtpmap:117 ulpfec\\/90000\\r\\na=rtpmap:96 rtx\\/90000\\r\\na=fmtp:96 apt=100\\r\\na=ssrc-group:FID 3991731061 1826556079\\r\\na=ssrc:3991731061 cname:QOl3hq3Z4\\/FooJ+H\\r\\na=ssrc:3991731061 msid:g7oxcNrUqwuwbUOvVLFMKO5OvWTOi8ee8ND6 b8e33db0-d73b-4695-ade9-9fea843a0111\\r\\na=ssrc:3991731061 mslabel:g7oxcNrUqwuwbUOvVLFMKO5OvWTOi8ee8ND6\\r\\na=ssrc:3991731061 label:b8e33db0-d73b-4695-ade9-9fea843a0111\\r\\na=ssrc:1826556079 cname:QOl3hq3Z4\\/FooJ+H\\r\\na=ssrc:1826556079 msid:g7oxcNrUqwuwbUOvVLFMKO5OvWTOi8ee8ND6 b8e33db0-d73b-4695-ade9-9fea843a0111\\r\\na=ssrc:1826556079 mslabel:g7oxcNrUqwuwbUOvVLFMKO5OvWTOi8ee8ND6\\r\\na=ssrc:1826556079 label:b8e33db0-d73b-4695-ade9-9fea843a0111\\r\\n\"}"); 27 | 28 | assertTrue("Should indicate that SDP has video", RespokeCall.sdpHasVideo(videoSDP)); 29 | assertTrue("Should indicate that SDP does not have video", !RespokeCall.sdpHasVideo(audioOnlySDP)); 30 | } catch (JSONException e) { 31 | assertTrue("Could not build test SDPs", false); 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /respokeSDKTest/src/androidTest/java/com/digium/respokesdktest/unit/RespokeClientTests.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015, Digium, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under The MIT License found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | * For all details and documentation: https://www.respoke.io 9 | */ 10 | 11 | package com.digium.respokesdktest.unit; 12 | 13 | import com.digium.respokesdk.Respoke; 14 | import com.digium.respokesdk.RespokeClient; 15 | import com.digium.respokesdk.RespokeGroup; 16 | import com.digium.respokesdktest.RespokeTestCase; 17 | 18 | import java.util.ArrayList; 19 | 20 | 21 | public class RespokeClientTests extends RespokeTestCase { 22 | 23 | private boolean callbackDidSucceed; 24 | 25 | 26 | public void testUnconnectedClientBehavior() { 27 | RespokeClient client = Respoke.sharedInstance().createClient(getContext()); 28 | assertNotNull(client); 29 | 30 | assertFalse("Should indicate not connected", client.isConnected()); 31 | assertNull("Local endpoint should be nil if never connected", client.getEndpointID()); 32 | 33 | callbackDidSucceed = false; 34 | asyncTaskDone = false; 35 | 36 | ArrayList groupList = new ArrayList(); 37 | groupList.add("newGroupID"); 38 | client.joinGroups(groupList, new RespokeClient.JoinGroupCompletionListener() { 39 | @Override 40 | public void onSuccess(final ArrayList groupList) { 41 | assertTrue("Should not call success handler", false); 42 | asyncTaskDone = true; 43 | } 44 | 45 | @Override 46 | public void onError(String errorMessage) { 47 | assertTrue("Should be called in UI thread", RespokeTestCase.currentlyOnUIThread()); 48 | callbackDidSucceed = true; 49 | assertEquals("Can't complete request when not connected. Please reconnect!", errorMessage); 50 | asyncTaskDone = true; 51 | } 52 | }); 53 | 54 | assertTrue("Test timed out", waitForCompletion(RespokeTestCase.TEST_TIMEOUT)); 55 | assertTrue("Did not call error handler when not connected", callbackDidSucceed); 56 | 57 | // Should behave when calling disconnect on a client that is not connected (i.e. don't crash) 58 | 59 | client.disconnect(); 60 | 61 | assertNull("Should return a nil presence when it has not been set", client.getPresence()); 62 | } 63 | 64 | 65 | public void testConnectParameterErrorHandling() { 66 | RespokeClient client = Respoke.sharedInstance().createClient(getContext()); 67 | assertNotNull(client); 68 | 69 | // Test bad parameters 70 | 71 | callbackDidSucceed = false; 72 | asyncTaskDone = false; 73 | 74 | client.connect("myEndpointID", null, false, null, getContext(), new RespokeClient.ConnectCompletionListener(){ 75 | @Override 76 | public void onError(String errorMessage) { 77 | assertTrue("Should be called in UI thread", RespokeTestCase.currentlyOnUIThread()); 78 | callbackDidSucceed = true; 79 | assertEquals("AppID and endpointID must be specified", errorMessage); 80 | asyncTaskDone = true; 81 | } 82 | }); 83 | 84 | assertTrue("Test timed out", waitForCompletion(RespokeTestCase.TEST_TIMEOUT)); 85 | assertTrue("Did not call error handler", callbackDidSucceed); 86 | 87 | callbackDidSucceed = false; 88 | asyncTaskDone = false; 89 | 90 | client.connect(null, "anAwesomeAppID", false, null, getContext(), new RespokeClient.ConnectCompletionListener(){ 91 | @Override 92 | public void onError(String errorMessage) { 93 | assertTrue("Should be called in UI thread", RespokeTestCase.currentlyOnUIThread()); 94 | callbackDidSucceed = true; 95 | assertEquals("AppID and endpointID must be specified", errorMessage); 96 | asyncTaskDone = true; 97 | } 98 | }); 99 | 100 | assertTrue("Test timed out", waitForCompletion(RespokeTestCase.TEST_TIMEOUT)); 101 | assertTrue("Did not call error handler", callbackDidSucceed); 102 | 103 | callbackDidSucceed = false; 104 | asyncTaskDone = false; 105 | 106 | client.connect("", "", false, null, getContext(), new RespokeClient.ConnectCompletionListener(){ 107 | @Override 108 | public void onError(String errorMessage) { 109 | assertTrue("Should be called in UI thread", RespokeTestCase.currentlyOnUIThread()); 110 | callbackDidSucceed = true; 111 | assertEquals("AppID and endpointID must be specified", errorMessage); 112 | asyncTaskDone = true; 113 | } 114 | }); 115 | 116 | assertTrue("Test timed out", waitForCompletion(RespokeTestCase.TEST_TIMEOUT)); 117 | assertTrue("Did not call error handler", callbackDidSucceed); 118 | 119 | 120 | // Test more bad parameters 121 | 122 | 123 | callbackDidSucceed = false; 124 | asyncTaskDone = false; 125 | 126 | client.connect(null, null, getContext(), new RespokeClient.ConnectCompletionListener() { 127 | @Override 128 | public void onError(String errorMessage) { 129 | assertTrue("Should be called in UI thread", RespokeTestCase.currentlyOnUIThread()); 130 | callbackDidSucceed = true; 131 | assertEquals("TokenID must be specified", errorMessage); 132 | asyncTaskDone = true; 133 | } 134 | }); 135 | 136 | assertTrue("Test timed out", waitForCompletion(RespokeTestCase.TEST_TIMEOUT)); 137 | assertTrue("Did not call error handler", callbackDidSucceed); 138 | 139 | callbackDidSucceed = false; 140 | asyncTaskDone = false; 141 | 142 | client.connect("", null, getContext(), new RespokeClient.ConnectCompletionListener() { 143 | @Override 144 | public void onError(String errorMessage) { 145 | assertTrue("Should be called in UI thread", RespokeTestCase.currentlyOnUIThread()); 146 | callbackDidSucceed = true; 147 | assertEquals("TokenID must be specified", errorMessage); 148 | asyncTaskDone = true; 149 | } 150 | }); 151 | 152 | assertTrue("Test timed out", waitForCompletion(RespokeTestCase.TEST_TIMEOUT)); 153 | assertTrue("Did not call error handler", callbackDidSucceed); 154 | 155 | 156 | // Should fail silently if no error handler is specified (i.e. don't crash) 157 | 158 | client.connect("myEndpointID", null, false, null, getContext(), null); 159 | client.connect("", null, getContext(), null); 160 | } 161 | 162 | 163 | public void testGroupSearch() { 164 | RespokeClient client = Respoke.sharedInstance().createClient(getContext()); 165 | assertNotNull(client); 166 | 167 | assertNull("Should return null for a null group", client.getGroup(null)); 168 | assertNull("Should return null for a group that does not exist", client.getGroup("someGroup")); 169 | } 170 | 171 | 172 | } 173 | -------------------------------------------------------------------------------- /respokeSDKTest/src/androidTest/java/com/digium/respokesdktest/unit/RespokeEndpointTests.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015, Digium, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under The MIT License found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | * For all details and documentation: https://www.respoke.io 9 | */ 10 | 11 | package com.digium.respokesdktest.unit; 12 | 13 | import android.app.Application; 14 | import android.test.ApplicationTestCase; 15 | 16 | import com.digium.respokesdk.Respoke; 17 | import com.digium.respokesdk.RespokeCall; 18 | import com.digium.respokesdk.RespokeClient; 19 | import com.digium.respokesdk.RespokeConnection; 20 | import com.digium.respokesdk.RespokeDirectConnection; 21 | import com.digium.respokesdk.RespokeEndpoint; 22 | import com.digium.respokesdktest.RespokeTestCase; 23 | 24 | import java.util.ArrayList; 25 | import java.util.Date; 26 | import java.util.Dictionary; 27 | import java.util.Enumeration; 28 | import java.util.HashMap; 29 | import java.util.concurrent.CountDownLatch; 30 | import java.util.concurrent.TimeUnit; 31 | 32 | 33 | public class RespokeEndpointTests extends RespokeTestCase implements RespokeEndpoint.Listener, RespokeClient.ResolvePresenceListener { 34 | 35 | private boolean callbackDidSucceed; 36 | private RespokeEndpoint presenceTestEndpoint; 37 | private Object callbackPresence; 38 | private Object customPresenceResolution; 39 | private static final String TEST_MESSAGE = "This is a test message!"; 40 | 41 | 42 | public void testUnconnectedEndpointBehavior() { 43 | RespokeClient client = Respoke.sharedInstance().createClient(getContext()); 44 | assertNotNull(client); 45 | 46 | assertNull("Should return nil if no endpoint exists", client.getEndpoint("someEndpointID", true)); 47 | 48 | RespokeEndpoint endpoint = client.getEndpoint("someEndpointID", false); 49 | assertNotNull("Should create an endpoint instance if it does not exist and it so commanded to", endpoint); 50 | assertEquals("Should have the correct endpoint ID", "someEndpointID", endpoint.getEndpointID()); 51 | 52 | ArrayList connections = endpoint.getConnections(); 53 | assertNotNull("Should return an empty list of connections when not connected", connections); 54 | assertTrue("Should return an empty list of connections when not connected", 0 == connections.size()); 55 | 56 | callbackDidSucceed = false; 57 | asyncTaskDone = false; 58 | endpoint.sendMessage("Hi there!", false, true, new Respoke.TaskCompletionListener(){ 59 | @Override 60 | public void onSuccess() { 61 | assertTrue("Should not call success handler", false); 62 | asyncTaskDone = true; 63 | } 64 | 65 | @Override 66 | public void onError(String errorMessage) { 67 | assertTrue("Should be called in UI thread", RespokeTestCase.currentlyOnUIThread()); 68 | callbackDidSucceed = true; 69 | assertEquals("Can't complete request when not connected. Please reconnect!", errorMessage); 70 | asyncTaskDone = true; 71 | } 72 | }); 73 | 74 | assertTrue("Test timed out", waitForCompletion(RespokeTestCase.TEST_TIMEOUT)); 75 | assertTrue("Should call error handler", callbackDidSucceed); 76 | assertNull("Should not create a call object when not connected", endpoint.startCall(null, getContext(), null,false)); 77 | } 78 | 79 | 80 | public void testPresence() { 81 | RespokeClient client = Respoke.sharedInstance().createClient(getContext()); 82 | assertNotNull(client); 83 | 84 | assertNull("Should return nil if no endpoint exists", client.getEndpoint("someEndpointID", true)); 85 | 86 | presenceTestEndpoint = client.getEndpoint("someEndpointID", false); 87 | assertNotNull("Should create an endpoint instance if it does not exist and it so commanded to", presenceTestEndpoint); 88 | presenceTestEndpoint.setListener(this); 89 | 90 | assertNull("Presence should initially be null", presenceTestEndpoint.presence); 91 | 92 | 93 | // Test presence with no connections 94 | 95 | 96 | callbackDidSucceed = false; 97 | callbackPresence = null; 98 | asyncTaskDone = false; 99 | presenceTestEndpoint.resolvePresence(); 100 | assertTrue("Test timed out", waitForCompletion(RespokeTestCase.TEST_TIMEOUT)); 101 | assertTrue("Presence delegate should be called", callbackDidSucceed); 102 | assertTrue("Should resolve to the correct presence value", "unavailable".equals(callbackPresence)); 103 | 104 | 105 | // Test presence with one connection 106 | 107 | 108 | RespokeConnection connection = new RespokeConnection(Respoke.makeGUID(), presenceTestEndpoint); 109 | assertNotNull("Should create connection", connection); 110 | assertNull("Presence should initially be null", connection.presence); 111 | presenceTestEndpoint.connections.add(connection); 112 | 113 | callbackDidSucceed = false; 114 | callbackPresence = null; 115 | asyncTaskDone = false; 116 | presenceTestEndpoint.resolvePresence(); 117 | assertTrue("Test timed out", waitForCompletion(RespokeTestCase.TEST_TIMEOUT)); 118 | assertTrue("Presence delegate should be called", callbackDidSucceed); 119 | assertTrue("Should resolve to the correct presence value", "unavailable".equals(callbackPresence)); 120 | 121 | ArrayList options = new ArrayList(); 122 | options.add("chat"); 123 | options.add("available"); 124 | options.add("away"); 125 | options.add("dnd"); 126 | options.add("xa"); 127 | options.add("unavailable"); 128 | 129 | for (String eachPresence : options) { 130 | connection.presence = eachPresence; 131 | 132 | callbackDidSucceed = false; 133 | callbackPresence = null; 134 | asyncTaskDone = false; 135 | presenceTestEndpoint.resolvePresence(); 136 | assertTrue("Test timed out", waitForCompletion(RespokeTestCase.TEST_TIMEOUT)); 137 | assertTrue("Presence listener should be called", callbackDidSucceed); 138 | assertTrue("Expected presence to be [" + eachPresence + "] but found [" + callbackPresence + "]", eachPresence.equals(connection.presence)); 139 | assertTrue("Resolved endpoint presence should match the connections", eachPresence.equals(presenceTestEndpoint.presence)); 140 | } 141 | 142 | 143 | // Test presence with 2 connections 144 | 145 | 146 | RespokeConnection secondConnection = new RespokeConnection(Respoke.makeGUID(), presenceTestEndpoint); 147 | assertNotNull("Should create connection", secondConnection); 148 | assertNull("Presence should initially be null", secondConnection.presence); 149 | presenceTestEndpoint.connections.add(secondConnection); 150 | 151 | for (Integer ii = 0; ii < options.size(); ii++) { 152 | String firstPresence = options.get(ii); 153 | 154 | for (Integer jj = 0; jj < options.size(); jj++) { 155 | String secondPresence = options.get(jj); 156 | 157 | connection.presence = firstPresence; 158 | secondConnection.presence = secondPresence; 159 | 160 | String expectedPresence = null; 161 | 162 | if (ii <= jj) { 163 | expectedPresence = firstPresence; 164 | } else { 165 | expectedPresence = secondPresence; 166 | } 167 | 168 | callbackDidSucceed = false; 169 | callbackPresence = null; 170 | asyncTaskDone = false; 171 | presenceTestEndpoint.resolvePresence(); 172 | assertTrue("Test timed out", waitForCompletion(RespokeTestCase.TEST_TIMEOUT)); 173 | assertTrue("Presence delegate should be called", callbackDidSucceed); 174 | assertTrue("Expected presence to be [" + expectedPresence + "] but found [" + callbackPresence + "]", expectedPresence.equals(callbackPresence)); 175 | assertTrue("Resolved endpoint presence should match the connections", expectedPresence.equals(presenceTestEndpoint.presence)); 176 | } 177 | } 178 | } 179 | 180 | 181 | public void testCustomPresence() { 182 | RespokeClient client = Respoke.sharedInstance().createClient(getContext()); 183 | assertNotNull(client); 184 | client.setResolvePresenceListener(this); 185 | 186 | presenceTestEndpoint = client.getEndpoint("someEndpointID", false); 187 | assertNotNull("Should create an endpoint instance if it does not exist and it so commanded to", presenceTestEndpoint); 188 | presenceTestEndpoint.setListener(this); 189 | 190 | RespokeConnection connection1 = new RespokeConnection(Respoke.makeGUID(), presenceTestEndpoint); 191 | assertNotNull("Should create connection", connection1); 192 | presenceTestEndpoint.connections.add(connection1); 193 | 194 | RespokeConnection connection2 = new RespokeConnection(Respoke.makeGUID(), presenceTestEndpoint); 195 | assertNotNull("Should create connection", connection2); 196 | presenceTestEndpoint.connections.add(connection2); 197 | 198 | RespokeConnection connection3 = new RespokeConnection(Respoke.makeGUID(), presenceTestEndpoint); 199 | assertNotNull("Should create connection", connection3); 200 | presenceTestEndpoint.connections.add(connection3); 201 | 202 | 203 | // Test presence values that are not strings 204 | 205 | 206 | customPresenceResolution = new HashMap(); 207 | ((HashMap)customPresenceResolution).put("myRealPresence", "ready"); 208 | 209 | HashMap connection1Presence = new HashMap(); 210 | connection1Presence.put("myRealPresence", "not ready"); 211 | connection1.presence = connection1Presence; 212 | 213 | connection2.presence = customPresenceResolution; 214 | 215 | HashMap connection3Presence = new HashMap(); 216 | connection3Presence.put("myRealPresence", "not ready"); 217 | connection3.presence = connection3Presence; 218 | 219 | callbackDidSucceed = false; 220 | callbackPresence = null; 221 | presenceTestEndpoint.resolvePresence(); 222 | assertTrue("Test timed out", waitForCompletion(RespokeTestCase.TEST_TIMEOUT)); 223 | assertTrue("Presence delegate should be called", callbackDidSucceed); 224 | assertTrue("Custom presence should be a hash map", presenceTestEndpoint.presence instanceof HashMap); 225 | HashMap resolvedPresence = (HashMap)presenceTestEndpoint.presence; 226 | assertTrue("Should resolve to correct custom presence", resolvedPresence.get("myRealPresence").equals("ready")); 227 | assertTrue("Should resolve correct custom presence in callback", ((HashMap)callbackPresence).get("myRealPresence").equals("ready")); 228 | } 229 | 230 | 231 | // RespokeEndpoint.Listener methods 232 | 233 | 234 | public void onMessage(String message, Date timestamp, RespokeEndpoint endpoint, boolean didSend) { 235 | // Not under test 236 | } 237 | 238 | 239 | public void onPresence(Object presence, RespokeEndpoint sender) { 240 | assertTrue("Sender should be set correctly", sender == presenceTestEndpoint); 241 | assertTrue("Should be called in UI thread", RespokeTestCase.currentlyOnUIThread()); 242 | callbackPresence = presence; 243 | callbackDidSucceed = true; 244 | asyncTaskDone = true; 245 | } 246 | 247 | 248 | // RespokeClient.ResolvePresenceListener methods 249 | 250 | 251 | public Object resolvePresence(ArrayList presenceArray) { 252 | assertTrue("presence array should contain the correct number of values", 3 == presenceArray.size()); 253 | return customPresenceResolution; 254 | } 255 | 256 | 257 | } 258 | -------------------------------------------------------------------------------- /respokeSDKTest/src/androidTest/java/com/digium/respokesdktest/unit/RespokeGroupTests.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015, Digium, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under The MIT License found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | * For all details and documentation: https://www.respoke.io 9 | */ 10 | 11 | package com.digium.respokesdktest.unit; 12 | 13 | import com.digium.respokesdk.Respoke; 14 | import com.digium.respokesdk.RespokeClient; 15 | import com.digium.respokesdk.RespokeConnection; 16 | import com.digium.respokesdk.RespokeGroup; 17 | import com.digium.respokesdktest.RespokeTestCase; 18 | 19 | import java.util.ArrayList; 20 | 21 | 22 | public class RespokeGroupTests extends RespokeTestCase { 23 | 24 | private boolean callbackDidSucceed; 25 | 26 | 27 | public void testUnconnectedGroupBehaviour() { 28 | RespokeClient client = Respoke.sharedInstance().createClient(getContext()); 29 | assertNotNull(client); 30 | 31 | String testGroupID = "myGroupID"; 32 | RespokeGroup group = new RespokeGroup(testGroupID, null, client); 33 | 34 | assertNotNull("Group should not be null", group); 35 | assertTrue("Should indicate group is not joined", !group.isJoined()); 36 | 37 | callbackDidSucceed = false; 38 | asyncTaskDone = false; 39 | group.sendMessage("A message", false, new Respoke.TaskCompletionListener() { 40 | @Override 41 | public void onSuccess() { 42 | assertTrue("Should not send a message to a group that is not joined", false); 43 | asyncTaskDone = true; 44 | } 45 | 46 | @Override 47 | public void onError(String errorMessage) { 48 | assertTrue("Should be called in UI thread", RespokeTestCase.currentlyOnUIThread()); 49 | assertTrue("Should return the correct error message", errorMessage.equals("Not a member of this group anymore.")); 50 | callbackDidSucceed = true; 51 | asyncTaskDone = true; 52 | } 53 | }); 54 | 55 | assertTrue("Test timed out", waitForCompletion(RespokeTestCase.TEST_TIMEOUT)); 56 | assertTrue("Should have called the error handler", callbackDidSucceed); 57 | 58 | callbackDidSucceed = false; 59 | asyncTaskDone = false; 60 | 61 | group.leave(new Respoke.TaskCompletionListener() { 62 | @Override 63 | public void onSuccess() { 64 | assertTrue("Leaving an unjoined group should fail", false); 65 | asyncTaskDone = true; 66 | } 67 | 68 | @Override 69 | public void onError(String errorMessage) { 70 | assertTrue("Should be called in UI thread", RespokeTestCase.currentlyOnUIThread()); 71 | assertTrue("Should return the correct error message", "Not a member of this group anymore.".equals(errorMessage)); 72 | callbackDidSucceed = true; 73 | asyncTaskDone = true; 74 | } 75 | }); 76 | 77 | assertTrue("Test timed out", waitForCompletion(RespokeTestCase.TEST_TIMEOUT)); 78 | assertTrue("Should have called the error handler", callbackDidSucceed); 79 | 80 | callbackDidSucceed = false; 81 | asyncTaskDone = false; 82 | group.getMembers(new RespokeGroup.GetGroupMembersCompletionListener() { 83 | @Override 84 | public void onSuccess(ArrayList memberArray) { 85 | assertTrue("Getting members of an unjoined group should fail", false); 86 | asyncTaskDone = true; 87 | } 88 | 89 | @Override 90 | public void onError(String errorMessage) { 91 | assertTrue("Should be called in UI thread", RespokeTestCase.currentlyOnUIThread()); 92 | assertTrue("Should return the correct error message", errorMessage.equals("Not a member of this group anymore.")); 93 | callbackDidSucceed = true; 94 | asyncTaskDone = true; 95 | } 96 | }); 97 | 98 | assertTrue("Test timed out", waitForCompletion(RespokeTestCase.TEST_TIMEOUT)); 99 | assertTrue("Should have called the error handler", callbackDidSucceed); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /respokeSDKTest/src/androidTest/java/com/digium/respokesdktest/unit/RespokeTests.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015, Digium, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under The MIT License found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | * For all details and documentation: https://www.respoke.io 9 | */ 10 | 11 | package com.digium.respokesdktest.unit; 12 | 13 | import android.app.Application; 14 | import android.test.ApplicationTestCase; 15 | 16 | import com.digium.respokesdk.Respoke; 17 | 18 | 19 | public class RespokeTests extends ApplicationTestCase { 20 | 21 | public RespokeTests() { 22 | super(Application.class); 23 | } 24 | 25 | 26 | public void testMakeGUID() { 27 | String guid1 = Respoke.makeGUID(); 28 | String guid2 = Respoke.makeGUID(); 29 | String guid3 = Respoke.makeGUID(); 30 | String guid4 = Respoke.makeGUID(); 31 | 32 | assertTrue("GUIDs should be " + Respoke.GUID_STRING_LENGTH + " characters", Respoke.GUID_STRING_LENGTH == guid1.length()); 33 | assertTrue("GUIDs should be " + Respoke.GUID_STRING_LENGTH + " characters", Respoke.GUID_STRING_LENGTH == guid2.length()); 34 | assertTrue("GUIDs should be " + Respoke.GUID_STRING_LENGTH + " characters", Respoke.GUID_STRING_LENGTH == guid3.length()); 35 | assertTrue("GUIDs should be " + Respoke.GUID_STRING_LENGTH + " characters", Respoke.GUID_STRING_LENGTH == guid4.length()); 36 | 37 | assertFalse("Should create unique GUIDs every time", guid1.equals(guid2)); 38 | assertFalse("Should create unique GUIDs every time", guid1.equals(guid3)); 39 | assertFalse("Should create unique GUIDs every time", guid1.equals(guid4)); 40 | 41 | assertFalse("Should create unique GUIDs every time", guid2.equals(guid3)); 42 | assertFalse("Should create unique GUIDs every time", guid2.equals(guid4)); 43 | 44 | assertFalse("Should create unique GUIDs every time", guid3.equals(guid4)); 45 | } 46 | 47 | 48 | } -------------------------------------------------------------------------------- /respokeSDKTest/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /respokeSDKTest/src/main/java/com/digium/respokesdktest/MainActivity.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015, Digium, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under The MIT License found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | * For all details and documentation: https://www.respoke.io 9 | */ 10 | 11 | package com.digium.respokesdktest; 12 | 13 | import android.os.Bundle; 14 | import android.support.v7.app.AppCompatActivity; 15 | import android.view.Menu; 16 | import android.view.MenuItem; 17 | 18 | 19 | public class MainActivity extends AppCompatActivity { 20 | 21 | @Override 22 | protected void onCreate(Bundle savedInstanceState) { 23 | super.onCreate(savedInstanceState); 24 | setContentView(R.layout.activity_main); 25 | } 26 | 27 | 28 | @Override 29 | public boolean onCreateOptionsMenu(Menu menu) { 30 | // Inflate the menu; this adds items to the action bar if it is present. 31 | getMenuInflater().inflate(R.menu.menu_main, menu); 32 | return true; 33 | } 34 | 35 | @Override 36 | public boolean onOptionsItemSelected(MenuItem item) { 37 | // Handle action bar item clicks here. The action bar will 38 | // automatically handle clicks on the Home/Up button, so long 39 | // as you specify a parent activity in AndroidManifest.xml. 40 | int id = item.getItemId(); 41 | 42 | //noinspection SimplifiableIfStatement 43 | if (id == R.id.action_settings) { 44 | return true; 45 | } 46 | 47 | return super.onOptionsItemSelected(item); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /respokeSDKTest/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /respokeSDKTest/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /respokeSDKTest/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/respoke/respoke-sdk-android/34a15f0558d29b1f1bc8481bbc5c505e855e05ef/respokeSDKTest/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /respokeSDKTest/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/respoke/respoke-sdk-android/34a15f0558d29b1f1bc8481bbc5c505e855e05ef/respokeSDKTest/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /respokeSDKTest/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/respoke/respoke-sdk-android/34a15f0558d29b1f1bc8481bbc5c505e855e05ef/respokeSDKTest/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /respokeSDKTest/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/respoke/respoke-sdk-android/34a15f0558d29b1f1bc8481bbc5c505e855e05ef/respokeSDKTest/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /respokeSDKTest/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /respokeSDKTest/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /respokeSDKTest/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | SDK Test UI 3 | 4 | Hello world! 5 | Settings 6 | 7 | -------------------------------------------------------------------------------- /respokeSDKTest/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':respokeSDKTest', ':respokeSDK' 2 | project(':respokeSDK').projectDir = new File('respokeSDK') --------------------------------------------------------------------------------