├── .github └── workflows │ └── gradle.yml ├── .gitignore ├── .idea ├── codeStyles │ └── Project.xml ├── compiler.xml ├── encodings.xml ├── jarRepositories.xml ├── misc.xml └── runConfigurations.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── example │ │ └── android │ │ └── myagoraapplication │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── android │ │ │ └── myagoraapplication │ │ │ └── MainActivity.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── audio_toggle_active_btn.png │ │ ├── audio_toggle_btn.png │ │ ├── end_call.png │ │ ├── ic_launcher_background.xml │ │ ├── join_call.png │ │ ├── video_disabled.png │ │ ├── video_toggle_active_btn.png │ │ └── video_toggle_btn.png │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.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 │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── example │ └── android │ └── myagoraapplication │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Gradle 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 3 | 4 | name: Java CI with Gradle 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up JDK 1.8 20 | uses: actions/setup-java@v1 21 | with: 22 | java-version: 1.8 23 | - name: Grant execute permission for gradlew 24 | run: chmod +x gradlew 25 | - name: Build with Gradle 26 | run: ./gradlew build 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | *.aab 5 | 6 | # Files for the ART/Dalvik VM 7 | *.dex 8 | 9 | # Java class files 10 | *.class 11 | 12 | # Generated files 13 | bin/ 14 | gen/ 15 | out/ 16 | 17 | # Gradle files 18 | .gradle/ 19 | build/ 20 | 21 | # Local configuration file (sdk path, etc) 22 | local.properties 23 | 24 | # Proguard folder generated by Eclipse 25 | proguard/ 26 | 27 | # Log Files 28 | *.log 29 | 30 | # Android Studio Navigation editor temp files 31 | .navigation/ 32 | 33 | # Android Studio captures folder 34 | captures/ 35 | 36 | # IntelliJ 37 | *.iml 38 | .idea/workspace.xml 39 | .idea/tasks.xml 40 | .idea/gradle.xml 41 | .idea/assetWizardSettings.xml 42 | .idea/dictionaries 43 | .idea/libraries 44 | .idea/caches 45 | # Android Studio 3 in .gitignore file. 46 | .idea/caches/build_file_checksums.ser 47 | .idea/modules.xml 48 | 49 | # Keystore files 50 | # Uncomment the following lines if you do not want to check your keystore files in. 51 | #*.jks 52 | #*.keystore 53 | 54 | # External native build folder generated in Android Studio 2.2 and later 55 | .externalNativeBuild 56 | 57 | # Google Services (e.g. APIs or Firebase) 58 | # google-services.json 59 | 60 | # Freeline 61 | freeline.py 62 | freeline/ 63 | freeline_project_description.json 64 | 65 | # fastlane 66 | fastlane/report.xml 67 | fastlane/Preview.html 68 | fastlane/screenshots 69 | fastlane/test_output 70 | fastlane/readme.md 71 | 72 | # Version control 73 | vcs.xml 74 | 75 | # lint 76 | lint/intermediates/ 77 | lint/generated/ 78 | lint/outputs/ 79 | lint/tmp/ 80 | # lint/reports/ 81 | 82 | .DS_Store 83 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | xmlns:android 14 | 15 | ^$ 16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 | xmlns:.* 25 | 26 | ^$ 27 | 28 | 29 | BY_NAME 30 | 31 |
32 |
33 | 34 | 35 | 36 | .*:id 37 | 38 | http://schemas.android.com/apk/res/android 39 | 40 | 41 | 42 |
43 |
44 | 45 | 46 | 47 | .*:name 48 | 49 | http://schemas.android.com/apk/res/android 50 | 51 | 52 | 53 |
54 |
55 | 56 | 57 | 58 | name 59 | 60 | ^$ 61 | 62 | 63 | 64 |
65 |
66 | 67 | 68 | 69 | style 70 | 71 | ^$ 72 | 73 | 74 | 75 |
76 |
77 | 78 | 79 | 80 | .* 81 | 82 | ^$ 83 | 84 | 85 | BY_NAME 86 | 87 |
88 |
89 | 90 | 91 | 92 | .* 93 | 94 | http://schemas.android.com/apk/res/android 95 | 96 | 97 | ANDROID_ATTRIBUTE_ORDER 98 | 99 |
100 |
101 | 102 | 103 | 104 | .* 105 | 106 | .* 107 | 108 | 109 | BY_NAME 110 | 111 |
112 |
113 |
114 |
115 |
116 |
-------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 27 | 47 | 48 | 49 | 50 | 51 | 52 | 54 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # How to: Build a Video Chat App on Android 2 | ![](https://www.agora.io/en/wp-content/uploads/2021/03/1-to-1-video-chat-app-on-android-using-agora.png) 3 | In this repo, we’ll build a basic video chat app in 10 easy steps, using the [Agora.io Video SDK](https://docs.agora.io/en/Video/product_video?platform=All%20Platforms) for Android. 4 | 5 | ## Prerequisites ## 6 | * [Android Studio](https://developer.android.com/studio) 7 | * [Basic knowledge of Java and the Android SDK](https://developer.android.com/training/basics/firstapp) 8 | * An Agora Developer Account (see: How To Get Started with Agora)[https://www.agora.io/en/blog/how-to-get-started-with-agora?utm_source=devto&utm_medium=blog&&utm_campaign=How_To_Build_a_Video_Chat_App_on_Android] 9 | 10 | ## Step 1: Agora.io Account ## 11 | Once you finish the sign-up process, you will be redirected to the Dashboard. Open the Projects tab on the left-hand nav to see your default project’s App ID. 12 | 13 | ![Image of Agora Developer Console](https://miro.medium.com/max/2000/1*HrdtuE1BiXTH9J7-ygv9eQ.png) 14 | 15 | ## Step 2: Create an Android App ## 16 | Within Android Studio, create a new Single Activity app. 17 | 18 | ![Android Studio: Create Project Screen](https://miro.medium.com/max/1400/1*FCXsbepZT9oxu8_fhhhreg.png) 19 | 20 | ![Android Studio: Use target defaults](https://miro.medium.com/max/1400/1*t7EIjdx08Y-KNLb_T4VgDA.png) 21 | 22 | ## Step 3: Integrate the Agora SDK ## 23 | There are two ways to add the Agora Video SDK into your project. You can use [JCenter](https://mvnrepository.com/repos/jcenter) or you can manually add the SDK. For this project we'll add the project using JCenter. 24 | 25 | Add the following line in your project level `build.gradle`: 26 | ``` 27 | allprojects { 28 | repositories { 29 | ... 30 | maven { url 'https://www.jitpack.io' } 31 | ... 32 | } 33 | } 34 | ``` 35 | 36 | Add the following line in the `/app/build.gradle` file of your project: 37 | ``` 38 | dependencies { 39 | ... 40 | //Agora RTC SDK for video call 41 | implementation 'com.github.agorabuilder:native-full-sdk:3.4.1' 42 | } 43 | ``` 44 | ![Agora SDK integration using JCenter](https://miro.medium.com/max/1400/1*y4a3LlgBivMETeN0dFxBHQ.png) 45 | 46 | ## Set up Agora APP ID ### 47 | Next, its time to add your Agora.io App ID (see Step-1) to the Android project’s `Strings.xml` _(app/src/main/res/values/Strings.xml)_. 48 | ```XML 49 | 50 | Agora-Android-Video-Tutorial 51 | <#YOUR APP ID#> 52 | 53 | ``` 54 | ![Strings.xml](https://miro.medium.com/max/2000/1*gydJufAZupExvZb57u8Qgg.png) 55 | 56 | The next step is to add the appropriate permissions within `Manifest.xml` 57 | ```XML 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | ``` 67 | 68 | The final step is to prevent obfuscation of the Agora classes, while this might sound complex it’s really simple. In the `proguard-rules.pro` file, add: 69 | ``` 70 | -keep class io.agora.**{*;} 71 | ``` 72 | 73 | ![proguard-rules.pro: prevent obfuscation](https://miro.medium.com/max/2000/1*rqvu2Xo89mi6wNk8fFSsTg.png) 74 | 75 | > NOTE: Ensure that the Android NDK plugin is installed and setup for this project 76 | 77 | ## Step 4: Setup views ## 78 | Now that we have the Agora.io SDK integrated, let’s set up our UI. I will breeze through this portion as we will be using standard UI elements. 79 | 80 | ![ActivityMain.xml](https://miro.medium.com/max/2000/1*up1tsY6Jru7CpN5jS4XNyA.png) 81 | 82 | In the example, I chose to use `ImageView` instead of `Button` for the various UI elements. Either works, the important part is to note that there are functions that we link to using the `onClick` property. 83 | 84 | ## Step 5: Checking Permissions ## 85 | I know what you must be thinking… “didn’t we already set up the Permissions?” Earlier we let the applications Manifest know which permissions our app plans to use, but we still have to explicitly request the user grant these permissions. Don’t worry this is the final step in getting the boilerplate project running and it’s painless. 86 | 87 | First, let's declare which permissions we want to request. 88 | ```Java 89 | // Permissions 90 | private static final int PERMISSION_REQ_ID = 22; 91 | private static final String[] REQUESTED_PERMISSIONS = {Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA}; 92 | ``` 93 | 94 | Next, we set up a couple of functions to help us. First we'll add a method that will request permissions for a given permission string and code. 95 | ```Java 96 | public boolean checkSelfPermission(String permission, int requestCode) { 97 | if (ContextCompat.checkSelfPermission(this, 98 | permission) 99 | != PackageManager.PERMISSION_GRANTED) { 100 | 101 | ActivityCompat.requestPermissions(this, 102 | REQUESTED_PERMISSIONS, 103 | requestCode); 104 | return false; 105 | } 106 | return true; 107 | } 108 | ``` 109 | 110 | Next, we have a callback method that will get called after the user has responded to the permissions request prompt. 111 | 112 | ```Java 113 | @Override 114 | public void onRequestPermissionsResult(int requestCode, 115 | @NonNull String permissions[], @NonNull int[] grantResults) { 116 | Log.i(LOG_TAG, "onRequestPermissionsResult " + grantResults[0] + " " + requestCode); 117 | 118 | switch (requestCode) { 119 | case PERMISSION_REQ_ID: { 120 | if (grantResults[0] != PackageManager.PERMISSION_GRANTED || grantResults[1] != PackageManager.PERMISSION_GRANTED) { 121 | Log.i(LOG_TAG, "Need permissions " + Manifest.permission.RECORD_AUDIO + "/" + Manifest.permission.CAMERA); 122 | break; 123 | } 124 | // if permission granted, initialize the engine 125 | initAgoraEngine(); 126 | break; 127 | } 128 | } 129 | } 130 | ``` 131 | 132 | Last, within our Class’s `onCreate` we check if our permissions have been granted and if not the above methods will handle the requests. 133 | 134 | ![onCreate](https://miro.medium.com/max/2000/1*tIG96dUF74UNFjiXn_JE7Q.png) 135 | 136 | ## Step 6: Initializing the Agora.io SDK ## 137 | Now that we have our view, we are ready to initialize the Agora.io SDK, set up the user profile and set the video quality settings. 138 | 139 | In the previous step, you may have noticed there are a couple places that I call `initAgoraEngine()`. Before we can dive into the initialization we need to make sure that our Activity has access to an instance of the Agora.io `RtcEngine`. 140 | 141 | Within our `MainActivity` Class, we need to declare a Class property to store our instance of RtcEngine. 142 | ```Java 143 | private RtcEngine mRtcEngine; 144 | ``` 145 | Now its time to initialize! After all the boilerplate setup we are finally at the step where we can start playing with the Agora.io engine! 146 | 147 | Go ahead and declare your `initAgoraEngine` method within your class. Within this function, we will create a new instance of the `RtcEngine` using the baseContext, the Agora `AppID` _(declared above)_, and an instance of the `RtcEngineEventHandler` _(we’ll get into this a little later)_. 148 | ```Java 149 | private void initAgoraEngine() { 150 | try { 151 | mRtcEngine = RtcEngine.create(getBaseContext(), getString(R.string.agora_app_id), mRtcEventHandler); 152 | } catch (Exception e) { 153 | Log.e(LOG_TAG, Log.getStackTraceString(e)); 154 | 155 | throw new RuntimeException("NEED TO check rtc sdk init fatal error\n" + Log.getStackTraceString(e)); 156 | } 157 | setupSession(); 158 | } 159 | ``` 160 | 161 | Once we have our new instance it’s time to set up our user’s session. Here we can set the Channel Profile to Communication, as this is a video chat and not a broadcast. This is also where we configure our video encoder settings. 162 | 163 | ```Java 164 | private void setupSession() { 165 | mRtcEngine.setChannelProfile(Constants.CHANNEL_PROFILE_COMMUNICATION); 166 | 167 | mRtcEngine.enableVideo(); 168 | 169 | mRtcEngine.setVideoEncoderConfiguration(new VideoEncoderConfiguration(VideoEncoderConfiguration.VD_1920x1080, VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_30, 170 | VideoEncoderConfiguration.STANDARD_BITRATE, 171 | VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_FIXED_PORTRAIT)); 172 | } 173 | ``` 174 | 175 | ## Step 7: Connecting the Video Streams ## 176 | Before we can join a view call we need to be able to present the local video stream to the user via the UI elements we setup earlier _(Step 4)_. 177 | 178 | In the first line, we get a reference for the UI element will act as our parent view for our video stream. The second step is to use the `RtcEngine` to create a [`SurfaceView`](https://developer.android.com/reference/android/view/SurfaceView) that will render the stream from the front camera, we also set the new `VideoSurface` to render on top of its parent view. The next step is to add the `VideoSurface` as a subview of the UI element. Lastly, we pass the `VideoSurface` to the engine as part of a `VideoCanvas` object. We leave the `uid` parameter blank so the SDK can handle creating a dynamic id for each user. 179 | 180 | ```Java 181 | private void setupLocalVideoFeed() { 182 | FrameLayout videoContainer = findViewById(R.id.floating_video_container); 183 | SurfaceView videoSurface = RtcEngine.CreateRendererView(getBaseContext()); 184 | videoSurface.setZOrderMediaOverlay(true); 185 | videoContainer.addView(videoSurface); 186 | mRtcEngine.setupLocalVideo(new VideoCanvas(videoSurface, VideoCanvas.RENDER_MODE_FIT, 0)); 187 | } 188 | ``` 189 | 190 | Now, that we have our local video feed setup we need to use a similar function to connect our remote video stream. 191 | ```Java 192 | private void setupRemoteVideoStream(int uid) { 193 | FrameLayout videoContainer = findViewById(R.id.bg_video_container); 194 | SurfaceView videoSurface = RtcEngine.CreateRendererView(getBaseContext()); 195 | videoContainer.addView(videoSurface); 196 | mRtcEngine.setupRemoteVideo(new VideoCanvas(videoSurface, VideoCanvas.RENDER_MODE_FIT, uid)); 197 | mRtcEngine.setRemoteSubscribeFallbackOption(io.agora.rtc.Constants.STREAM_FALLBACK_OPTION_AUDIO_ONLY); 198 | } 199 | ``` 200 | 201 | The main difference with the remote video from the local, is the user `id` parameter that gets passed to the engine as part of the `VideoCanvas` object that gets passed to the engine. The last line sets the fall back option in case the video degrades the engine will revert to audio only. 202 | 203 | ![setupLocalVideoFeed and setupRemoteVideoStream methods](https://miro.medium.com/max/2000/1*P1lAwOdHR4m_vCnKnmvHJQ.png) 204 | 205 | 206 | ## Step 8: Setup the SDK Event Handler ## 207 | Earlier, I made a reference to the `RtcEngineEventHandler`, and now it’s time to declare it as a property of our `MainActivity` Class. The engine will call these methods from the `RtcEngineEventHandler`. 208 | 209 | ```Java 210 | // Handle SDK Events 211 | private final IRtcEngineEventHandler mRtcEventHandler = new IRtcEngineEventHandler() { 212 | @Override 213 | public void onUserJoined(final int uid, int elapsed) { 214 | runOnUiThread(new Runnable() { 215 | @Override 216 | public void run() { 217 | // set first remote user to the main bg video container 218 | setupRemoteVideoStream(uid); 219 | } 220 | }); 221 | } 222 | 223 | // remote user has left channel 224 | @Override 225 | public void onUserOffline(int uid, int reason) { // Tutorial Step 7 226 | runOnUiThread(new Runnable() { 227 | @Override 228 | public void run() { 229 | onRemoteUserLeft(); 230 | } 231 | }); 232 | } 233 | 234 | // remote user has toggled their video 235 | @Override 236 | public void onRemoteVideoStateChanged(final int uid, final int state, int reason, int elapsed) { 237 | runOnUiThread(new Runnable() { 238 | @Override 239 | public void run() { 240 | onRemoteUserVideoToggle(uid, state); 241 | } 242 | }); 243 | } 244 | }; 245 | ``` 246 | 247 | Each event triggers some fairly straight forward functions, including one we wrote in the previous step. In the interest of keeping this brief, I will provide the code below but I won’t give an in-depth breakdown. 248 | 249 | ```Java 250 | private void onRemoteUserVideoToggle(int uid, int state) { 251 | FrameLayout videoContainer = findViewById(R.id.bg_video_container); 252 | 253 | SurfaceView videoSurface = (SurfaceView) videoContainer.getChildAt(0); 254 | videoSurface.setVisibility(state == 0 ? View.GONE : View.VISIBLE); 255 | 256 | // add an icon to let the other user know remote video has been disabled 257 | if(state == 0){ 258 | ImageView noCamera = new ImageView(this); 259 | noCamera.setImageResource(R.drawable.video_disabled); 260 | videoContainer.addView(noCamera); 261 | } else { 262 | ImageView noCamera = (ImageView) videoContainer.getChildAt(1); 263 | if(noCamera != null) { 264 | videoContainer.removeView(noCamera); 265 | } 266 | } 267 | } 268 | 269 | private void onRemoteUserLeft() { 270 | removeVideo(R.id.bg_video_container); 271 | } 272 | 273 | private void removeVideo(int containerID) { 274 | FrameLayout videoContainer = findViewById(containerID); 275 | videoContainer.removeAllViews(); 276 | } 277 | ``` 278 | 279 | ## Step 9: Joining and Leaving Channels ## 280 | I know what you’re thinking, STEP 9 ?!! Don’t sweat it the next two steps are really simple. Let’s start by joining a call… 281 | 282 | Below you can see from the first line, Agora SDK makes it simple, the engine calls joinChannel, passing in the channel name followed by the call to set up our local video stream. _(Step 7)_ 283 | 284 | ```Java 285 | public void onjoinChannelClicked(View view) { 286 | mRtcEngine.joinChannel(null, "test-channel", "Extra Optional Data", 0); 287 | setupLocalVideoFeed(); 288 | findViewById(R.id.joinBtn).setVisibility(View.GONE); // set the join button hidden 289 | findViewById(R.id.audioBtn).setVisibility(View.VISIBLE); // set the audio button hidden 290 | findViewById(R.id.leaveBtn).setVisibility(View.VISIBLE); // set the leave button hidden 291 | findViewById(R.id.videoBtn).setVisibility(View.VISIBLE); // set the video button hidden 292 | } 293 | ``` 294 | 295 | Leaving the channel is even simpler, the engine calls leaveChannel. Above you’ll notice there are a few lines to remove the video stream subviews from each UI element. 296 | ```Java 297 | public void onLeaveChannelClicked(View view) { 298 | leaveChannel(); 299 | removeVideo(R.id.floating_video_container); 300 | removeVideo(R.id.bg_video_container); 301 | findViewById(R.id.joinBtn).setVisibility(View.VISIBLE); // set the join button visible 302 | findViewById(R.id.audioBtn).setVisibility(View.GONE); // set the audio button hidden 303 | findViewById(R.id.leaveBtn).setVisibility(View.GONE); // set the leave button hidden 304 | findViewById(R.id.videoBtn).setVisibility(View.GONE); // set the video button hidden 305 | } 306 | 307 | private void leaveChannel() { 308 | mRtcEngine.leaveChannel(); 309 | } 310 | 311 | private void removeVideo(int containerID) { 312 | FrameLayout videoContainer = findViewById(containerID); 313 | videoContainer.removeAllViews(); 314 | } 315 | ``` 316 | 317 | ## Step 10: Adding UI Functionality ## 318 | The last remaining parts are related to connecting the UI elements for toggling the microphone and video stream on the local device. Let’s start with the audio toggle. 319 | 320 | First, we get the reference to our button, and then check if it has been toggled on/off using `isSelected()`. Once we have updated the UI element state, we pass the button’s updated state to the engine. 321 | ```Java 322 | public void onAudioMuteClicked(View view) { 323 | ImageView btn = (ImageView) view; 324 | if (btn.isSelected()) { 325 | btn.setSelected(false); 326 | btn.setImageResource(R.drawable.audio_toggle_btn); 327 | } else { 328 | btn.setSelected(true); 329 | btn.setImageResource(R.drawable.audio_toggle_active_btn); 330 | } 331 | 332 | mRtcEngine.muteLocalAudioStream(btn.isSelected()); 333 | } 334 | ``` 335 | 336 | Moving on to the video toggle, as with the audio toggle we check/update the button’s state using `isSelected()` and then pass that to the engine. To give a better visual representation of the video being muted, we hide/show the `VideoSurface`. 337 | 338 | ```Java 339 | public void onVideoMuteClicked(View view) { 340 | ImageView btn = (ImageView) view; 341 | if (btn.isSelected()) { 342 | btn.setSelected(false); 343 | btn.setImageResource(R.drawable.video_toggle_btn); 344 | } else { 345 | btn.setSelected(true); 346 | btn.setImageResource(R.drawable.video_toggle_active_btn); 347 | } 348 | 349 | mRtcEngine.muteLocalVideoStream(btn.isSelected()); 350 | 351 | FrameLayout container = findViewById(R.id.floating_video_container); 352 | container.setVisibility(btn.isSelected() ? View.GONE : View.VISIBLE); 353 | SurfaceView videoSurface = (SurfaceView) container.getChildAt(0); 354 | videoSurface.setZOrderMediaOverlay(!btn.isSelected()); 355 | videoSurface.setVisibility(btn.isSelected() ? View.GONE : View.VISIBLE); 356 | } 357 | ``` 358 | 359 | ![Done](https://miro.medium.com/max/996/0*4morLI6MnOOzQUyx) 360 | I hope you enjoyed reading along and working together on creating a 1-to-1 Video Chat Android app using the [Agora.io Video SDK](https://docs.agora.io/en/Video/product_video?platform=All%20Platforms). 361 | 362 | ======= 363 | This repo is part of a tutorial posted on Medium. For an in depth explanation of how to build your own version please read: 364 | https://medium.com/agora-io/1-to-1-video-chat-app-on-android-using-agora-io-3c2fe6c42fb4 365 | 366 | ***Requirements:*** 367 | - Agora.io Developer Account (https://dashboard.agora.io) 368 | - Agora Video SDK for Android - available in the Agora.io Developer Center (https://docs.agora.io/en/Agora%20Platform/downloads) 369 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | defaultConfig { 6 | applicationId "com.example.android.myagoraapplication" 7 | minSdkVersion 16 8 | targetSdkVersion 28 9 | versionCode 1 10 | versionName "1.0" 11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | 20 | sourceSets { 21 | main { 22 | 23 | } 24 | } 25 | } 26 | 27 | dependencies { 28 | implementation 'com.github.agorabuilder:native-full-sdk:3.4.1' 29 | implementation 'com.android.support:appcompat-v7:28.0.0' 30 | implementation 'com.android.support.constraint:constraint-layout:1.1.3' 31 | testImplementation 'junit:junit:4.12' 32 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 33 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 34 | } 35 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | 24 | -keep class io.agora.**{*;} -------------------------------------------------------------------------------- /app/src/androidTest/java/com/example/android/myagoraapplication/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.example.android.myagoraapplication; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.example.android.myagoraapplication", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/myagoraapplication/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.android.myagoraapplication; 2 | 3 | import android.Manifest; 4 | import android.content.pm.PackageManager; 5 | import android.graphics.PorterDuff; 6 | import android.support.annotation.NonNull; 7 | import android.support.v4.app.ActivityCompat; 8 | import android.support.v4.content.ContextCompat; 9 | import android.support.v7.app.AppCompatActivity; 10 | import android.os.Bundle; 11 | import android.util.Log; 12 | import android.view.SurfaceView; 13 | import android.view.View; 14 | import android.widget.FrameLayout; 15 | import android.widget.Button; 16 | import android.widget.ImageView; 17 | import android.widget.Toast; 18 | 19 | import io.agora.rtc.Constants; 20 | import io.agora.rtc.IRtcEngineEventHandler; 21 | import io.agora.rtc.RtcEngine; 22 | import io.agora.rtc.video.VideoEncoderConfiguration; 23 | import io.agora.rtc.video.VideoCanvas; 24 | 25 | public class MainActivity extends AppCompatActivity { 26 | 27 | private RtcEngine mRtcEngine; 28 | 29 | // Permissions 30 | private static final int PERMISSION_REQ_ID = 22; 31 | private static final String[] REQUESTED_PERMISSIONS = {Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA}; 32 | 33 | private static final String LOG_TAG = MainActivity.class.getSimpleName(); 34 | 35 | // Handle SDK Events 36 | private final IRtcEngineEventHandler mRtcEventHandler = new IRtcEngineEventHandler() { 37 | @Override 38 | public void onUserJoined(final int uid, int elapsed) { 39 | runOnUiThread(new Runnable() { 40 | @Override 41 | public void run() { 42 | // set first remote user to the main bg video container 43 | setupRemoteVideoStream(uid); 44 | } 45 | }); 46 | } 47 | 48 | // remote user has left channel 49 | @Override 50 | public void onUserOffline(int uid, int reason) { // Tutorial Step 7 51 | runOnUiThread(new Runnable() { 52 | @Override 53 | public void run() { 54 | onRemoteUserLeft(); 55 | } 56 | }); 57 | } 58 | 59 | // remote user has toggled their video 60 | @Override 61 | public void onRemoteVideoStateChanged(final int uid, final int state, int reason, int elapsed) { 62 | runOnUiThread(new Runnable() { 63 | @Override 64 | public void run() { 65 | onRemoteUserVideoToggle(uid, state); 66 | } 67 | }); 68 | } 69 | }; 70 | 71 | @Override 72 | protected void onCreate(Bundle savedInstanceState) { 73 | super.onCreate(savedInstanceState); 74 | setContentView(R.layout.activity_main); 75 | 76 | if (checkSelfPermission(REQUESTED_PERMISSIONS[0], PERMISSION_REQ_ID) && 77 | checkSelfPermission(REQUESTED_PERMISSIONS[1], PERMISSION_REQ_ID)) { 78 | initAgoraEngine(); 79 | } 80 | 81 | findViewById(R.id.audioBtn).setVisibility(View.GONE); // set the audio button hidden 82 | findViewById(R.id.leaveBtn).setVisibility(View.GONE); // set the leave button hidden 83 | findViewById(R.id.videoBtn).setVisibility(View.GONE); // set the video button hidden 84 | } 85 | 86 | private void initAgoraEngine() { 87 | try { 88 | mRtcEngine = RtcEngine.create(getBaseContext(), getString(R.string.agora_app_id), mRtcEventHandler); 89 | } catch (Exception e) { 90 | Log.e(LOG_TAG, Log.getStackTraceString(e)); 91 | 92 | throw new RuntimeException("NEED TO check rtc sdk init fatal error\n" + Log.getStackTraceString(e)); 93 | } 94 | setupSession(); 95 | } 96 | 97 | private void setupSession() { 98 | mRtcEngine.setChannelProfile(Constants.CHANNEL_PROFILE_COMMUNICATION); 99 | 100 | mRtcEngine.enableVideo(); 101 | 102 | mRtcEngine.setVideoEncoderConfiguration(new VideoEncoderConfiguration(VideoEncoderConfiguration.VD_640x480, VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_30, 103 | VideoEncoderConfiguration.STANDARD_BITRATE, 104 | VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_FIXED_PORTRAIT)); 105 | } 106 | 107 | private void setupLocalVideoFeed() { 108 | 109 | // setup the container for the local user 110 | FrameLayout videoContainer = findViewById(R.id.floating_video_container); 111 | SurfaceView videoSurface = RtcEngine.CreateRendererView(getBaseContext()); 112 | videoSurface.setZOrderMediaOverlay(true); 113 | videoContainer.addView(videoSurface); 114 | mRtcEngine.setupLocalVideo(new VideoCanvas(videoSurface, VideoCanvas.RENDER_MODE_FIT, 0)); 115 | } 116 | 117 | private void setupRemoteVideoStream(int uid) { 118 | // setup ui element for the remote stream 119 | FrameLayout videoContainer = findViewById(R.id.bg_video_container); 120 | // ignore any new streams that join the session 121 | if (videoContainer.getChildCount() >= 1) { 122 | return; 123 | } 124 | 125 | SurfaceView videoSurface = RtcEngine.CreateRendererView(getBaseContext()); 126 | videoContainer.addView(videoSurface); 127 | mRtcEngine.setupRemoteVideo(new VideoCanvas(videoSurface, VideoCanvas.RENDER_MODE_FIT, uid)); 128 | mRtcEngine.setRemoteSubscribeFallbackOption(io.agora.rtc.Constants.STREAM_FALLBACK_OPTION_AUDIO_ONLY); 129 | 130 | } 131 | 132 | public void onAudioMuteClicked(View view) { 133 | ImageView btn = (ImageView) view; 134 | if (btn.isSelected()) { 135 | btn.setSelected(false); 136 | btn.setImageResource(R.drawable.audio_toggle_btn); 137 | } else { 138 | btn.setSelected(true); 139 | btn.setImageResource(R.drawable.audio_toggle_active_btn); 140 | } 141 | 142 | mRtcEngine.muteLocalAudioStream(btn.isSelected()); 143 | } 144 | 145 | public void onVideoMuteClicked(View view) { 146 | ImageView btn = (ImageView) view; 147 | if (btn.isSelected()) { 148 | btn.setSelected(false); 149 | btn.setImageResource(R.drawable.video_toggle_btn); 150 | } else { 151 | btn.setSelected(true); 152 | btn.setImageResource(R.drawable.video_toggle_active_btn); 153 | } 154 | 155 | mRtcEngine.muteLocalVideoStream(btn.isSelected()); 156 | 157 | FrameLayout container = findViewById(R.id.floating_video_container); 158 | container.setVisibility(btn.isSelected() ? View.GONE : View.VISIBLE); 159 | SurfaceView videoSurface = (SurfaceView) container.getChildAt(0); 160 | videoSurface.setZOrderMediaOverlay(!btn.isSelected()); 161 | videoSurface.setVisibility(btn.isSelected() ? View.GONE : View.VISIBLE); 162 | } 163 | 164 | // join the channel when user clicks UI button 165 | public void onjoinChannelClicked(View view) { 166 | mRtcEngine.joinChannel(null, "test-channel", "Extra Optional Data", 0); // if you do not specify the uid, Agora will assign one. 167 | setupLocalVideoFeed(); 168 | findViewById(R.id.joinBtn).setVisibility(View.GONE); // set the join button hidden 169 | findViewById(R.id.audioBtn).setVisibility(View.VISIBLE); // set the audio button hidden 170 | findViewById(R.id.leaveBtn).setVisibility(View.VISIBLE); // set the leave button hidden 171 | findViewById(R.id.videoBtn).setVisibility(View.VISIBLE); // set the video button hidden 172 | } 173 | 174 | public void onLeaveChannelClicked(View view) { 175 | leaveChannel(); 176 | removeVideo(R.id.floating_video_container); 177 | removeVideo(R.id.bg_video_container); 178 | findViewById(R.id.joinBtn).setVisibility(View.VISIBLE); // set the join button visible 179 | findViewById(R.id.audioBtn).setVisibility(View.GONE); // set the audio button hidden 180 | findViewById(R.id.leaveBtn).setVisibility(View.GONE); // set the leave button hidden 181 | findViewById(R.id.videoBtn).setVisibility(View.GONE); // set the video button hidden 182 | } 183 | 184 | private void leaveChannel() { 185 | mRtcEngine.leaveChannel(); 186 | } 187 | 188 | private void removeVideo(int containerID) { 189 | FrameLayout videoContainer = findViewById(containerID); 190 | videoContainer.removeAllViews(); 191 | } 192 | 193 | private void onRemoteUserVideoToggle(int uid, int state) { 194 | FrameLayout videoContainer = findViewById(R.id.bg_video_container); 195 | 196 | SurfaceView videoSurface = (SurfaceView) videoContainer.getChildAt(0); 197 | videoSurface.setVisibility(state == 0 ? View.GONE : View.VISIBLE); 198 | 199 | // add an icon to let the other user know remote video has been disabled 200 | if(state == 0){ 201 | ImageView noCamera = new ImageView(this); 202 | noCamera.setImageResource(R.drawable.video_disabled); 203 | videoContainer.addView(noCamera); 204 | } else { 205 | ImageView noCamera = (ImageView) videoContainer.getChildAt(1); 206 | if(noCamera != null) { 207 | videoContainer.removeView(noCamera); 208 | } 209 | } 210 | } 211 | 212 | private void onRemoteUserLeft() { 213 | removeVideo(R.id.bg_video_container); 214 | } 215 | 216 | 217 | 218 | public boolean checkSelfPermission(String permission, int requestCode) { 219 | Log.i(LOG_TAG, "checkSelfPermission " + permission + " " + requestCode); 220 | if (ContextCompat.checkSelfPermission(this, 221 | permission) 222 | != PackageManager.PERMISSION_GRANTED) { 223 | 224 | ActivityCompat.requestPermissions(this, 225 | REQUESTED_PERMISSIONS, 226 | requestCode); 227 | return false; 228 | } 229 | return true; 230 | } 231 | 232 | 233 | @Override 234 | public void onRequestPermissionsResult(int requestCode, 235 | @NonNull String permissions[], @NonNull int[] grantResults) { 236 | Log.i(LOG_TAG, "onRequestPermissionsResult " + grantResults[0] + " " + requestCode); 237 | 238 | switch (requestCode) { 239 | case PERMISSION_REQ_ID: { 240 | if (grantResults[0] != PackageManager.PERMISSION_GRANTED || grantResults[1] != PackageManager.PERMISSION_GRANTED) { 241 | Log.i(LOG_TAG, "Need permissions " + Manifest.permission.RECORD_AUDIO + "/" + Manifest.permission.CAMERA); 242 | break; 243 | } 244 | 245 | initAgoraEngine(); 246 | break; 247 | } 248 | } 249 | } 250 | 251 | @Override 252 | protected void onDestroy() { 253 | super.onDestroy(); 254 | 255 | leaveChannel(); 256 | RtcEngine.destroy(); 257 | mRtcEngine = null; 258 | } 259 | 260 | public final void showLongToast(final String msg) { 261 | this.runOnUiThread(new Runnable() { 262 | @Override 263 | public void run() { 264 | Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_LONG).show(); 265 | } 266 | }); 267 | } 268 | 269 | 270 | } 271 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/audio_toggle_active_btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitallysavvy/android-video-chat-demo/090fa8a4b630e7be635556fc3af98b69a1388327/app/src/main/res/drawable/audio_toggle_active_btn.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/audio_toggle_btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitallysavvy/android-video-chat-demo/090fa8a4b630e7be635556fc3af98b69a1388327/app/src/main/res/drawable/audio_toggle_btn.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/end_call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitallysavvy/android-video-chat-demo/090fa8a4b630e7be635556fc3af98b69a1388327/app/src/main/res/drawable/end_call.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/join_call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitallysavvy/android-video-chat-demo/090fa8a4b630e7be635556fc3af98b69a1388327/app/src/main/res/drawable/join_call.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/video_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitallysavvy/android-video-chat-demo/090fa8a4b630e7be635556fc3af98b69a1388327/app/src/main/res/drawable/video_disabled.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/video_toggle_active_btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitallysavvy/android-video-chat-demo/090fa8a4b630e7be635556fc3af98b69a1388327/app/src/main/res/drawable/video_toggle_active_btn.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/video_toggle_btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitallysavvy/android-video-chat-demo/090fa8a4b630e7be635556fc3af98b69a1388327/app/src/main/res/drawable/video_toggle_btn.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 | 20 | 21 | 35 | 36 | 48 | 49 | 55 | 56 | 64 | 65 | 73 | 74 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitallysavvy/android-video-chat-demo/090fa8a4b630e7be635556fc3af98b69a1388327/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitallysavvy/android-video-chat-demo/090fa8a4b630e7be635556fc3af98b69a1388327/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitallysavvy/android-video-chat-demo/090fa8a4b630e7be635556fc3af98b69a1388327/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitallysavvy/android-video-chat-demo/090fa8a4b630e7be635556fc3af98b69a1388327/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitallysavvy/android-video-chat-demo/090fa8a4b630e7be635556fc3af98b69a1388327/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitallysavvy/android-video-chat-demo/090fa8a4b630e7be635556fc3af98b69a1388327/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitallysavvy/android-video-chat-demo/090fa8a4b630e7be635556fc3af98b69a1388327/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitallysavvy/android-video-chat-demo/090fa8a4b630e7be635556fc3af98b69a1388327/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitallysavvy/android-video-chat-demo/090fa8a4b630e7be635556fc3af98b69a1388327/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitallysavvy/android-video-chat-demo/090fa8a4b630e7be635556fc3af98b69a1388327/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #D81B60 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Agora - 1:1 Video Call 3 | AGORA_KEY 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/test/java/com/example/android/myagoraapplication/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.example.android.myagoraapplication; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.5.3' 11 | 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | jcenter() 22 | maven { url 'https://www.jitpack.io' } 23 | } 24 | } 25 | 26 | task clean(type: Delete) { 27 | delete rootProject.buildDir 28 | } 29 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | 15 | 16 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitallysavvy/android-video-chat-demo/090fa8a4b630e7be635556fc3af98b69a1388327/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Jun 03 12:07:43 EDT 2020 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-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /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 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------