├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── buildsystem ├── bitrise.gradle └── dependencies.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── sample ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── debug │ └── res │ │ └── values │ │ └── strings.xml │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── co │ │ │ └── netguru │ │ │ └── android │ │ │ └── chatandroll │ │ │ ├── app │ │ │ ├── App.kt │ │ │ ├── ApplicationComponent.kt │ │ │ └── ApplicationModule.kt │ │ │ ├── common │ │ │ ├── di │ │ │ │ ├── ActivityScope.kt │ │ │ │ ├── FragmentScope.kt │ │ │ │ └── ServiceScope.kt │ │ │ ├── extension │ │ │ │ ├── ContextExt.kt │ │ │ │ └── RxFirebaseExt.kt │ │ │ └── util │ │ │ │ └── RxUtils.kt │ │ │ ├── data │ │ │ ├── firebase │ │ │ │ ├── FirebaseIceCandidates.kt │ │ │ │ ├── FirebaseIceServers.kt │ │ │ │ ├── FirebaseModule.kt │ │ │ │ ├── FirebaseSignalingAnswers.kt │ │ │ │ ├── FirebaseSignalingDisconnect.kt │ │ │ │ ├── FirebaseSignalingOffers.kt │ │ │ │ └── FirebaseSignalingOnline.kt │ │ │ └── model │ │ │ │ ├── IceCandidateFirebase.kt │ │ │ │ ├── IceServerFirebase.kt │ │ │ │ ├── RouletteConnectionFirebase.kt │ │ │ │ ├── RouletteDisconnectOrderFirebase.kt │ │ │ │ └── SessionDescriptionFirebase.kt │ │ │ ├── feature │ │ │ ├── base │ │ │ │ ├── BaseActivity.kt │ │ │ │ ├── BaseFragment.kt │ │ │ │ ├── BaseMvpActivity.kt │ │ │ │ ├── BaseMvpFragment.kt │ │ │ │ ├── BasePresenter.kt │ │ │ │ ├── MvpView.kt │ │ │ │ ├── Presenter.kt │ │ │ │ └── service │ │ │ │ │ ├── BaseServiceController.kt │ │ │ │ │ ├── BaseServiceWithFacade.kt │ │ │ │ │ ├── ServiceController.kt │ │ │ │ │ └── ServiceFacade.kt │ │ │ └── main │ │ │ │ ├── MainActivity.kt │ │ │ │ └── video │ │ │ │ ├── MoveUpBehavior.kt │ │ │ │ ├── VideoFragment.kt │ │ │ │ ├── VideoFragmentComponent.kt │ │ │ │ ├── VideoFragmentPresenter.kt │ │ │ │ └── VideoFragmentView.kt │ │ │ └── webrtc │ │ │ └── service │ │ │ ├── WebRtcService.kt │ │ │ ├── WebRtcServiceComponent.kt │ │ │ ├── WebRtcServiceController.kt │ │ │ ├── WebRtcServiceFacade.kt │ │ │ └── WebRtcServiceListener.kt │ └── res │ │ ├── drawable-hdpi │ │ ├── chat_and_roll_logo.png │ │ └── ic_notification_small_logo.png │ │ ├── drawable-mdpi │ │ ├── chat_and_roll_logo.png │ │ └── ic_notification_small_logo.png │ │ ├── drawable-xhdpi │ │ ├── chat_and_roll_logo.png │ │ └── ic_notification_small_logo.png │ │ ├── drawable-xxhdpi │ │ ├── chat_and_roll_logo.png │ │ └── ic_notification_small_logo.png │ │ ├── drawable-xxxhdpi │ │ ├── chat_and_roll_logo.png │ │ └── ic_notification_small_logo.png │ │ ├── drawable │ │ ├── background_splash.xml │ │ ├── call_end_button.xml │ │ ├── circle_transparent_button.xml │ │ ├── ic_call_end_white_24dp.xml │ │ ├── ic_mic_off_white_24dp.xml │ │ ├── ic_mic_white_24dp.xml │ │ ├── ic_microphone_toggle.xml │ │ ├── ic_switch_video_white_24dp.xml │ │ ├── ic_videocam_off_white_24dp.xml │ │ ├── ic_videocam_on_off_toggle.xml │ │ ├── ic_videocam_white_24dp.xml │ │ ├── ic_videocam_white_72dp.xml │ │ ├── mic_off_button.xml │ │ ├── mic_on_button.xml │ │ ├── rounded_button.xml │ │ ├── videocam_off_button.xml │ │ └── videocam_on_button.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ └── fragment_video.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── co │ └── netguru │ └── android │ └── chatandroll │ ├── RxSchedulerOverrideRule.kt │ └── data │ └── model │ └── IceCandidateFirebaseTest.kt ├── settings.gradle └── videochatguru ├── .gitignore ├── LICENSE ├── bintray.gradle ├── build.gradle ├── install.gradle ├── proguard-rules.pro ├── publish.gradle └── src ├── main ├── AndroidManifest.xml └── java │ └── co │ └── netguru │ └── videochatguru │ ├── CustomPeerConnectionObserver.kt │ ├── PeerConnectionListener.kt │ ├── RemoteVideoListener.kt │ ├── SdpCreateObserver.kt │ ├── SdpSetObserver.kt │ ├── VideoPeerConnectionObserver.kt │ ├── WebRtcAnsweringPartyHandler.kt │ ├── WebRtcAnsweringPartyListener.kt │ ├── WebRtcClient.kt │ ├── WebRtcLogs.kt │ ├── WebRtcOfferingActionListener.kt │ ├── WebRtcOfferingPartyHandler.kt │ ├── constraints │ ├── BooleanAudioConstraints.kt │ ├── IntegerAudioConstraints.kt │ ├── OfferAnswerConstraints.kt │ ├── PeerConnectionConstraints.kt │ ├── WebRtcConstraint.kt │ └── WebRtcConstraints.kt │ └── util │ ├── Logger.kt │ ├── MediaConstraintsExt.kt │ ├── WebRtcCameraUtils.kt │ └── WebRtcUtils.kt └── test └── java └── co └── netguru └── chatguru └── ExampleUnitTest.java /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Task 2 | 3 | - [Issue](https://github.com/netguru/videochatguru-android/issues/) 4 | 5 | ### Description 6 | 7 | 8 | ### Additional Notes (optional) 9 | 10 | 11 | ### I have tested the solution on these devices: 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Android 2 | 3 | # Built application files 4 | *.apk 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | reports/ 16 | 17 | # Gradle files 18 | .gradle/ 19 | build/ 20 | 21 | # Local configuration file (sdk path, etc) 22 | local.properties 23 | 24 | # Application secrets 25 | secret.properties 26 | 27 | Proguard folder generated by Eclipse 28 | #proguard/ 29 | 30 | # Log Files 31 | *.log 32 | 33 | # Android Studio Navigation editor temp files 34 | .navigation/ 35 | 36 | # Android Studio captures folder 37 | captures/ 38 | 39 | # Keystore files 40 | *.jks 41 | 42 | ### JetBrains 43 | 44 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 45 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 46 | 47 | # User-specific stuff: 48 | .idea/ 49 | 50 | 51 | ## File-based project format: 52 | *.iml 53 | *.iws 54 | 55 | ## Plugin-specific files: 56 | 57 | # IntelliJ 58 | /out/ 59 | 60 | # JIRA plugin 61 | atlassian-ide-plugin.xml 62 | 63 | # Crashlytics plugin (for Android Studio and IntelliJ) 64 | com_crashlytics_export_strings.xml 65 | crashlytics.properties 66 | crashlytics-build.properties 67 | fabric.properties 68 | 69 | ### Temporary files 70 | 71 | ## Vim 72 | [._]*.s[a-w][a-z] 73 | [._]s[a-w][a-z] 74 | Session.vim 75 | .netrwhist 76 | tags 77 | 78 | ## OS X 79 | *.DS_Store 80 | .AppleDouble 81 | .LSOverride 82 | # Icon must end with two \r 83 | Icon 84 | # Thumbnails 85 | ._* 86 | # Files that might appear in the root of a volume 87 | .DocumentRevisions-V100 88 | .fseventsd 89 | .Spotlight-V100 90 | .TemporaryItems 91 | .Trashes 92 | .VolumeIcon.icns 93 | .com.apple.timemachine.donotpresent 94 | # Directories potentially created on remote AFP share 95 | .AppleDB 96 | .AppleDesktop 97 | Network Trash Folder 98 | Temporary Items 99 | .apdisk 100 | 101 | ## Linux 102 | *~ 103 | 104 | # temporary files which can be created if a process still has a handle open of a deleted file 105 | .fuse_hidden* 106 | 107 | # KDE directory preferences 108 | .directory 109 | 110 | # Linux trash folder which might appear on any partition or disk 111 | .Trash-* 112 | 113 | # Google Services 114 | google-services.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | License for everything expect WebRTC: 2 | ============================ 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | 180 | APPENDIX: How to apply the Apache License to your work. 181 | 182 | To apply the Apache License to your work, attach the following 183 | boilerplate notice, with the fields enclosed by brackets "[]" 184 | replaced with your own identifying information. (Don't include 185 | the brackets!) The text should be enclosed in the appropriate 186 | comment syntax for the file format. We also recommend that a 187 | file or class name and description of purpose be included on the 188 | same "printed page" as the copyright notice for easier 189 | identification within third-party archives. 190 | 191 | Copyright [yyyy] [name of copyright owner] 192 | 193 | Licensed under the Apache License, Version 2.0 (the "License"); 194 | you may not use this file except in compliance with the License. 195 | You may obtain a copy of the License at 196 | 197 | http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software 200 | distributed under the License is distributed on an "AS IS" BASIS, 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | See the License for the specific language governing permissions and 203 | limitations under the License. 204 | 205 | 206 | License for WebRTC: 207 | 208 | ============================ 209 | Copyright (c) 2011, The WebRTC project authors. All rights reserved. 210 | 211 | Redistribution and use in source and binary forms, with or without 212 | modification, are permitted provided that the following conditions are 213 | met: 214 | 215 | * Redistributions of source code must retain the above copyright 216 | notice, this list of conditions and the following disclaimer. 217 | 218 | * Redistributions in binary form must reproduce the above copyright 219 | notice, this list of conditions and the following disclaimer in 220 | the documentation and/or other materials provided with the 221 | distribution. 222 | 223 | * Neither the name of Google nor the names of its contributors may 224 | be used to endorse or promote products derived from this software 225 | without specific prior written permission. 226 | 227 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 228 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 229 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 230 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 231 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 232 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 233 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 234 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 235 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 236 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 237 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 238 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | # VideoChatGuru simple webrtc for Android 14 | 15 | 16 | 17 | 18 | | environment | status | 19 | |-------------|--------------------| 20 | | Android | [![Build Status](https://www.bitrise.io/app/efa82287989764d2/status.svg?token=wB5901VLKALuCDo5ZAMuyA&branch=master)](https://www.bitrise.io/app/efa82287989764d2) | 21 | 22 | 23 | ## About 24 | VideoChatGuru is an open-source solution written entirely in Kotlin, based on a [WebRTC](https://webrtc.org/native-code/android/) implementation. It’s not bound with any service, and you are free to choose whether you are going to use an established service provider or host your own. VideoChatGuru prioritises peer-to-peer connections, thanks to which we reduce delays to a minimum. 25 | 26 | VideoChatGuru wraps the [WebRTC](https://webrtc.org/native-code/android/) API, providing a friendlier access to its features that you are going to need for implementing video chats. We also provide our Chat&Roll example implementation, so that you can see VideoChatGuru in action. Thanks to the fact that we used Firebase for signalling, it should be much easier to learn and understand how webRTC signalling works by observing it unfold live. 27 | 28 | ## VideoChatGuru setup 29 | - Create layout for your Video Chat - you will need to provide two SurfaceViewRenderer views for remote and local video. 30 | - Create instance of `WebRtcClient` all params are optional and default ones should suffice most of use cases. 31 | - Attach views using `webRtcClient.attachRemoteView(view)` and `webRtcClient.attachLocalView(view)` respectiviely there are also methods which allows you to unbind those. 32 | - Initialize peer connection by calling `webRtcClient.initializePeerConnection(...)` method. Here you will need to provide list of Interactive Connectivity Establishment servers(read more below) and implementation for three listeners that are required to make everything work. 33 | 34 | - `PeerConnectionListener.onIceCandidate` and `PeerConnectionListener.onIceCandidatesRemoved` - those callbacks are called when WebRTC produces or removes ICE candidates, you are responsible to pass those to the other party through any other estabilished communication channel. Other party should handle those respectively by calling `addRemoteIceCandidate` or `removeRemoteIceCandidate` 35 | - `WebRtcOfferingActionListener` - callbacks launched for offering party - one which will initialize call using create offer. `LocalSessionDescription` object which is passed in `onOfferRemoteDescription` should be passed to the answering party and handled using `handleRemoteOffer` method. 36 | - `WebRtcAnsweringPartyListener` - callbacks launched for answering party - one which will call handleRemoteOffer method. `LocalSessionDescription` object which is passed in `onSuccess` should be passed to the offering party and handled using `handleRemoteAnswer` method. 37 | - At this point `WebRtcClient` setup is done and you should initialize handshake process on one of the clients using `createOffer` method handling all the callbacks as described earlier. 38 | 39 | You can also reffer to the sample. 40 | 41 | ## What this library won't do: 42 | WebRTC is signaling agnostic meaning that it's your responsibility to provide communication channell that will allow to go through handshake phase. You are free to use your own solutions based on for example: FCM, WebSockets, Pooling, Firebase and any other that allows you to exchange messages beetwen clients in real time. 43 | 44 | You can reffer to our sample for solution based on Firebase that allowed us to create and implement Chat Roullette logic. Firebase will also allow you to easily observe handshake process in real time. 45 | 46 | ## Sample 47 | Chat&Roll sample allows you to have a video chat with random stranger, project showcase use of VideoChatGuru WebRTC wrapper on Android. Signaling is done through Firebase. Your Firebase setup should provide at least one IceServer - for best results you should provide at least one Turn server to be able to make connection when peer to peer connection fails. 48 | 49 | You can download Chat&Roll sample app from Google Play: https://play.google.com/store/apps/details?id=co.netguru.android.chatandroll 50 | 51 | ## Development 52 | To run sample project you need to prepare Firebase instance first. 53 | ### Setting up Firebase 54 | 1. Go to https://console.firebase.google.com/ 55 | 2. Create new project 56 | 3. Add Firebase to your android app 57 | - To setup debug version insert package name `co.netguru.android.chatandroll.debug` 58 | - To setup release version insert package name `co.netguru.android.chatandroll` 59 | 4. Download google-services.json 60 | 5. Place it in App folder 61 | 62 | #### Setting up Interactive Connectivity Establishment servers node. 63 | You will need your STUN/TURN servers credentials here. 64 | 65 | 1. Go to Database 66 | 2. Add new child name it `ice_servers` 67 | 3. Add new child to `ice_servers` named `0` 68 | 4. Add child to `0` 69 | 5. Insert ICE servers credentials 70 | - `uri : "turn:ipAddress"` or `uri : "stun:ipAddress"` in case of stun server 71 | 72 | optionally: 73 | 74 | - `username: "your username"` 75 | - `password: "your password"` 76 | 6. You can add more ICE servers to be used by WebRTC 77 | 7. This should be enough happy testing! 78 | 79 | ## STUN/TURN servers 80 | ### STUN (Session Traversal Utilities for NAT) 81 | STUN server will allow two parties to expose what their public IP addres is. In most cases we are hidden behind routers in local networks with private IP's. It's used only in first phase of connection while estabilishing peer to peer communication. There are many free STUN servers available as they are used only during connection initialization phase. 82 | 83 | ### TURN (Traversal Using Relay NAT) 84 | TURN server is relay server which is used only when peer to peer connection cannot be estabilished, this can be caused for example by NAT restricions. When TURN server has to be used it might cause higher latency, as all the data has to go to the server and then to the other party, especially if the server is located far from both clients. TURN servers are obtainable mostly through paid services as lot of traffic is generated when TURN has to act as relay between clients, or you could host your own using [Coturn](https://github.com/coturn/coturn). 85 | 86 | If you want to learn more about how STUN and TURN works go on and watch : https://youtu.be/p2HzZkd2A40?t=1123 87 | 88 | ### Public STUN/TURN servers 89 | You can find many public STUN servers on the web, you might also want to try out: 90 | - http://numb.viagenie.ca/ 91 | Numb provides both STUN and TURN servers free of charge. 92 | Take in mind that using public servers might not be the best choice for production code. 93 | ### Private STUN/TURN servers 94 | #### External services 95 | Many private services provides their STUN servers free of charge. You can also find free TURN servers with limited amount of bandwidth to use up. 96 | #### Set up your own STUN/TURN server 97 | There are various options available out there we've tested Coturn and setup is fairly easy, you can follow this guide https://github.com/coturn/coturn/wiki/CoturnConfig . 98 | 99 | ### Sample integrations 100 | 101 | - Firebase - WebRTC signaling solution based on firebase real time database - https://firebase.google.com/ 102 | - STUN/TURN servers 103 | 104 | ## Contribution 105 | You're more than welcome to contribute or report an issue in case of any problems, questions or improvement proposals. 106 | 107 | ### How to contribute 108 | * Fork it ( https://github.com/netguru/videochatguru-android/fork ) 109 | * Create your feature branch (git checkout -b my-new-feature) 110 | * Commit your changes (git commit -am 'Add some feature') 111 | * Push to the branch (git push origin my-new-feature) 112 | * Create a new Pull Request 113 | 114 | ## Download 115 | To use this library in your project, place the following code in your top hierarchy build.gradle file: 116 | ```groovy 117 | allprojects { 118 | repositories { 119 | maven { url 'https://dl.bintray.com/netguru/maven/' } 120 | } 121 | } 122 | ``` 123 | 124 | Just add the following dependency in your app's build.gradle: 125 | ```groovy 126 | dependencies { 127 | compile 'co.netguru.videochatguru:videochatguru:0.1.2@aar' 128 | } 129 | ``` 130 | 131 | Copyright © 2017 [Netguru](http://netguru.co). 132 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | apply from: 'buildsystem/dependencies.gradle' 3 | apply from: 'buildsystem/bitrise.gradle' 4 | 5 | ext { 6 | globalVersionCode = isBitrise ? Integer.parseInt(bitrise.io.buildNumber) : 1 7 | globalVersionName = '0.1.0' 8 | } 9 | 10 | buildscript { 11 | 12 | ext.versions = [ 13 | kotlin : '1.2.10', 14 | buildTools: '28.0.3', 15 | minSdk : 16, 16 | compileSdk: 28, 17 | ] 18 | 19 | repositories { 20 | jcenter() 21 | google() 22 | } 23 | dependencies { 24 | classpath 'com.android.tools.build:gradle:3.0.1' 25 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin" 26 | classpath 'com.google.gms:google-services:3.1.0' 27 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.1' 28 | classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' 29 | 30 | // NOTE: Do not place your application dependencies here; they belong 31 | // in the individual module build.gradle files 32 | } 33 | } 34 | 35 | allprojects { 36 | repositories { 37 | jcenter() 38 | google() 39 | } 40 | } 41 | 42 | task clean(type: Delete) { 43 | delete rootProject.buildDir 44 | } 45 | 46 | // Workaround https://github.com/novoda/bintray-release/issues/71 47 | allprojects { 48 | tasks.withType(Javadoc) { 49 | excludes = ['**/*.kt'] // < ---- Exclude all kotlin files from javadoc file. 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /buildsystem/bitrise.gradle: -------------------------------------------------------------------------------- 1 | ext { 2 | bitrise = [ 3 | // Bitrise command line interface specific properties 4 | cli: [ 5 | workflowId : System.getenv('BITRISE_TRIGGERED_WORKFLOW_ID'), 6 | workflowTitle: System.getenv('BITRISE_TRIGGERED_WORKFLOW_TITLE'), 7 | buildStatus : System.getenv('BITRISE_BUILD_STATUS'), 8 | sourceDir : System.getenv('BITRISE_SOURCE_DIR'), 9 | deployDir : System.getenv('BITRISE_DEPLOY_DIR'), 10 | isCi : "true" == System.getenv('CI'), 11 | isPr : "true" == System.getenv('PR'), 12 | ], 13 | // Bitrise.io - only 14 | io : [ 15 | buildNumber : System.getenv('BITRISE_BUILD_NUMBER'), 16 | gitUrl : System.getenv('GIT_REPOSITORY_URL'), 17 | appTitle : System.getenv('BITRISE_APP_TITLE'), 18 | appUrl : System.getenv('BITRISE_APP_URL'), 19 | appSlug : System.getenv('BITRISE_APP_SLUG'), 20 | buildUrl : System.getenv('BITRISE_BUILD_URL'), 21 | buildSlug : System.getenv('BITRISE_BUILD_SLUG'), 22 | buildTriggerTimestamp: System.getenv('BITRISE_BUILD_TRIGGER_TIMESTAMP'), 23 | gitBranch : System.getenv('BITRISE_GIT_BRANCH'), 24 | gitTag : System.getenv('BITRISE_GIT_TAG'), 25 | gitCommit : System.getenv('BITRISE_GIT_COMMIT'), 26 | gitMessage : System.getenv('BITRISE_GIT_MESSAGE'), 27 | pullRequest : System.getenv('BITRISE_PULL_REQUEST'), 28 | provisionUrl : System.getenv('BITRISE_PROVISION_URL'), 29 | certificateUrl : System.getenv('BITRISE_CERTIFICATE_URL'), 30 | certificatePassphrase: System.getenv('BITRISE_CERTIFICATE_PASSPHRASE'), 31 | ] 32 | ] 33 | 34 | isBitrise = bitrise.cli.isCi || bitrise.cli.isPr 35 | } -------------------------------------------------------------------------------- /buildsystem/dependencies.gradle: -------------------------------------------------------------------------------- 1 | ext { 2 | // Versions 3 | supportLibraryVersion = '26.0.1' 4 | daggerVersion = '2.10' 5 | leakCanary = '1.5.1' 6 | firebaseVersion = '11.2.0' 7 | webRTCVersion = '1.0.20284' 8 | 9 | libs = [ 10 | kotlinStdlib : "org.jetbrains.kotlin:kotlin-stdlib-jre7:$versions.kotlin", 11 | appCompat : "com.android.support:appcompat-v7:$supportLibraryVersion", 12 | design : "com.android.support:design:$supportLibraryVersion", 13 | cardView : "com.android.support:cardview-v7:$supportLibraryVersion", 14 | rxJava2 : "io.reactivex.rxjava2:rxjava:2.1.0", 15 | rxAndroid2 : "io.reactivex.rxjava2:rxandroid:2.0.1", 16 | rxKotlin2 : "io.reactivex.rxjava2:rxkotlin:2.1.0", 17 | dagger : "com.google.dagger:dagger:$daggerVersion", 18 | timber : "com.jakewharton.timber:timber:4.5.1", 19 | leakCanary : "com.squareup.leakcanary:leakcanary-android:$leakCanary", 20 | leakCanaryNoOp : "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanary", 21 | firebaseCore : "com.google.firebase:firebase-core:$firebaseVersion", 22 | firebaseDatabase: "com.google.firebase:firebase-database:$firebaseVersion", 23 | webRTC : "org.webrtc:google-webrtc:$webRTCVersion" 24 | ] 25 | 26 | proc = [ 27 | dagger : "com.google.dagger:dagger-compiler:$daggerVersion", // apt 28 | javaxAnnotation: "org.glassfish:javax.annotation:10.0-b28", // apt (provided) 29 | ] 30 | 31 | test = [ 32 | mockito : "org.mockito:mockito-core:2.8.47", 33 | mockitoKotlin: "com.nhaarman:mockito-kotlin:1.5.0", 34 | junit : "junit:junit:4.12", 35 | jsr305 : "com.google.code.findbugs:jsr305:3.0.1", 36 | ] 37 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 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 | org.gradle.jvmargs = -Xmx2048m 13 | # When configured, Gradle will run in incubating parallel mode. 14 | # This option should only be used with decoupled projects. More details, visit 15 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 16 | # org.gradle.parallel=true 17 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/videochatguru-android/e5790fec71db013aac00dac2faf15d87fc46b80a/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Nov 28 16:07:29 CET 2017 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-4.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 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android-extensions' 3 | apply plugin: 'kotlin-android' 4 | apply plugin: 'kotlin-kapt' 5 | 6 | android { 7 | compileSdkVersion versions.compileSdk 8 | buildToolsVersion versions.buildTools 9 | 10 | defaultConfig { 11 | applicationId "co.netguru.android.chatandroll" 12 | minSdkVersion versions.minSdk 13 | targetSdkVersion versions.compileSdk 14 | versionCode globalVersionCode 15 | versionName globalVersionName 16 | 17 | vectorDrawables.useSupportLibrary = true 18 | } 19 | 20 | buildTypes { 21 | debug { 22 | versionNameSuffix "-DEBUG" 23 | applicationIdSuffix ".debug" 24 | } 25 | release { 26 | debuggable false 27 | minifyEnabled true 28 | shrinkResources true 29 | zipAlignEnabled true 30 | useProguard true 31 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 32 | } 33 | } 34 | 35 | sourceSets { 36 | main.java.srcDirs += 'src/main/kotlin' 37 | } 38 | buildToolsVersion '28.0.3' 39 | } 40 | 41 | dependencies { 42 | implementation fileTree(dir: 'libs', include: ['*.jar']) 43 | implementation project(':videochatguru') 44 | 45 | implementation libs.appCompat 46 | implementation libs.design 47 | 48 | implementation libs.dagger 49 | kapt proc.dagger 50 | compileOnly proc.javaxAnnotation 51 | 52 | implementation libs.rxJava2 53 | implementation libs.rxAndroid2 54 | implementation libs.rxKotlin2 55 | 56 | debugImplementation libs.leakCanary 57 | releaseImplementation libs.leakCanaryNoOp 58 | testImplementation libs.leakCanaryNoOp 59 | 60 | implementation libs.timber 61 | 62 | implementation libs.firebaseDatabase 63 | 64 | testImplementation test.junit 65 | testImplementation test.mockito 66 | testImplementation(test.mockitoKotlin) { 67 | exclude group: 'org.jetbrains.kotlin' 68 | } 69 | } 70 | //To avoid dependency collisions GMS plugin should be placed at the bottom 71 | apply plugin: 'com.google.gms.google-services' 72 | -------------------------------------------------------------------------------- /sample/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/maciek/Library/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 | 19 | # base option from *App Dev Note* 20 | -optimizationpasses 5 21 | -dontusemixedcaseclassnames 22 | -dontskipnonpubliclibraryclasses 23 | -dontskipnonpubliclibraryclassmembers 24 | -dontpreverify 25 | -optimizations !code/simplification/arithmetic,!field/*,!class/merging/* 26 | -keepattributes LineNumberTable,SourceFile,Signature,*Annotation*,Exceptions,InnerClasses 27 | 28 | # Keep native methods 29 | -keepclassmembers class * { 30 | native ; 31 | } 32 | 33 | # remove log call 34 | -assumenosideeffects class android.util.Log { 35 | public static *** d(...); 36 | } 37 | -assumenosideeffects class timber.log.Timber { 38 | public static *** d(...); 39 | } 40 | 41 | # app compat-v7 42 | -keep class android.support.v7.widget.SearchView { *; } 43 | 44 | # FragmentArgs 45 | -keep class com.hannesdorfmann.fragmentargs.** { *; } 46 | 47 | # dagger 48 | -keepclassmembers,allowobfuscation class * { 49 | @javax.inject.* *; 50 | @dagger.* *; 51 | (); 52 | } 53 | -keep class javax.inject.** { *; } 54 | -keep class **$$ModuleAdapter 55 | -keep class **$$InjectAdapter 56 | -keep class **$$StaticInjection 57 | -keep class dagger.** { *; } 58 | -dontwarn dagger.internal.codegen.** 59 | 60 | # leak canary 61 | -keep class org.eclipse.mat.** { *; } 62 | -keep class com.squareup.leakcanary.** { *; } 63 | -dontwarn android.app.Notification 64 | 65 | # Kotlin 66 | -keep class kotlin.Metadata { *; } 67 | -dontnote kotlin.internal.PlatformImplementationsKt 68 | -dontnote kotlin.reflect.jvm.internal.** 69 | 70 | #Firebase 71 | # Add this global rule 72 | -keepattributes Signature 73 | 74 | # This rule will properly ProGuard all the model classes in 75 | # the package co.netguru.android.chatandroll.data.model.** 76 | -keepclassmembers class co.netguru.android.chatandroll.data.model.** { 77 | *; 78 | } 79 | 80 | #GMS 81 | -dontnote com.google.android.gms.** -------------------------------------------------------------------------------- /sample/src/debug/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Chat&Roll 3 | Chat&Roll leaks 4 | 5 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/app/App.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.app 2 | 3 | import android.app.Application 4 | import android.app.NotificationChannel 5 | import android.app.NotificationManager 6 | import android.content.Context 7 | import android.os.Build 8 | import android.support.v4.app.NotificationManagerCompat 9 | import android.support.v7.app.AppCompatDelegate 10 | import android.widget.Toast 11 | import co.netguru.videochatguru.disableWebRtcLogs 12 | import co.netguru.videochatguru.enableInternalWebRtclogs 13 | import co.netguru.videochatguru.enableWebRtcLogs 14 | import co.netguru.android.chatandroll.BuildConfig 15 | import co.netguru.android.chatandroll.R 16 | import co.netguru.android.chatandroll.data.firebase.FirebaseModule 17 | import com.squareup.leakcanary.LeakCanary 18 | import org.webrtc.Logging 19 | import timber.log.Timber 20 | import java.util.* 21 | 22 | 23 | class App : Application() { 24 | 25 | companion object Factory { 26 | 27 | val BACKGROUND_WORK_NOTIFICATIONS_CHANNEL_ID = "background_channel" 28 | 29 | val CURRENT_DEVICE_UUID = UUID.randomUUID().toString() 30 | 31 | fun get(context: Context): App = context.applicationContext as App 32 | 33 | fun getApplicationComponent(context: Context): ApplicationComponent = 34 | (context.applicationContext as App).applicationComponent 35 | } 36 | 37 | val applicationComponent: ApplicationComponent by lazy { 38 | DaggerApplicationComponent.builder() 39 | .applicationModule(ApplicationModule(this)) 40 | .firebaseModule(FirebaseModule()) 41 | .build() 42 | } 43 | 44 | init { 45 | AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) 46 | } 47 | 48 | override fun onCreate() { 49 | super.onCreate() 50 | if (LeakCanary.isInAnalyzerProcess(this)) { 51 | return 52 | } 53 | LeakCanary.install(this) 54 | if (BuildConfig.DEBUG) { 55 | Timber.plant(Timber.DebugTree()) 56 | //Enables WebRTC Logging 57 | enableWebRtcLogs(true) 58 | enableInternalWebRtclogs(Logging.Severity.LS_INFO) 59 | Toast.makeText(this, "Uuid: ${App.CURRENT_DEVICE_UUID}", Toast.LENGTH_LONG).show() 60 | } else { 61 | disableWebRtcLogs() 62 | } 63 | 64 | createNotificationChannels() 65 | } 66 | 67 | private fun createNotificationChannels() { 68 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 69 | val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 70 | val channel = NotificationChannel(BACKGROUND_WORK_NOTIFICATIONS_CHANNEL_ID, 71 | getString(R.string.background_work_notifications_channel), 72 | NotificationManagerCompat.IMPORTANCE_HIGH) 73 | .apply { 74 | description = getString(R.string.background_work_notification_channel_description) 75 | } 76 | notificationManager.createNotificationChannel(channel) 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/app/ApplicationComponent.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.app 2 | 3 | import co.netguru.android.chatandroll.data.firebase.FirebaseModule 4 | import co.netguru.android.chatandroll.feature.main.video.VideoFragmentComponent 5 | import co.netguru.android.chatandroll.webrtc.service.WebRtcServiceComponent 6 | import dagger.Component 7 | import javax.inject.Singleton 8 | 9 | @Singleton 10 | @Component(modules = arrayOf(ApplicationModule::class, FirebaseModule::class)) 11 | interface ApplicationComponent { 12 | 13 | fun videoFragmentComponent(): VideoFragmentComponent 14 | 15 | fun webRtcServiceComponent(): WebRtcServiceComponent 16 | } 17 | -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/app/ApplicationModule.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.app 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.content.res.Resources 6 | import co.netguru.android.chatandroll.data.firebase.FirebaseIceCandidates 7 | import co.netguru.android.chatandroll.data.firebase.FirebaseIceServers 8 | import co.netguru.android.chatandroll.data.firebase.FirebaseSignalingAnswers 9 | import co.netguru.android.chatandroll.data.firebase.FirebaseSignalingOffers 10 | import co.netguru.android.chatandroll.webrtc.service.WebRtcServiceController 11 | import co.netguru.videochatguru.WebRtcClient 12 | import dagger.Module 13 | import dagger.Provides 14 | import javax.inject.Singleton 15 | 16 | @Module 17 | class ApplicationModule(private val application: Application) { 18 | 19 | @Provides 20 | @Singleton 21 | fun provideContext(): Context = application.applicationContext 22 | 23 | @Provides 24 | @Singleton 25 | fun provideApplication() = application 26 | 27 | @Provides 28 | @Singleton 29 | fun provideResources(): Resources = application.resources 30 | 31 | @Provides 32 | fun provideWebRtcClient(context: Context) = WebRtcClient(context) 33 | 34 | @Provides 35 | fun provideWebRtcServiceController(webRtcClient: WebRtcClient, firebaseSignalingAnswers: FirebaseSignalingAnswers, 36 | firebaseSignalingOffers: FirebaseSignalingOffers, firebaseIceCandidates: FirebaseIceCandidates, 37 | firebaseIceServers: FirebaseIceServers): WebRtcServiceController { 38 | return WebRtcServiceController( 39 | webRtcClient, firebaseSignalingAnswers, firebaseSignalingOffers, 40 | firebaseIceCandidates, firebaseIceServers) 41 | } 42 | } -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/common/di/ActivityScope.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.common.di 2 | 3 | import javax.inject.Scope 4 | 5 | @Scope 6 | @Retention(AnnotationRetention.RUNTIME) 7 | annotation class ActivityScope -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/common/di/FragmentScope.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.common.di 2 | 3 | import javax.inject.Scope 4 | 5 | @Scope 6 | @Retention(AnnotationRetention.RUNTIME) 7 | annotation class FragmentScope -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/common/di/ServiceScope.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.common.di 2 | 3 | import javax.inject.Scope 4 | 5 | @Scope 6 | @Retention(AnnotationRetention.RUNTIME) 7 | annotation class ServiceScope -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/common/extension/ContextExt.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.common.extension 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.content.pm.PackageManager 6 | import android.net.Uri 7 | import android.provider.Settings 8 | import android.support.annotation.ColorRes 9 | import android.support.v4.content.ContextCompat 10 | 11 | fun Context.getColorCompat(@ColorRes color: Int) = ContextCompat.getColor(this, color) 12 | 13 | fun Context.startAppSettings() { 14 | Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).run { 15 | addCategory(Intent.CATEGORY_DEFAULT) 16 | data = Uri.parse("package:" + packageName) 17 | startActivity(this) 18 | } 19 | } 20 | 21 | fun Context.checkIsPermissionGranted(permission: String) = 22 | ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED 23 | 24 | fun Context.areAllPermissionsGranted(vararg permission: String) = permission.all { checkIsPermissionGranted(it) } -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/common/extension/RxFirebaseExt.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.common.extension 2 | 3 | import co.netguru.android.chatandroll.common.util.RxUtils 4 | import com.google.firebase.database.* 5 | import io.reactivex.BackpressureStrategy 6 | import io.reactivex.Flowable 7 | import io.reactivex.Maybe 8 | import io.reactivex.Single 9 | 10 | interface ChildEvent { 11 | val data: T 12 | } 13 | 14 | data class ChildEventMoved(override val data: T, val previousChildName: String?) : ChildEvent 15 | 16 | data class ChildEventChanged(override val data: T, val previousChildName: String?) : ChildEvent 17 | 18 | data class ChildEventAdded(override val data: T, val previousChildName: String?) : ChildEvent 19 | 20 | data class ChildEventRemoved(override val data: T) : ChildEvent 21 | 22 | data class DataChangeEvent(override val data: T) : ChildEvent 23 | 24 | fun DatabaseReference.rxChildEvents() = RxUtils.createFlowable>(BackpressureStrategy.BUFFER) { 25 | 26 | val listener = object : ChildEventListener { 27 | override fun onChildMoved(ds: DataSnapshot, previousChildName: String?) { 28 | it.onNext(ChildEventMoved(ds, previousChildName)) 29 | } 30 | 31 | override fun onChildChanged(ds: DataSnapshot, previousChildName: String?) { 32 | it.onNext(ChildEventChanged(ds, previousChildName)) 33 | } 34 | 35 | override fun onChildAdded(ds: DataSnapshot, previousChildName: String?) { 36 | it.onNext(ChildEventAdded(ds, previousChildName)) 37 | } 38 | 39 | override fun onChildRemoved(ds: DataSnapshot) { 40 | it.onNext(ChildEventRemoved(ds)) 41 | } 42 | 43 | override fun onCancelled(error: DatabaseError) { 44 | it.onError(error.toException()) 45 | } 46 | } 47 | it.setCancellable { removeEventListener(listener) } 48 | 49 | addChildEventListener(listener) 50 | } 51 | 52 | fun DatabaseReference.rxChildEvents(genericTypeIndicator: GenericTypeIndicator) 53 | = RxUtils.createFlowable>(BackpressureStrategy.BUFFER) { 54 | 55 | val listener = object : ChildEventListener { 56 | override fun onChildMoved(ds: DataSnapshot, previousChildName: String?) { 57 | val data = ds.getValue(genericTypeIndicator) 58 | it.onNext(ChildEventMoved(data, previousChildName)) 59 | } 60 | 61 | override fun onChildChanged(ds: DataSnapshot, previousChildName: String?) { 62 | val data = ds.getValue(genericTypeIndicator) 63 | it.onNext(ChildEventChanged(data, previousChildName)) 64 | } 65 | 66 | override fun onChildAdded(ds: DataSnapshot, previousChildName: String?) { 67 | val data = ds.getValue(genericTypeIndicator) 68 | it.onNext(ChildEventAdded(data, previousChildName)) 69 | } 70 | 71 | override fun onChildRemoved(ds: DataSnapshot) { 72 | val data = ds.getValue(genericTypeIndicator) 73 | it.onNext(ChildEventRemoved(data)) 74 | } 75 | 76 | override fun onCancelled(error: DatabaseError) { 77 | it.onError(error.toException()) 78 | } 79 | } 80 | it.setCancellable { removeEventListener(listener) } 81 | 82 | addChildEventListener(listener) 83 | } 84 | 85 | 86 | fun DatabaseReference.rxSingleValue(): Single = Single.create { 87 | val listener = object : ValueEventListener { 88 | override fun onCancelled(databaseError: DatabaseError) { 89 | it.onError(databaseError.toException()) 90 | } 91 | 92 | override fun onDataChange(dataSnapshot: DataSnapshot) { 93 | it.onSuccess(dataSnapshot) 94 | } 95 | 96 | } 97 | it.setCancellable { removeEventListener(listener) } 98 | 99 | addListenerForSingleValueEvent(listener) 100 | } 101 | 102 | fun DatabaseReference.rxSingleValue(clazz: Class): Maybe = rxSingleValue() 103 | .flatMapMaybe { 104 | val data = it.getValue(clazz) 105 | if (data == null) Maybe.empty() else Maybe.just(data) 106 | } 107 | 108 | fun DatabaseReference.rxSingleValue(genericTypeIndicator: GenericTypeIndicator): Maybe = rxSingleValue() 109 | .flatMapMaybe { 110 | val data = it.getValue(genericTypeIndicator) 111 | if (data == null) Maybe.empty() else Maybe.just(data) 112 | } 113 | 114 | fun DatabaseReference.rxValueEvents() = RxUtils.createFlowable(BackpressureStrategy.BUFFER) { 115 | val listener = object : ValueEventListener { 116 | override fun onCancelled(databaseError: DatabaseError) { 117 | it.onError(databaseError.toException()) 118 | } 119 | 120 | override fun onDataChange(dataSnapshot: DataSnapshot) { 121 | it.onNext(dataSnapshot) 122 | } 123 | 124 | } 125 | it.setCancellable { removeEventListener(listener) } 126 | 127 | addValueEventListener(listener) 128 | } 129 | 130 | fun DatabaseReference.rxValueEvents(clazz: Class): Flowable> = rxValueEvents() 131 | .map { 132 | val data = it.getValue(clazz) 133 | DataChangeEvent(data) 134 | } 135 | 136 | fun DatabaseReference.rxValueEvents(genericTypeIndicator: GenericTypeIndicator): Flowable> = rxValueEvents() 137 | .map { 138 | val data = it.getValue(genericTypeIndicator) 139 | DataChangeEvent(data) 140 | } -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/common/util/RxUtils.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.common.util 2 | 3 | import io.reactivex.* 4 | import io.reactivex.android.schedulers.AndroidSchedulers 5 | import io.reactivex.schedulers.Schedulers 6 | 7 | internal object RxUtils { 8 | fun createFlowable(mode: BackpressureStrategy, source: (FlowableEmitter) -> Unit): Flowable { 9 | return Flowable.create(source, mode) 10 | } 11 | 12 | fun applySingleIoSchedulers(): SingleTransformer { 13 | return SingleTransformer { 14 | it.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) 15 | } 16 | } 17 | 18 | fun applyCompletableIoSchedulers(): CompletableTransformer { 19 | return CompletableTransformer { 20 | it.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) 21 | } 22 | } 23 | 24 | fun applyFlowableIoSchedulers(): FlowableTransformer { 25 | return FlowableTransformer { 26 | it.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) 27 | } 28 | } 29 | 30 | fun applyObservableIoSchedulers(): ObservableTransformer { 31 | return ObservableTransformer { 32 | it.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) 33 | } 34 | } 35 | 36 | fun applyMaybeIoSchedulers(): MaybeTransformer { 37 | return MaybeTransformer { 38 | it.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/data/firebase/FirebaseIceCandidates.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.data.firebase 2 | 3 | import co.netguru.android.chatandroll.app.App 4 | import co.netguru.android.chatandroll.common.extension.ChildEvent 5 | import co.netguru.android.chatandroll.common.extension.ChildEventAdded 6 | import co.netguru.android.chatandroll.common.extension.ChildEventRemoved 7 | import co.netguru.android.chatandroll.common.extension.rxChildEvents 8 | import co.netguru.android.chatandroll.data.model.IceCandidateFirebase 9 | import com.google.firebase.database.* 10 | import io.reactivex.Completable 11 | import io.reactivex.Flowable 12 | import org.webrtc.IceCandidate 13 | import javax.inject.Inject 14 | import javax.inject.Singleton 15 | 16 | @Singleton 17 | class FirebaseIceCandidates @Inject constructor(private val firebaseDatabase: FirebaseDatabase) { 18 | 19 | companion object { 20 | private const val ICE_CANDIDATES_PATH = "ice_candidates/" 21 | } 22 | 23 | private fun deviceIceCandidatesPath(uuid: String) = ICE_CANDIDATES_PATH.plus(uuid) 24 | 25 | fun send(iceCandidate: IceCandidate): Completable = Completable.create { 26 | val reference = firebaseDatabase.getReference(deviceIceCandidatesPath(App.CURRENT_DEVICE_UUID)) 27 | with(reference) { 28 | onDisconnect().removeValue() 29 | push().setValue(IceCandidateFirebase.createFromIceCandidate(iceCandidate)) 30 | } 31 | it.onComplete() 32 | } 33 | 34 | fun remove(iceCandidatesToRemove: Array): Completable = Completable.create { 35 | val iceCandidatesToRemoveList = iceCandidatesToRemove 36 | .map { IceCandidateFirebase.createFromIceCandidate(it) } 37 | .toMutableList() 38 | val reference = firebaseDatabase.getReference(deviceIceCandidatesPath(App.CURRENT_DEVICE_UUID)) 39 | 40 | reference.runTransaction(object : Transaction.Handler { 41 | override fun doTransaction(mutableData: MutableData): Transaction.Result { 42 | val typeIndicator = object : GenericTypeIndicator>() {} 43 | val currentIceCandidatesInFirebaseMap = mutableData.getValue(typeIndicator) ?: 44 | return Transaction.success(mutableData) 45 | 46 | 47 | for ((key, value) in currentIceCandidatesInFirebaseMap) { 48 | if (iceCandidatesToRemoveList.remove(value)) { 49 | currentIceCandidatesInFirebaseMap.remove(key) 50 | } 51 | } 52 | mutableData.value = currentIceCandidatesInFirebaseMap 53 | return Transaction.success(mutableData) 54 | } 55 | 56 | override fun onComplete(databaseError: DatabaseError, committed: Boolean, p2: DataSnapshot?) { 57 | if (committed) { 58 | it.onComplete() 59 | } else { 60 | it.onError(databaseError.toException()) 61 | } 62 | } 63 | 64 | }) 65 | } 66 | 67 | fun get(remoteUuid: String): Flowable> { 68 | return firebaseDatabase.getReference(deviceIceCandidatesPath(remoteUuid)).rxChildEvents() 69 | .filter { it is ChildEventAdded || it is ChildEventRemoved } 70 | .map { 71 | val iceCandidateFirebase: IceCandidateFirebase = it.data.getValue(IceCandidateFirebase::class.java) as IceCandidateFirebase 72 | val iceCandidate = iceCandidateFirebase.toIceCandidate() 73 | if (it is ChildEventAdded) { 74 | ChildEventAdded(iceCandidate, it.previousChildName) 75 | } else { 76 | ChildEventRemoved(iceCandidate) 77 | } 78 | } 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/data/firebase/FirebaseIceServers.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.data.firebase 2 | 3 | import co.netguru.android.chatandroll.common.extension.rxSingleValue 4 | import co.netguru.android.chatandroll.data.model.IceServerFirebase 5 | import com.google.firebase.database.FirebaseDatabase 6 | import com.google.firebase.database.GenericTypeIndicator 7 | import io.reactivex.Maybe 8 | import org.webrtc.PeerConnection 9 | import javax.inject.Inject 10 | 11 | class FirebaseIceServers @Inject constructor(firebaseDatabase: FirebaseDatabase) { 12 | 13 | companion object { 14 | private const val ICE_SERVERS_PATH = "ice_servers" 15 | } 16 | 17 | private val firebaseIceServersReference by lazy { firebaseDatabase.getReference(ICE_SERVERS_PATH) } 18 | 19 | fun getIceServers(): Maybe> = firebaseIceServersReference 20 | .rxSingleValue(object : GenericTypeIndicator>() {}) 21 | .map { it.map { it.toIceServer() } } 22 | 23 | } -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/data/firebase/FirebaseModule.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.data.firebase 2 | 3 | import com.google.firebase.database.FirebaseDatabase 4 | import dagger.Module 5 | import dagger.Provides 6 | import javax.inject.Singleton 7 | 8 | @Module 9 | class FirebaseModule { 10 | 11 | @Singleton 12 | @Provides 13 | fun provideFirebaseDatabase(): FirebaseDatabase = FirebaseDatabase.getInstance() 14 | } -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/data/firebase/FirebaseSignalingAnswers.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.data.firebase 2 | 3 | import co.netguru.android.chatandroll.app.App 4 | import co.netguru.android.chatandroll.common.extension.rxValueEvents 5 | import co.netguru.android.chatandroll.data.model.SessionDescriptionFirebase 6 | import com.google.firebase.database.FirebaseDatabase 7 | import io.reactivex.Completable 8 | import io.reactivex.Flowable 9 | import io.reactivex.Single 10 | import io.reactivex.rxkotlin.toMaybe 11 | import org.webrtc.SessionDescription 12 | import javax.inject.Inject 13 | 14 | 15 | class FirebaseSignalingAnswers @Inject constructor(private val firebaseDatabase: FirebaseDatabase) { 16 | 17 | companion object { 18 | private const val ANSWERS_PATH = "answers/" 19 | } 20 | 21 | private fun deviceAnswersPath(deviceUuid: String) = ANSWERS_PATH.plus(deviceUuid) 22 | 23 | fun create(recipientUuid: String, localSessionDescription: SessionDescription): Completable = Completable.create { 24 | val reference = firebaseDatabase.getReference(deviceAnswersPath(recipientUuid)) 25 | reference.onDisconnect().removeValue() 26 | reference.setValue(SessionDescriptionFirebase.fromSessionDescriptionWithDefaultSenderUuid(localSessionDescription)) 27 | it.onComplete() 28 | } 29 | 30 | fun listenForNewAnswers(): Flowable { 31 | return Single.just { firebaseDatabase.getReference(deviceAnswersPath(App.CURRENT_DEVICE_UUID)) } 32 | .flatMapPublisher { it().rxValueEvents(SessionDescriptionFirebase::class.java) } 33 | .flatMapMaybe { it.data.toMaybe() } 34 | .map { it.toSessionDescription() } 35 | } 36 | } -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/data/firebase/FirebaseSignalingDisconnect.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.data.firebase 2 | 3 | import co.netguru.android.chatandroll.app.App 4 | import co.netguru.android.chatandroll.common.extension.ChildEventAdded 5 | import co.netguru.android.chatandroll.common.extension.rxChildEvents 6 | import co.netguru.android.chatandroll.data.model.RouletteDisconnectOrderFirebase 7 | import com.google.firebase.database.FirebaseDatabase 8 | import io.reactivex.Completable 9 | import io.reactivex.Flowable 10 | import io.reactivex.rxkotlin.ofType 11 | import javax.inject.Inject 12 | 13 | 14 | class FirebaseSignalingDisconnect @Inject constructor(private val firebaseDatabase: FirebaseDatabase) { 15 | 16 | companion object { 17 | private const val DISCONNECT_PATH = "should_disconnect/" 18 | } 19 | 20 | private fun deviceDisconnectPath(deviceUuid: String) = DISCONNECT_PATH.plus(deviceUuid) 21 | 22 | fun sendDisconnectOrderToOtherParty(recipientUuid: String): Completable = Completable.create { 23 | val reference = firebaseDatabase.getReference(deviceDisconnectPath(recipientUuid)) 24 | reference.setValue(RouletteDisconnectOrderFirebase(App.CURRENT_DEVICE_UUID)) { 25 | databaseError, _ -> 26 | if (databaseError != null) { 27 | it.onError(databaseError.toException()) 28 | } else { 29 | it.onComplete() 30 | } 31 | } 32 | } 33 | 34 | fun listenForDisconnectOrders(): Flowable> { 35 | return firebaseDatabase.getReference(deviceDisconnectPath(App.CURRENT_DEVICE_UUID)) 36 | .rxChildEvents() 37 | .ofType>() 38 | } 39 | 40 | fun cleanDisconnectOrders(): Completable = Completable.create { 41 | val reference = firebaseDatabase.getReference(deviceDisconnectPath(App.CURRENT_DEVICE_UUID)) 42 | reference.onDisconnect().removeValue() 43 | reference.removeValue { databaseError, _ -> 44 | if (databaseError != null) { 45 | it.onError(databaseError.toException()) 46 | } else { 47 | it.onComplete() 48 | } 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/data/firebase/FirebaseSignalingOffers.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.data.firebase 2 | 3 | import co.netguru.android.chatandroll.app.App 4 | import co.netguru.android.chatandroll.common.extension.rxValueEvents 5 | import co.netguru.android.chatandroll.data.model.SessionDescriptionFirebase 6 | import com.google.firebase.database.FirebaseDatabase 7 | import io.reactivex.Completable 8 | import io.reactivex.Flowable 9 | import io.reactivex.Single 10 | import io.reactivex.rxkotlin.toMaybe 11 | import org.webrtc.SessionDescription 12 | import javax.inject.Inject 13 | 14 | 15 | class FirebaseSignalingOffers @Inject constructor(private val firebaseDatabase: FirebaseDatabase) { 16 | 17 | companion object { 18 | private const val OFFERS_PATH = "offers/" 19 | } 20 | 21 | private fun deviceOffersPath(deviceUuid: String) = OFFERS_PATH.plus(deviceUuid) 22 | 23 | fun create(recipientUuid: String, localSessionDescription: SessionDescription): Completable = Completable.create { 24 | val reference = firebaseDatabase.getReference(deviceOffersPath(recipientUuid)) 25 | reference.onDisconnect().removeValue() 26 | reference.setValue(SessionDescriptionFirebase.fromSessionDescriptionWithDefaultSenderUuid(localSessionDescription)) 27 | it.onComplete() 28 | } 29 | 30 | fun listenForNewOffersWithUuid(): Flowable> { 31 | return Single.just { firebaseDatabase.getReference(deviceOffersPath(App.CURRENT_DEVICE_UUID)) } 32 | .flatMapPublisher { it().rxValueEvents(SessionDescriptionFirebase::class.java) } 33 | .flatMapMaybe { it.data.toMaybe() } 34 | .map { Pair(it.toSessionDescription(), it.senderUuid) } 35 | } 36 | } -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/data/firebase/FirebaseSignalingOnline.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.data.firebase 2 | 3 | import co.netguru.android.chatandroll.app.App 4 | import co.netguru.android.chatandroll.data.model.RouletteConnectionFirebase 5 | import com.google.firebase.database.* 6 | import io.reactivex.Completable 7 | import io.reactivex.Maybe 8 | import timber.log.Timber 9 | import java.security.SecureRandom 10 | import javax.inject.Inject 11 | import javax.inject.Singleton 12 | 13 | @Singleton 14 | class FirebaseSignalingOnline @Inject constructor(private val firebaseDatabase: FirebaseDatabase) { 15 | 16 | companion object { 17 | private const val ONLINE_DEVICES_PATH = "online_devices/" 18 | } 19 | 20 | private fun deviceOnlinePath(deviceUuid: String) = ONLINE_DEVICES_PATH.plus(deviceUuid) 21 | 22 | fun setOnlineAndRetrieveRandomDevice(): Maybe = Completable.create { 23 | val firebaseOnlineReference = firebaseDatabase.getReference(deviceOnlinePath(App.CURRENT_DEVICE_UUID)) 24 | with(firebaseOnlineReference) { 25 | onDisconnect().removeValue() 26 | setValue(RouletteConnectionFirebase()) 27 | } 28 | it.onComplete() 29 | }.andThen(chooseRandomDevice()) 30 | 31 | fun disconnect(): Completable = Completable.fromAction { 32 | firebaseDatabase.goOffline() 33 | } 34 | 35 | fun connect(): Completable = Completable.fromAction { 36 | firebaseDatabase.goOnline() 37 | } 38 | 39 | private fun chooseRandomDevice(): Maybe = Maybe.create { 40 | var lastUuid: String? = null 41 | 42 | firebaseDatabase.getReference(ONLINE_DEVICES_PATH).runTransaction(object : Transaction.Handler { 43 | override fun doTransaction(mutableData: MutableData): Transaction.Result { 44 | lastUuid = null 45 | val genericTypeIndicator = object : GenericTypeIndicator>() {} 46 | val availableDevices = mutableData.getValue(genericTypeIndicator) ?: 47 | return Transaction.success(mutableData) 48 | 49 | val removedSelfValue = availableDevices.remove(App.CURRENT_DEVICE_UUID) 50 | 51 | if (removedSelfValue != null && !availableDevices.isEmpty()) { 52 | lastUuid = deleteRandomDevice(availableDevices) 53 | mutableData.value = availableDevices 54 | } 55 | 56 | return Transaction.success(mutableData) 57 | } 58 | 59 | private fun deleteRandomDevice(availableDevices: MutableMap): String { 60 | val devicesCount = availableDevices.count() 61 | val randomDevicePosition = SecureRandom().nextInt(devicesCount) 62 | val randomDeviceToRemoveUuid = availableDevices.keys.toList()[randomDevicePosition] 63 | Timber.d("Device number $randomDevicePosition from $devicesCount devices was chosen.") 64 | availableDevices.remove(randomDeviceToRemoveUuid) 65 | return randomDeviceToRemoveUuid 66 | } 67 | 68 | override fun onComplete(databaseError: DatabaseError?, completed: Boolean, p2: DataSnapshot?) { 69 | if (databaseError != null) { 70 | it.onError(databaseError.toException()) 71 | } else if (completed && lastUuid != null) { 72 | it.onSuccess(lastUuid as String) 73 | } 74 | it.onComplete() 75 | } 76 | }) 77 | } 78 | } -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/data/model/IceCandidateFirebase.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.data.model 2 | 3 | import org.webrtc.IceCandidate 4 | 5 | 6 | data class IceCandidateFirebase(val sdpMLineIndex: Int? = null, val sdpMid: String? = null, val sdp: String? = null) { 7 | 8 | companion object { 9 | fun createFromIceCandidate(iceCandidate: IceCandidate): IceCandidateFirebase = 10 | IceCandidateFirebase(sdpMLineIndex = iceCandidate.sdpMLineIndex, sdpMid = iceCandidate.sdpMid, sdp = iceCandidate.sdp) 11 | } 12 | 13 | fun toIceCandidate() = IceCandidate(sdpMid, sdpMLineIndex ?: -1, sdp) 14 | 15 | } -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/data/model/IceServerFirebase.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.data.model 2 | 3 | import org.webrtc.PeerConnection 4 | 5 | 6 | @Suppress("MemberVisibilityCanPrivate")//Firebase model - members cant be private 7 | data class IceServerFirebase(val uri: String? = null, val username: String? = null, val password: String? = null) { 8 | 9 | fun toIceServer(): PeerConnection.IceServer { 10 | return if (username == null || password == null) { 11 | PeerConnection.IceServer(uri) 12 | } else { 13 | PeerConnection.IceServer(uri, username, password) 14 | } 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/data/model/RouletteConnectionFirebase.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.data.model 2 | 3 | 4 | data class RouletteConnectionFirebase(val inCall: Boolean = false) 5 | -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/data/model/RouletteDisconnectOrderFirebase.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.data.model 2 | 3 | 4 | data class RouletteDisconnectOrderFirebase(val from: String) -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/data/model/SessionDescriptionFirebase.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.data.model 2 | 3 | import co.netguru.android.chatandroll.app.App 4 | import org.webrtc.SessionDescription 5 | 6 | 7 | data class SessionDescriptionFirebase(val senderUuid: String = App.CURRENT_DEVICE_UUID, 8 | val type: SessionDescription.Type? = null, 9 | val description: String? = null) { 10 | companion object { 11 | fun fromSessionDescriptionWithDefaultSenderUuid(sessionDescription: SessionDescription): SessionDescriptionFirebase = 12 | SessionDescriptionFirebase(type = sessionDescription.type, description = sessionDescription.description) 13 | } 14 | 15 | fun toSessionDescription() = SessionDescription(type, description) 16 | } -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/feature/base/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.feature.base 2 | 3 | import android.os.Bundle 4 | import android.support.annotation.LayoutRes 5 | import android.support.v4.app.Fragment 6 | import android.support.v4.app.FragmentTransaction 7 | import android.support.v7.app.AppCompatActivity 8 | 9 | abstract class BaseActivity : AppCompatActivity() { 10 | 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | setContentView(getLayoutId()) 14 | } 15 | 16 | @LayoutRes 17 | abstract fun getLayoutId(): Int 18 | 19 | fun getReplaceFragmentTransaction(containerViewId: Int, fragment: Fragment, tag: String): FragmentTransaction { 20 | val fragmentTransaction = supportFragmentManager.beginTransaction() 21 | fragmentTransaction.replace(containerViewId, fragment, tag) 22 | return fragmentTransaction 23 | } 24 | } -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/feature/base/BaseFragment.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.feature.base 2 | 3 | import android.os.Bundle 4 | import android.support.annotation.LayoutRes 5 | import android.support.annotation.StringRes 6 | import android.support.design.widget.BaseTransientBottomBar 7 | import android.support.design.widget.Snackbar 8 | import android.support.v4.app.Fragment 9 | import android.view.LayoutInflater 10 | import android.view.View 11 | import android.view.ViewGroup 12 | import co.netguru.android.chatandroll.R 13 | import co.netguru.android.chatandroll.common.extension.getColorCompat 14 | 15 | abstract class BaseFragment : Fragment() { 16 | 17 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = 18 | inflater.inflate(getLayoutId(), container, false) 19 | 20 | @LayoutRes 21 | abstract fun getLayoutId(): Int 22 | 23 | fun showSnackbarMessage(@StringRes resId: Int, @BaseTransientBottomBar.Duration duration: Int) { 24 | view?.let { 25 | val snackbar = Snackbar.make(it, resId, duration) 26 | val layout = snackbar.view as Snackbar.SnackbarLayout 27 | layout.setBackgroundColor(context.getColorCompat(R.color.transparent_black)) 28 | snackbar.show() 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/feature/base/BaseMvpActivity.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.feature.base 2 | 3 | import android.os.Bundle 4 | 5 | 6 | abstract class BaseMvpActivity> : BaseActivity() { 7 | 8 | private lateinit var presenter: P 9 | 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | super.onCreate(savedInstanceState) 12 | attachViewToPresenter() 13 | } 14 | 15 | override fun onDestroy() { 16 | presenter.detachView() 17 | super.onDestroy() 18 | } 19 | 20 | @Suppress("UNCHECKED_CAST") 21 | private fun attachViewToPresenter() { 22 | presenter = retrievePresenter() 23 | presenter.attachView(this as T) 24 | } 25 | 26 | abstract fun retrievePresenter(): P 27 | 28 | fun getPresenter(): P = presenter 29 | } -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/feature/base/BaseMvpFragment.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.feature.base 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | 6 | 7 | abstract class BaseMvpFragment> : BaseFragment() { 8 | 9 | private lateinit var presenter: P 10 | 11 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 12 | super.onViewCreated(view, savedInstanceState) 13 | attachViewToPresenter() 14 | } 15 | 16 | override fun onDestroyView() { 17 | presenter.detachView() 18 | super.onDestroyView() 19 | } 20 | 21 | @Suppress("UNCHECKED_CAST") 22 | private fun attachViewToPresenter() { 23 | presenter = retrievePresenter() 24 | presenter.attachView(this as T) 25 | } 26 | 27 | abstract fun retrievePresenter(): P 28 | 29 | fun getPresenter(): P = presenter 30 | 31 | } -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/feature/base/BasePresenter.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.feature.base 2 | 3 | 4 | abstract class BasePresenter : Presenter { 5 | 6 | //ToDo 16.08.2017 can be reverted to old form when https://youtrack.jetbrains.com/issue/KT-19306 is fixed 7 | private var mvpView: T? = null 8 | 9 | override fun attachView(mvpView: T) { 10 | this.mvpView = mvpView 11 | } 12 | 13 | override fun detachView() { 14 | mvpView = null 15 | } 16 | 17 | override fun getView() = mvpView 18 | 19 | } -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/feature/base/MvpView.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.feature.base 2 | 3 | 4 | interface MvpView -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/feature/base/Presenter.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.feature.base 2 | 3 | 4 | interface Presenter { 5 | 6 | fun attachView(mvpView: T) 7 | 8 | fun detachView() 9 | 10 | fun getView(): T? 11 | 12 | } -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/feature/base/service/BaseServiceController.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.feature.base.service 2 | 3 | 4 | abstract class BaseServiceController : ServiceController { 5 | private var serviceFacade: T? = null 6 | 7 | override fun attachService(service: T) { 8 | serviceFacade = service 9 | } 10 | 11 | override fun detachService() { 12 | serviceFacade = null 13 | } 14 | 15 | override fun getService() = serviceFacade 16 | } -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/feature/base/service/BaseServiceWithFacade.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.feature.base.service 2 | 3 | import android.app.Service 4 | 5 | 6 | abstract class BaseServiceWithFacade> : Service() { 7 | 8 | private lateinit var serviceController: C 9 | 10 | override fun onCreate() { 11 | super.onCreate() 12 | attachServiceToController() 13 | } 14 | 15 | override fun onDestroy() { 16 | super.onDestroy() 17 | serviceController.detachService() 18 | } 19 | 20 | @Suppress("UNCHECKED_CAST") 21 | private fun attachServiceToController() { 22 | serviceController = retrieveController() 23 | serviceController.attachService(this as T) 24 | } 25 | 26 | abstract fun retrieveController(): C 27 | } -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/feature/base/service/ServiceController.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.feature.base.service 2 | 3 | 4 | interface ServiceController { 5 | 6 | fun attachService(service: T) 7 | 8 | fun detachService() 9 | 10 | fun getService(): T? 11 | 12 | } -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/feature/base/service/ServiceFacade.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.feature.base.service 2 | 3 | 4 | interface ServiceFacade -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/feature/main/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.feature.main 2 | 3 | import android.os.Bundle 4 | import co.netguru.android.chatandroll.R 5 | import co.netguru.android.chatandroll.feature.base.BaseActivity 6 | import co.netguru.android.chatandroll.feature.main.video.VideoFragment 7 | 8 | 9 | class MainActivity : BaseActivity() { 10 | 11 | private val videoFragment = VideoFragment.newInstance() 12 | 13 | override fun getLayoutId() = R.layout.activity_main 14 | 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | if (savedInstanceState == null) { 18 | getReplaceFragmentTransaction(R.id.fragmentContainer, videoFragment, VideoFragment.TAG).commit() 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/feature/main/video/MoveUpBehavior.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.feature.main.video 2 | 3 | import android.support.design.widget.CoordinatorLayout 4 | import android.view.Gravity 5 | import android.view.View 6 | 7 | 8 | class MoveUpBehavior : CoordinatorLayout.Behavior() { 9 | 10 | override fun onAttachedToLayoutParams(lp: CoordinatorLayout.LayoutParams) { 11 | if (lp.dodgeInsetEdges == Gravity.NO_GRAVITY) { 12 | lp.dodgeInsetEdges = Gravity.BOTTOM 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/feature/main/video/VideoFragment.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.feature.main.video 2 | 3 | import android.Manifest 4 | import android.content.ActivityNotFoundException 5 | import android.content.ComponentName 6 | import android.content.ServiceConnection 7 | import android.content.pm.PackageManager 8 | import android.media.AudioManager 9 | import android.os.Bundle 10 | import android.os.IBinder 11 | import android.support.design.widget.CoordinatorLayout 12 | import android.support.design.widget.Snackbar 13 | import android.view.View 14 | import android.view.animation.OvershootInterpolator 15 | import co.netguru.android.chatandroll.R 16 | import co.netguru.android.chatandroll.app.App 17 | import co.netguru.android.chatandroll.common.extension.areAllPermissionsGranted 18 | import co.netguru.android.chatandroll.common.extension.startAppSettings 19 | import co.netguru.android.chatandroll.feature.base.BaseMvpFragment 20 | import co.netguru.android.chatandroll.webrtc.service.WebRtcService 21 | import co.netguru.android.chatandroll.webrtc.service.WebRtcServiceListener 22 | import kotlinx.android.synthetic.main.fragment_video.* 23 | import org.webrtc.PeerConnection 24 | import timber.log.Timber 25 | 26 | 27 | class VideoFragment : BaseMvpFragment(), VideoFragmentView, WebRtcServiceListener { 28 | 29 | companion object { 30 | val TAG: String = VideoFragment::class.java.name 31 | 32 | fun newInstance() = VideoFragment() 33 | 34 | private const val KEY_IN_CHAT = "key:in_chat" 35 | private const val CHECK_PERMISSIONS_AND_CONNECT_REQUEST_CODE = 1 36 | private val NECESSARY_PERMISSIONS = arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO) 37 | private const val CONNECT_BUTTON_ANIMATION_DURATION_MS = 500L 38 | } 39 | 40 | private lateinit var serviceConnection: ServiceConnection 41 | 42 | override fun getLayoutId() = R.layout.fragment_video 43 | 44 | override fun retrievePresenter() = App.getApplicationComponent(context).videoFragmentComponent().videoFragmentPresenter() 45 | 46 | var service: WebRtcService? = null 47 | 48 | override val remoteUuid 49 | get() = service?.getRemoteUuid() 50 | 51 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 52 | super.onViewCreated(view, savedInstanceState) 53 | (buttonPanel.layoutParams as CoordinatorLayout.LayoutParams).behavior = MoveUpBehavior() 54 | (localVideoView.layoutParams as CoordinatorLayout.LayoutParams).behavior = MoveUpBehavior() 55 | activity.volumeControlStream = AudioManager.STREAM_VOICE_CALL 56 | 57 | if (savedInstanceState?.getBoolean(KEY_IN_CHAT) == true) { 58 | initAlreadyRunningConnection() 59 | } 60 | connectButton.setOnClickListener { 61 | checkPermissionsAndConnect() 62 | } 63 | 64 | disconnectButton.setOnClickListener { 65 | getPresenter().disconnectByUser() 66 | } 67 | 68 | switchCameraButton.setOnClickListener { 69 | service?.switchCamera() 70 | } 71 | 72 | cameraEnabledToggle.setOnCheckedChangeListener { _, enabled -> 73 | service?.enableCamera(enabled) 74 | } 75 | 76 | microphoneEnabledToggle.setOnCheckedChangeListener { _, enabled -> 77 | service?.enableMicrophone(enabled) 78 | } 79 | } 80 | 81 | override fun onStart() { 82 | super.onStart() 83 | service?.hideBackgroundWorkWarning() 84 | } 85 | 86 | override fun onStop() { 87 | super.onStop() 88 | if (!activity.isChangingConfigurations) { 89 | service?.showBackgroundWorkWarning() 90 | } 91 | } 92 | 93 | override fun onDestroyView() { 94 | super.onDestroyView() 95 | service?.let { 96 | it.detachViews() 97 | unbindService() 98 | } 99 | } 100 | 101 | override fun onSaveInstanceState(outState: Bundle) { 102 | super.onSaveInstanceState(outState) 103 | if (remoteVideoView.visibility == View.VISIBLE) { 104 | outState.putBoolean(KEY_IN_CHAT, true) 105 | } 106 | } 107 | 108 | override fun onDestroy() { 109 | super.onDestroy() 110 | if (!activity.isChangingConfigurations) disconnect() 111 | } 112 | 113 | override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) = when (requestCode) { 114 | CHECK_PERMISSIONS_AND_CONNECT_REQUEST_CODE -> { 115 | val grantResult = grantResults.isNotEmpty() && grantResults.all { it == PackageManager.PERMISSION_GRANTED } 116 | if (grantResult) { 117 | checkPermissionsAndConnect() 118 | } else { 119 | showNoPermissionsSnackbar() 120 | } 121 | } 122 | else -> { 123 | error("Unknown permission request code $requestCode") 124 | } 125 | } 126 | 127 | override fun attachService() { 128 | serviceConnection = object : ServiceConnection { 129 | override fun onServiceConnected(componentName: ComponentName, iBinder: IBinder) { 130 | onWebRtcServiceConnected((iBinder as (WebRtcService.LocalBinder)).service) 131 | getPresenter().startRoulette() 132 | } 133 | 134 | override fun onServiceDisconnected(componentName: ComponentName) { 135 | onWebRtcServiceDisconnected() 136 | } 137 | } 138 | startAndBindWebRTCService(serviceConnection) 139 | } 140 | 141 | override fun criticalWebRTCServiceException(throwable: Throwable) { 142 | unbindService() 143 | showSnackbarMessage(R.string.error_web_rtc_error, Snackbar.LENGTH_LONG) 144 | Timber.e(throwable, "Critical WebRTC service error") 145 | } 146 | 147 | override fun connectionStateChange(iceConnectionState: PeerConnection.IceConnectionState) { 148 | getPresenter().connectionStateChange(iceConnectionState) 149 | } 150 | 151 | override fun connectTo(uuid: String) { 152 | service?.offerDevice(uuid) 153 | } 154 | 155 | override fun disconnect() { 156 | service?.let { 157 | it.stopSelf() 158 | unbindService() 159 | } 160 | } 161 | 162 | private fun unbindService() { 163 | service?.let { 164 | it.detachServiceActionsListener() 165 | context.unbindService(serviceConnection) 166 | service = null 167 | } 168 | } 169 | 170 | override fun showCamViews() { 171 | buttonPanel.visibility = View.VISIBLE 172 | remoteVideoView.visibility = View.VISIBLE 173 | localVideoView.visibility = View.VISIBLE 174 | connectButton.visibility = View.GONE 175 | } 176 | 177 | override fun showStartRouletteView() { 178 | buttonPanel.visibility = View.GONE 179 | remoteVideoView.visibility = View.GONE 180 | localVideoView.visibility = View.GONE 181 | connectButton.visibility = View.VISIBLE 182 | } 183 | 184 | override fun showErrorWhileChoosingRandom() { 185 | showSnackbarMessage(R.string.error_choosing_random_partner, Snackbar.LENGTH_LONG) 186 | } 187 | 188 | override fun showNoOneAvailable() { 189 | showSnackbarMessage(R.string.msg_no_one_available, Snackbar.LENGTH_LONG) 190 | } 191 | 192 | override fun showLookingForPartnerMessage() { 193 | showSnackbarMessage(R.string.msg_looking_for_partner, Snackbar.LENGTH_SHORT) 194 | } 195 | 196 | override fun hideConnectButtonWithAnimation() { 197 | connectButton.animate().scaleX(0f).scaleY(0f) 198 | .setInterpolator(OvershootInterpolator()) 199 | .setDuration(CONNECT_BUTTON_ANIMATION_DURATION_MS) 200 | .withStartAction { connectButton.isClickable = false } 201 | .withEndAction { 202 | connectButton.isClickable = true 203 | connectButton.visibility = View.GONE 204 | connectButton.scaleX = 1f 205 | connectButton.scaleY = 1f 206 | } 207 | .start() 208 | } 209 | 210 | override fun showOtherPartyFinished() { 211 | showSnackbarMessage(R.string.msg_other_party_finished, Snackbar.LENGTH_SHORT) 212 | } 213 | 214 | override fun showConnectedMsg() { 215 | showSnackbarMessage(R.string.msg_connected_to_other_party, Snackbar.LENGTH_LONG) 216 | } 217 | 218 | override fun showWillTryToRestartMsg() { 219 | showSnackbarMessage(R.string.msg_will_try_to_restart_msg, Snackbar.LENGTH_LONG) 220 | } 221 | 222 | private fun initAlreadyRunningConnection() { 223 | showCamViews() 224 | serviceConnection = object : ServiceConnection { 225 | override fun onServiceConnected(componentName: ComponentName, iBinder: IBinder) { 226 | onWebRtcServiceConnected((iBinder as (WebRtcService.LocalBinder)).service) 227 | getPresenter().listenForDisconnectOrders() 228 | } 229 | 230 | override fun onServiceDisconnected(componentName: ComponentName) { 231 | onWebRtcServiceDisconnected() 232 | } 233 | } 234 | startAndBindWebRTCService(serviceConnection) 235 | } 236 | 237 | private fun startAndBindWebRTCService(serviceConnection: ServiceConnection) { 238 | WebRtcService.startService(context) 239 | WebRtcService.bindService(context, serviceConnection) 240 | } 241 | 242 | private fun checkPermissionsAndConnect() { 243 | if (context.areAllPermissionsGranted(*NECESSARY_PERMISSIONS)) { 244 | getPresenter().connect() 245 | } else { 246 | requestPermissions(arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO), CHECK_PERMISSIONS_AND_CONNECT_REQUEST_CODE) 247 | } 248 | } 249 | 250 | private fun showNoPermissionsSnackbar() { 251 | view?.let { 252 | Snackbar.make(it, R.string.msg_permissions, Snackbar.LENGTH_LONG) 253 | .setAction(R.string.action_settings) { 254 | try { 255 | context.startAppSettings() 256 | } catch (e: ActivityNotFoundException) { 257 | showSnackbarMessage(R.string.error_permissions_couldnt_start_settings, Snackbar.LENGTH_LONG) 258 | } 259 | } 260 | .show() 261 | } 262 | } 263 | 264 | private fun onWebRtcServiceConnected(service: WebRtcService) { 265 | Timber.d("Service connected") 266 | this.service = service 267 | service.attachLocalView(localVideoView) 268 | service.attachRemoteView(remoteVideoView) 269 | syncButtonsState(service) 270 | service.attachServiceActionsListener(webRtcServiceListener = this) 271 | } 272 | 273 | private fun syncButtonsState(service: WebRtcService) { 274 | cameraEnabledToggle.isChecked = service.isCameraEnabled() 275 | microphoneEnabledToggle.isChecked = service.isMicrophoneEnabled() 276 | } 277 | 278 | private fun onWebRtcServiceDisconnected() { 279 | Timber.d("Service disconnected") 280 | } 281 | } -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/feature/main/video/VideoFragmentComponent.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.feature.main.video 2 | 3 | import co.netguru.android.chatandroll.common.di.FragmentScope 4 | import dagger.Subcomponent 5 | 6 | @FragmentScope 7 | @Subcomponent 8 | interface VideoFragmentComponent { 9 | fun inject(videoFragment: VideoFragment) 10 | 11 | fun videoFragmentPresenter(): VideoFragmentPresenter 12 | } -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/feature/main/video/VideoFragmentPresenter.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.feature.main.video 2 | 3 | import co.netguru.android.chatandroll.common.util.RxUtils 4 | import co.netguru.android.chatandroll.data.firebase.FirebaseSignalingDisconnect 5 | import co.netguru.android.chatandroll.data.firebase.FirebaseSignalingOnline 6 | import co.netguru.android.chatandroll.feature.base.BasePresenter 7 | import io.reactivex.disposables.CompositeDisposable 8 | import io.reactivex.disposables.Disposable 9 | import io.reactivex.disposables.Disposables 10 | import io.reactivex.rxkotlin.plusAssign 11 | import io.reactivex.rxkotlin.subscribeBy 12 | import org.webrtc.PeerConnection 13 | import timber.log.Timber 14 | import javax.inject.Inject 15 | 16 | 17 | class VideoFragmentPresenter @Inject constructor( 18 | private val firebaseSignalingOnline: FirebaseSignalingOnline, 19 | private val firebaseSignalingDisconnect: FirebaseSignalingDisconnect 20 | ) : BasePresenter() { 21 | 22 | private val disposables = CompositeDisposable() 23 | private var disconnectOrdersSubscription: Disposable = Disposables.disposed() 24 | 25 | override fun detachView() { 26 | super.detachView() 27 | disposables.dispose() 28 | disconnectOrdersSubscription.dispose() 29 | } 30 | 31 | fun startRoulette() { 32 | disposables += firebaseSignalingOnline.connect() 33 | .andThen(firebaseSignalingDisconnect.cleanDisconnectOrders()) 34 | .doOnComplete { listenForDisconnectOrders() } 35 | .andThen(firebaseSignalingOnline.setOnlineAndRetrieveRandomDevice()) 36 | .compose(RxUtils.applyMaybeIoSchedulers()) 37 | .subscribeBy( 38 | onSuccess = { 39 | Timber.d("Next $it") 40 | getView()?.showCamViews() 41 | getView()?.connectTo(it) 42 | }, 43 | onError = { 44 | Timber.e(it, "Error while choosing random") 45 | getView()?.showErrorWhileChoosingRandom() 46 | }, 47 | onComplete = { 48 | Timber.d("Done") 49 | getView()?.showCamViews() 50 | getView()?.showNoOneAvailable() 51 | } 52 | ) 53 | 54 | } 55 | 56 | fun listenForDisconnectOrders() { 57 | disconnectOrdersSubscription = firebaseSignalingDisconnect.cleanDisconnectOrders() 58 | .andThen(firebaseSignalingDisconnect.listenForDisconnectOrders()) 59 | .compose(RxUtils.applyFlowableIoSchedulers()) 60 | .subscribeBy( 61 | onNext = { 62 | Timber.d("Disconnect order") 63 | getView()?.showOtherPartyFinished() 64 | disconnect() 65 | } 66 | ) 67 | } 68 | 69 | private fun disconnect() { 70 | disposables += firebaseSignalingOnline.disconnect() 71 | .compose(RxUtils.applyCompletableIoSchedulers()) 72 | .subscribeBy( 73 | onError = { 74 | Timber.d(it) 75 | }, 76 | onComplete = { 77 | disconnectOrdersSubscription.dispose() 78 | getView()?.disconnect() 79 | getView()?.showStartRouletteView() 80 | } 81 | ) 82 | 83 | } 84 | 85 | fun connect() = getView()?.run { 86 | attachService() 87 | showLookingForPartnerMessage() 88 | hideConnectButtonWithAnimation() 89 | } 90 | 91 | fun disconnectByUser() { 92 | val remoteUuid = getView()?.remoteUuid 93 | if (remoteUuid != null) { 94 | disposables += firebaseSignalingDisconnect.sendDisconnectOrderToOtherParty(remoteUuid) 95 | .compose(RxUtils.applyCompletableIoSchedulers()) 96 | .subscribeBy( 97 | onComplete = { 98 | disconnect() 99 | } 100 | ) 101 | } else { 102 | disconnect() 103 | } 104 | 105 | } 106 | 107 | fun connectionStateChange(iceConnectionState: PeerConnection.IceConnectionState) { 108 | Timber.d("Ice connection state changed: $iceConnectionState") 109 | when (iceConnectionState) { 110 | PeerConnection.IceConnectionState.CONNECTED -> { 111 | getView()?.showConnectedMsg() 112 | } 113 | PeerConnection.IceConnectionState.DISCONNECTED -> { 114 | getView()?.showWillTryToRestartMsg() 115 | } 116 | else -> { 117 | //no-op for now - could show or hide progress bars or messages on given event 118 | } 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/feature/main/video/VideoFragmentView.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.feature.main.video 2 | 3 | import co.netguru.android.chatandroll.feature.base.MvpView 4 | 5 | interface VideoFragmentView : MvpView { 6 | val remoteUuid: String? 7 | 8 | fun connectTo(uuid: String) 9 | fun showCamViews() 10 | fun showStartRouletteView() 11 | fun disconnect() 12 | fun attachService() 13 | fun showErrorWhileChoosingRandom() 14 | fun showNoOneAvailable() 15 | fun showLookingForPartnerMessage() 16 | fun showOtherPartyFinished() 17 | fun showConnectedMsg() 18 | fun showWillTryToRestartMsg() 19 | fun hideConnectButtonWithAnimation() 20 | } -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/webrtc/service/WebRtcService.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.webrtc.service 2 | 3 | import android.app.PendingIntent 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.content.ServiceConnection 7 | import android.os.Binder 8 | import android.support.v4.app.NotificationCompat 9 | import android.support.v4.app.NotificationManagerCompat 10 | import co.netguru.android.chatandroll.R 11 | import co.netguru.android.chatandroll.app.App 12 | import co.netguru.android.chatandroll.common.extension.getColorCompat 13 | import co.netguru.android.chatandroll.feature.base.service.BaseServiceWithFacade 14 | import co.netguru.android.chatandroll.feature.main.MainActivity 15 | import org.webrtc.SurfaceViewRenderer 16 | import timber.log.Timber 17 | import javax.inject.Inject 18 | 19 | 20 | class WebRtcService : BaseServiceWithFacade(), WebRtcServiceFacade { 21 | 22 | companion object { 23 | fun startService(packageContext: Context) { 24 | packageContext.startService(Intent(packageContext, WebRtcService::class.java)) 25 | } 26 | 27 | fun bindService(context: Context, connection: ServiceConnection) { 28 | context.bindService(Intent(context, WebRtcService::class.java), connection, 0) 29 | } 30 | 31 | private val BACKGROUND_WORK_NOTIFICATION_ID = 1 32 | private val PENDING_INTENT_REQUEST_CODE = 1 33 | } 34 | 35 | @Inject lateinit var webRtcServiceController: WebRtcServiceController 36 | 37 | private val binder = LocalBinder() 38 | 39 | private val notificationManager by lazy { 40 | NotificationManagerCompat.from(this) 41 | } 42 | 43 | override fun onBind(intent: Intent) = binder 44 | 45 | override fun onCreate() { 46 | App.getApplicationComponent(this).webRtcServiceComponent().inject(this) 47 | Timber.d("WebRtc service created") 48 | super.onCreate() 49 | } 50 | 51 | override fun onDestroy() { 52 | super.onDestroy() 53 | hideBackgroundWorkWarning() 54 | } 55 | 56 | override fun retrieveController(): WebRtcServiceController = webRtcServiceController 57 | 58 | override fun stop() = stopSelf() 59 | 60 | fun attachServiceActionsListener(webRtcServiceListener: WebRtcServiceListener) { 61 | webRtcServiceController.serviceListener = webRtcServiceListener 62 | } 63 | 64 | fun detachServiceActionsListener() { 65 | webRtcServiceController.serviceListener = null 66 | } 67 | 68 | fun offerDevice(deviceUuid: String) { 69 | webRtcServiceController.offerDevice(deviceUuid) 70 | } 71 | 72 | fun attachRemoteView(remoteView: SurfaceViewRenderer) { 73 | webRtcServiceController.attachRemoteView(remoteView) 74 | } 75 | 76 | fun attachLocalView(localView: SurfaceViewRenderer) { 77 | webRtcServiceController.attachLocalView(localView) 78 | } 79 | 80 | fun detachViews() { 81 | webRtcServiceController.detachViews() 82 | } 83 | 84 | fun getRemoteUuid() = webRtcServiceController.remoteUuid 85 | 86 | fun switchCamera() = webRtcServiceController.switchCamera() 87 | 88 | fun enableCamera(isEnabled: Boolean) { 89 | webRtcServiceController.enableCamera(isEnabled) 90 | } 91 | 92 | fun isCameraEnabled() = webRtcServiceController.isCameraEnabled() 93 | 94 | fun enableMicrophone(isEnabled: Boolean) { 95 | webRtcServiceController.enableMicrophone(isEnabled) 96 | } 97 | 98 | fun isMicrophoneEnabled() = webRtcServiceController.isMicrophoneEnabled() 99 | 100 | fun showBackgroundWorkWarning() { 101 | val mainActivityIntent = Intent(this, MainActivity::class.java).apply { 102 | action = Intent.ACTION_MAIN 103 | addCategory(Intent.CATEGORY_LAUNCHER) 104 | } 105 | val pendingMainActivityIntent = PendingIntent.getActivity(this, PENDING_INTENT_REQUEST_CODE, mainActivityIntent, PendingIntent.FLAG_UPDATE_CURRENT) 106 | val notification = NotificationCompat.Builder(this, App.BACKGROUND_WORK_NOTIFICATIONS_CHANNEL_ID) 107 | .setSmallIcon(R.drawable.ic_notification_small_logo) 108 | .setContentTitle(getString(R.string.ongoing_call_notification_title)) 109 | .setContentText(getString(R.string.ongoing_call_notification_text)) 110 | .setColor(getColorCompat(R.color.accent)) 111 | .setContentIntent(pendingMainActivityIntent) 112 | .setOngoing(true) 113 | .setDefaults(NotificationCompat.DEFAULT_VIBRATE) 114 | .build() 115 | notificationManager.notify(BACKGROUND_WORK_NOTIFICATION_ID, notification) 116 | } 117 | 118 | fun hideBackgroundWorkWarning() { 119 | notificationManager.cancel(BACKGROUND_WORK_NOTIFICATION_ID) 120 | } 121 | 122 | inner class LocalBinder : Binder() { 123 | val service: WebRtcService 124 | get() = this@WebRtcService 125 | } 126 | } 127 | 128 | -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/webrtc/service/WebRtcServiceComponent.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.webrtc.service 2 | 3 | import co.netguru.android.chatandroll.common.di.ServiceScope 4 | import dagger.Subcomponent 5 | 6 | @ServiceScope 7 | @Subcomponent 8 | interface WebRtcServiceComponent { 9 | 10 | fun inject(webRtcService: WebRtcService) 11 | 12 | } -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/webrtc/service/WebRtcServiceController.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.webrtc.service 2 | 3 | import android.os.Handler 4 | import android.os.Looper 5 | import co.netguru.videochatguru.PeerConnectionListener 6 | import co.netguru.videochatguru.WebRtcAnsweringPartyListener 7 | import co.netguru.videochatguru.WebRtcClient 8 | import co.netguru.videochatguru.WebRtcOfferingActionListener 9 | import co.netguru.android.chatandroll.common.extension.ChildEventAdded 10 | import co.netguru.android.chatandroll.common.util.RxUtils 11 | import co.netguru.android.chatandroll.data.firebase.FirebaseIceCandidates 12 | import co.netguru.android.chatandroll.data.firebase.FirebaseIceServers 13 | import co.netguru.android.chatandroll.data.firebase.FirebaseSignalingAnswers 14 | import co.netguru.android.chatandroll.data.firebase.FirebaseSignalingOffers 15 | import co.netguru.android.chatandroll.feature.base.service.BaseServiceController 16 | import io.reactivex.disposables.CompositeDisposable 17 | import io.reactivex.rxkotlin.plusAssign 18 | import io.reactivex.rxkotlin.subscribeBy 19 | import org.webrtc.IceCandidate 20 | import org.webrtc.PeerConnection 21 | import org.webrtc.SessionDescription 22 | import org.webrtc.SurfaceViewRenderer 23 | import timber.log.Timber 24 | import javax.inject.Inject 25 | 26 | 27 | class WebRtcServiceController @Inject constructor( 28 | private val webRtcClient: WebRtcClient, 29 | private val firebaseSignalingAnswers: FirebaseSignalingAnswers, 30 | private val firebaseSignalingOffers: FirebaseSignalingOffers, 31 | private val firebaseIceCandidates: FirebaseIceCandidates, 32 | private val firebaseIceServers: FirebaseIceServers) : BaseServiceController() { 33 | 34 | var serviceListener: WebRtcServiceListener? = null 35 | var remoteUuid: String? = null 36 | val mainThreadHandler = Handler(Looper.getMainLooper()) 37 | 38 | private val disposables = CompositeDisposable() 39 | 40 | private var finishedInitializing = false 41 | private var shouldCreateOffer = false 42 | private var isOfferingParty = false 43 | 44 | override fun attachService(service: WebRtcServiceFacade) { 45 | super.attachService(service) 46 | loadIceServers() 47 | } 48 | 49 | override fun detachService() { 50 | super.detachService() 51 | disposables.dispose() 52 | webRtcClient.detachViews() 53 | webRtcClient.dispose() 54 | } 55 | 56 | fun offerDevice(deviceUuid: String) { 57 | isOfferingParty = true 58 | this.remoteUuid = deviceUuid 59 | listenForIceCandidates(deviceUuid) 60 | if (finishedInitializing) webRtcClient.createOffer() else shouldCreateOffer = true 61 | } 62 | 63 | fun attachRemoteView(remoteView: SurfaceViewRenderer) { 64 | webRtcClient.attachRemoteView(remoteView) 65 | } 66 | 67 | fun attachLocalView(localView: SurfaceViewRenderer) { 68 | webRtcClient.attachLocalView(localView) 69 | } 70 | 71 | fun detachViews() { 72 | webRtcClient.detachViews() 73 | } 74 | 75 | fun switchCamera() = webRtcClient.switchCamera() 76 | 77 | fun enableCamera(isEnabled: Boolean) { 78 | webRtcClient.cameraEnabled = isEnabled 79 | } 80 | 81 | fun isCameraEnabled() = webRtcClient.cameraEnabled 82 | 83 | fun enableMicrophone(enabled: Boolean) { 84 | webRtcClient.microphoneEnabled = enabled 85 | } 86 | 87 | fun isMicrophoneEnabled() = webRtcClient.microphoneEnabled 88 | 89 | private fun loadIceServers() { 90 | disposables += firebaseIceServers.getIceServers() 91 | .subscribeBy( 92 | onSuccess = { 93 | listenForOffers() 94 | initializeWebRtc(it) 95 | }, 96 | onError = { 97 | handleCriticalException(it) 98 | } 99 | ) 100 | } 101 | 102 | private fun initializeWebRtc(iceServers: List) { 103 | webRtcClient.initializePeerConnection(iceServers, 104 | peerConnectionListener = object : PeerConnectionListener { 105 | override fun onIceConnectionChange(iceConnectionState: PeerConnection.IceConnectionState) { 106 | if (iceConnectionState == PeerConnection.IceConnectionState.DISCONNECTED && isOfferingParty) { 107 | webRtcClient.restart() 108 | } 109 | mainThreadHandler.post { 110 | serviceListener?.connectionStateChange(iceConnectionState) 111 | } 112 | 113 | } 114 | 115 | override fun onIceCandidate(iceCandidate: IceCandidate) { 116 | sendIceCandidate(iceCandidate) 117 | } 118 | 119 | override fun onIceCandidatesRemoved(iceCandidates: Array) { 120 | removeIceCandidates(iceCandidates) 121 | } 122 | 123 | }, 124 | webRtcOfferingActionListener = object : WebRtcOfferingActionListener { 125 | override fun onError(error: String) { 126 | Timber.e("Error in offering party: $error") 127 | } 128 | 129 | override fun onOfferRemoteDescription(localSessionDescription: SessionDescription) { 130 | listenForAnswers() 131 | sendOffer(localSessionDescription) 132 | } 133 | 134 | }, 135 | webRtcAnsweringPartyListener = object : WebRtcAnsweringPartyListener { 136 | override fun onError(error: String) { 137 | Timber.e("Error in answering party: $error") 138 | } 139 | 140 | override fun onSuccess(localSessionDescription: SessionDescription) { 141 | sendAnswer(localSessionDescription) 142 | } 143 | }) 144 | if (shouldCreateOffer) webRtcClient.createOffer() 145 | finishedInitializing = true 146 | } 147 | 148 | 149 | private fun listenForIceCandidates(remoteUuid: String) { 150 | disposables += firebaseIceCandidates.get(remoteUuid) 151 | .compose(RxUtils.applyFlowableIoSchedulers()) 152 | .subscribeBy( 153 | onNext = { 154 | if (it is ChildEventAdded) { 155 | webRtcClient.addRemoteIceCandidate(it.data) 156 | } else { 157 | webRtcClient.removeRemoteIceCandidate(arrayOf(it.data)) 158 | } 159 | }, 160 | onError = { 161 | handleCriticalException(it) 162 | } 163 | ) 164 | } 165 | 166 | private fun sendIceCandidate(iceCandidate: IceCandidate) { 167 | disposables += firebaseIceCandidates.send(iceCandidate) 168 | .compose(RxUtils.applyCompletableIoSchedulers()) 169 | .subscribeBy( 170 | onComplete = { 171 | Timber.d("Ice message sent") 172 | }, 173 | onError = { 174 | Timber.e(it, "Error while sending message") 175 | } 176 | ) 177 | } 178 | 179 | private fun removeIceCandidates(iceCandidates: Array) { 180 | disposables += firebaseIceCandidates.remove(iceCandidates) 181 | .compose(RxUtils.applyCompletableIoSchedulers()) 182 | .subscribeBy( 183 | onComplete = { 184 | Timber.d("Ice candidates successfully removed") 185 | }, 186 | onError = { 187 | Timber.e(it, "Error while removing ice candidates") 188 | } 189 | ) 190 | } 191 | 192 | private fun sendOffer(localDescription: SessionDescription) { 193 | disposables += firebaseSignalingOffers.create( 194 | recipientUuid = remoteUuid ?: throw IllegalArgumentException("Remote uuid should be set first"), 195 | localSessionDescription = localDescription 196 | ) 197 | .compose(RxUtils.applyCompletableIoSchedulers()) 198 | .subscribeBy( 199 | onComplete = { 200 | Timber.d("description set") 201 | }, 202 | onError = { 203 | handleCriticalException(it) 204 | } 205 | ) 206 | } 207 | 208 | private fun listenForOffers() { 209 | disposables += firebaseSignalingOffers.listenForNewOffersWithUuid() 210 | .compose(RxUtils.applyFlowableIoSchedulers()) 211 | .subscribeBy( 212 | onNext = { (sessionDescription, remoteUuid) -> 213 | this.remoteUuid = remoteUuid 214 | listenForIceCandidates(remoteUuid) 215 | webRtcClient.handleRemoteOffer(sessionDescription) 216 | }, 217 | onError = { 218 | handleCriticalException(it) 219 | } 220 | ) 221 | } 222 | 223 | private fun sendAnswer(localDescription: SessionDescription) { 224 | disposables += firebaseSignalingAnswers.create( 225 | recipientUuid = remoteUuid ?: throw IllegalArgumentException("Remote uuid should be set first"), 226 | localSessionDescription = localDescription 227 | ) 228 | .compose(RxUtils.applyCompletableIoSchedulers()) 229 | .subscribeBy( 230 | onError = { 231 | handleCriticalException(it) 232 | } 233 | ) 234 | } 235 | 236 | private fun listenForAnswers() { 237 | disposables += firebaseSignalingAnswers.listenForNewAnswers() 238 | .compose(RxUtils.applyFlowableIoSchedulers()) 239 | .subscribeBy( 240 | onNext = { 241 | Timber.d("Next answer $it") 242 | webRtcClient.handleRemoteAnswer(it) 243 | }, 244 | onError = { 245 | handleCriticalException(it) 246 | } 247 | ) 248 | } 249 | 250 | private fun handleCriticalException(throwable: Throwable) { 251 | serviceListener?.criticalWebRTCServiceException(throwable) 252 | getService()?.stop() 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/webrtc/service/WebRtcServiceFacade.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.webrtc.service 2 | 3 | import co.netguru.android.chatandroll.feature.base.service.ServiceFacade 4 | 5 | 6 | interface WebRtcServiceFacade : ServiceFacade { 7 | fun stop() 8 | } -------------------------------------------------------------------------------- /sample/src/main/java/co/netguru/android/chatandroll/webrtc/service/WebRtcServiceListener.kt: -------------------------------------------------------------------------------- 1 | package co.netguru.android.chatandroll.webrtc.service 2 | 3 | import org.webrtc.PeerConnection 4 | 5 | 6 | interface WebRtcServiceListener { 7 | 8 | /** 9 | * When receiving this exception service is in unrecoverable state and will call stopSelf, bound view(if any) should unbind 10 | */ 11 | fun criticalWebRTCServiceException(throwable: Throwable) 12 | 13 | fun connectionStateChange(iceConnectionState: PeerConnection.IceConnectionState) 14 | } -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/chat_and_roll_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/videochatguru-android/e5790fec71db013aac00dac2faf15d87fc46b80a/sample/src/main/res/drawable-hdpi/chat_and_roll_logo.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_notification_small_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/videochatguru-android/e5790fec71db013aac00dac2faf15d87fc46b80a/sample/src/main/res/drawable-hdpi/ic_notification_small_logo.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/chat_and_roll_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/videochatguru-android/e5790fec71db013aac00dac2faf15d87fc46b80a/sample/src/main/res/drawable-mdpi/chat_and_roll_logo.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_notification_small_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/videochatguru-android/e5790fec71db013aac00dac2faf15d87fc46b80a/sample/src/main/res/drawable-mdpi/ic_notification_small_logo.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/chat_and_roll_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/videochatguru-android/e5790fec71db013aac00dac2faf15d87fc46b80a/sample/src/main/res/drawable-xhdpi/chat_and_roll_logo.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_notification_small_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/videochatguru-android/e5790fec71db013aac00dac2faf15d87fc46b80a/sample/src/main/res/drawable-xhdpi/ic_notification_small_logo.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/chat_and_roll_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/videochatguru-android/e5790fec71db013aac00dac2faf15d87fc46b80a/sample/src/main/res/drawable-xxhdpi/chat_and_roll_logo.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_notification_small_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/videochatguru-android/e5790fec71db013aac00dac2faf15d87fc46b80a/sample/src/main/res/drawable-xxhdpi/ic_notification_small_logo.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxxhdpi/chat_and_roll_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/videochatguru-android/e5790fec71db013aac00dac2faf15d87fc46b80a/sample/src/main/res/drawable-xxxhdpi/chat_and_roll_logo.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxxhdpi/ic_notification_small_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/videochatguru-android/e5790fec71db013aac00dac2faf15d87fc46b80a/sample/src/main/res/drawable-xxxhdpi/ic_notification_small_logo.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable/background_splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 | 13 | 14 | 17 | 20 | 21 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/call_end_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 31 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/circle_transparent_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_call_end_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_mic_off_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_mic_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_microphone_toggle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_switch_video_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_videocam_off_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_videocam_on_off_toggle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_videocam_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_videocam_white_72dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/mic_off_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/mic_on_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/rounded_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/videocam_off_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/videocam_on_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 17 | 18 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/fragment_video.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 16 | 17 | 22 | 23 | 31 | 32 |