├── .circleci └── config.yml ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── Docs ├── access-token.md ├── emulator-support.md ├── manage-push-credentials.md ├── migration-guide-2.x-3.x.md ├── migration-guide-3.x-4.x.md ├── migration-guide-4.x-5.x.md ├── new-features-3.0.md ├── new-features-4.0.md ├── playing-custom-ringtone.md ├── push-credentials-via-conversations-api.md ├── reducing-apk-size.md └── troubleshooting.md ├── LICENSE ├── README.md ├── Server ├── .env.example ├── .gitignore ├── .nvmrc ├── LICENSE ├── assets │ ├── index.html │ └── style.css ├── functions │ ├── access-token.js │ ├── hello-world.js │ ├── incoming.js │ ├── make-call.js │ └── place-call.js ├── package-lock.json └── package.json ├── app ├── .gitignore ├── build.gradle ├── google-services.json ├── gradle.properties ├── proguard-rules.pro └── src │ ├── connection_service │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── twilio │ │ │ └── voice │ │ │ └── quickstart │ │ │ ├── VoiceActivity.java │ │ │ └── VoiceConnectionService.java │ └── res │ │ └── values │ │ └── strings.xml │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-web.png │ ├── java │ │ └── com │ │ │ └── twilio │ │ │ └── voice │ │ │ └── quickstart │ │ │ ├── Constants.java │ │ │ ├── IncomingCallService.java │ │ │ ├── Logger.java │ │ │ ├── SoundPoolManager.java │ │ │ ├── VoiceApplication.java │ │ │ └── VoiceService.java │ └── res │ │ ├── drawable │ │ ├── ic_bluetooth_white_24dp.xml │ │ ├── ic_call_black_24dp.xml │ │ ├── ic_call_end_white_24dp.xml │ │ ├── ic_call_white_24dp.xml │ │ ├── ic_headset_mic_white_24dp.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_mic_white_24dp.xml │ │ ├── ic_mic_white_off_24dp.xml │ │ ├── ic_pause_white_24dp.xml │ │ ├── ic_phonelink_ring_white_24dp.xml │ │ └── ic_volume_up_white_24dp.xml │ │ ├── layout │ │ ├── activity_voice.xml │ │ └── dialog_call.xml │ │ ├── menu │ │ └── menu.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 │ │ ├── raw │ │ ├── disconnect.wav │ │ ├── incoming.wav │ │ └── outgoing.wav │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── standard │ ├── AndroidManifest.xml │ └── java │ └── com │ └── twilio │ └── voice │ └── quickstart │ └── VoiceActivity.java ├── build.gradle ├── exampleCustomAudioDevice ├── .gitignore ├── README.md ├── build.gradle ├── gradle.properties ├── libs │ └── voice-release.aar ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── twilio │ │ └── examplecustomaudiodevice │ │ ├── CustomDeviceActivity.java │ │ ├── FileAndMicAudioDevice.java │ │ └── SoundPoolManager.java │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── ic_call_black_24dp.xml │ ├── ic_call_end_white_24dp.xml │ ├── ic_call_white_24dp.xml │ ├── ic_launcher_background.xml │ ├── ic_launcher_foreground.xml │ ├── ic_mic_white_24dp.xml │ ├── ic_mic_white_off_24dp.xml │ ├── ic_music_white.xml │ ├── ic_pause_white_24dp.xml │ ├── ic_phonelink_ring_white_24dp.xml │ └── ic_volume_up_white_24dp.xml │ ├── layout │ ├── activity_custom_audio_device.xml │ └── dialog_call.xml │ ├── menu │ └── menu.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 │ ├── raw │ ├── disconnect.wav │ ├── incoming.wav │ ├── music.wav │ └── outgoing.wav │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── exampleCustomAudioDevice │ ├── audio-device-example.png │ ├── audio_device_microphone.png │ ├── audio_device_music_file_plays.png │ └── make_call_custom_audio_device.png └── quickstart │ ├── account-menu.png │ ├── credentials-sid.png │ ├── credentials-tab.png │ ├── firebase-fcm-token-creation.png │ ├── import_project.png │ ├── incoming_call.png │ ├── incoming_call_from_alice.png │ ├── invalid_google_service_json_error.png │ ├── make_call_to_client.png │ ├── make_call_to_number.png │ ├── twilio_cli_key_chain_access.png │ ├── voice_activity.png │ ├── voice_make_call.png │ └── voice_make_call_dialog.png └── settings.gradle /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.0 2 | 3 | aliases: 4 | # Workspace 5 | - &workspace 6 | ~/voice-quickstart-android 7 | 8 | - &gradle-cache-key 9 | key: jars-{{ checksum "build.gradle" }}-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }} 10 | - &restore-cache-gradle 11 | <<: *gradle-cache-key 12 | name: Restore Gradle Cache 13 | - &save-cache-gradle 14 | <<: *gradle-cache-key 15 | name: Save Gradle Cache 16 | paths: 17 | - ~/.gradle/caches 18 | - ~/.gradle/wrapper 19 | - &install-secrets 20 | name: Install secrets 21 | command: | 22 | echo $APP_GOOGLE_SERVICE_JSON | base64 -di > app/google-services.json 23 | 24 | # Containers 25 | - &build-defaults 26 | working_directory: *workspace 27 | docker: 28 | - image: cimg/android:2024.01.1-node 29 | environment: 30 | - _JAVA_OPTIONS: "-XX:+UnlockExperimentalVMOptions -Xmx3g" 31 | 32 | jobs: 33 | setup-workspace: 34 | <<: *build-defaults 35 | resource_class: medium+ 36 | steps: 37 | # Setup code and workspace for downstream jobs 38 | - checkout 39 | - restore-cache: *restore-cache-gradle 40 | 41 | # Save cache 42 | - save-cache: *save-cache-gradle 43 | 44 | build-quickstart: 45 | <<: *build-defaults 46 | resource_class: large 47 | steps: 48 | # Setup 49 | - checkout 50 | - attach_workspace: 51 | at: *workspace 52 | - restore-cache: *restore-cache-gradle 53 | - run: *install-secrets 54 | 55 | # Build app 56 | - run: 57 | name: Build app 58 | command: ./gradlew -q app:assemble 59 | 60 | # Save cache 61 | - save-cache: *save-cache-gradle 62 | 63 | build-examplecustomaudiodevice: 64 | <<: *build-defaults 65 | resource_class: large 66 | steps: 67 | # Setup 68 | - checkout 69 | - attach_workspace: 70 | at: *workspace 71 | - restore-cache: *restore-cache-gradle 72 | 73 | # Build app 74 | - run: 75 | name: Build examplecustomaudiodevice 76 | command: ./gradlew -q examplecustomaudiodevice:assemble 77 | 78 | # Save cache 79 | - save-cache: *save-cache-gradle 80 | 81 | workflows: 82 | version: 2 83 | 84 | build: 85 | jobs: 86 | # Setup 87 | - setup-workspace 88 | 89 | # Build 90 | - build-quickstart: 91 | requires: 92 | - setup-workspace 93 | - build-examplecustomaudiodevice: 94 | requires: 95 | - setup-workspace 96 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | > Before filing an issue please check that the issue is not already addressed by the following: 3 | > * [Voice SDK Guides](https://www.twilio.com/docs/api/voice-sdk) 4 | > * [GitHub Issues](https://github.com/twilio/voice-quickstart-android/issues) 5 | > * [Changelog](https://www.twilio.com/docs/api/voice-sdk/android/changelog) 6 | 7 | > Please ensure that you are not sharing any 8 | [Personally Identifiable Information(PII)](https://www.twilio.com/docs/glossary/what-is-personally-identifiable-information-pii) 9 | or sensitive account information (API keys, credentials, etc.) when reporting an issue. 10 | 11 | ### Description 12 | 13 | [Description of the issue] 14 | 15 | ### Steps to Reproduce 16 | 17 | 1. [Step one] 18 | 2. [Step two] 19 | 3. [Insert as many steps as needed] 20 | 21 | #### Code 22 | 23 | ```java 24 | // Code that helps reproduce the issue 25 | ``` 26 | 27 | #### Expected Behavior 28 | 29 | [What you expect to happen] 30 | 31 | #### Actual Behavior 32 | 33 | [What actually happens] 34 | 35 | #### Reproduces How Often 36 | 37 | [What percentage of the time does it reproduce?] 38 | 39 | #### Twilio Call SID(s) 40 | 41 | You can find the Call SID in the SDK using Call.getSid() or CallInvite.getCallSid(). The Call SID can also be found on the Twilio Calls Console: https://www.twilio.com/console/voice/calls/logs. 42 | 43 | #### Logs 44 | 45 | ``` 46 | // Log output when the issue occurs 47 | ``` 48 | 49 | ### Versions 50 | 51 | All relevant version information for the issue. 52 | 53 | #### Voice Android SDK 54 | 55 | [e.g. 2.0.0-beta14] 56 | 57 | #### OS Version 58 | 59 | [e.g. Android 7.1.2] 60 | 61 | #### Device Model 62 | 63 | [e.g. Nexus 6p] 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | 19 | # Local configuration file (sdk path, etc) 20 | local.properties 21 | 22 | # Proguard folder generated by Eclipse 23 | proguard/ 24 | 25 | # Log Files 26 | *.log 27 | 28 | # Android Studio Navigation editor temp files 29 | .navigation/ 30 | 31 | # Android Studio captures folder 32 | captures/ 33 | 34 | # Intellij 35 | *.iml 36 | .idea/ 37 | 38 | # GCM Configuration File 39 | google-services.json 40 | 41 | .DS_Store 42 | -------------------------------------------------------------------------------- /Docs/access-token.md: -------------------------------------------------------------------------------- 1 | ## Access Tokens 2 | 3 | The access token generated by your server component is a [jwt](https://jwt.io) that contains a `grant` for Programmable Voice, an `identity` that you specify, and a `time-to-live` that sets the lifetime of the generated access token. The default `time-to-live` is 1 hour and is configurable up to 24 hours using the Twilio helper libraries. 4 | 5 | ### Uses 6 | 7 | In the Android SDK the access token is used for the following: 8 | 9 | 1. To make an outgoing call via `Voice.call(Context context, String accessToken, String twiMLParams, Call.Listener listener)` 10 | 2. To register or unregister for incoming notifications via GCM or FCM via `Voice.register(String accessToken, Voice.RegistrationChannel registrationChannel, String registrationToken, RegistrationListener listener)` and `Voice.unregister(String accessToken, Voice.RegistrationChannel registrationChannel, String registrationToken, RegistrationListener listener)`. Once registered, incoming notifications are handled via a `CallInvite` where you can choose to accept or reject the invite. When accepting the call an access token is not required. Internally the `CallInvite` has its own accessToken that ensures it can connect to our infrastructure. 11 | 12 | ### Managing Expiry 13 | 14 | As mentioned above, an access token will eventually expire. If an access token has expired, our infrastructure will return error `EXCEPTION_INVALID_ACCESS_TOKEN_EXPIRY`/`20104` via a `CallException` or a `RegistrationException`. 15 | 16 | There are number of techniques you can use to ensure that access token expiry is managed accordingly: 17 | 18 | - Always fetch a new access token from your access token server before making an outbound call. 19 | - Retain the access token until getting a `EXCEPTION_INVALID_ACCESS_TOKEN_EXPIRY`/`20104` error before fetching a new access token. 20 | - Retain the access token along with the timestamp of when it was requested so you can verify ahead of time whether the token has already expired based on the `time-to-live` being used by your server. 21 | - Prefetch the access token whenever the `Application`, `Service`, `Activity`, or `Fragment` associated with an outgoing call is created. -------------------------------------------------------------------------------- /Docs/emulator-support.md: -------------------------------------------------------------------------------- 1 | ## Emulator Support 2 | 3 | The SDK supports using emulators except in the following known cases: 4 | 5 | 1. Emulators with API 22 or lower have bad audio emulation, the sound is generally inaudible 6 | 2. Emulators must have Google Play services support to use FCM to receive call invites 7 | 3. Running on x86 API 25 emulators results in application crashes 8 | 9 | In general we advise using a real device when doing development with our SDK since real-time audio is a performance oriented operation. -------------------------------------------------------------------------------- /Docs/manage-push-credentials.md: -------------------------------------------------------------------------------- 1 | ## Managing Push Credentials 2 | 3 | A Push Credential is a record for a push notification channel, for Android this Push Credential is a push notification channel record for FCM or GCM. Push Credentials are managed in the console under [Mobile Push Credentials](https://www.twilio.com/console/voice/sdks/credentials). 4 | 5 | Whenever a registration is performed via `Voice.register(…)` in the Android SDK the `identity` and the `Push Credential SID` provided in the JWT based access token, along with the `FCM/GCM token` are used as a unique address to send push notifications to this application instance whenever a call is made to reach that `identity`. Using `Voice.unregister(…)` removes the association for that `identity`. 6 | 7 | ### Updating a Push Credential 8 | 9 | If you need to change or update your server key token provided by Firebase (under `Project Settings` → `Cloud Messaging` → `Server key`) you can do so by selecting the Push Credential in the [console](https://www.twilio.com/console/voice/sdks/credentials) and adding your new `Server key` in the text box provided on the Push Credential page shown below: 10 | 11 | 12 | 13 | ### Deleting a Push Credential 14 | 15 | We **do not recommend that you delete a Push Credential** unless the application that it was created for is no longer being used. 16 | 17 | When a Push Credential is deleted **any associated registrations made with this Push Credential will be deleted**. Future attempts to reach an `identity` that was registered using the Push Credential SID of this deleted push credential will fail. 18 | 19 | If you are certain you want to delete a Push Credential you can click on `Delete this Credential` on the [console](https://www.twilio.com/console/voice/sdks/credentials) page of the selected Push Credential. 20 | 21 | Please ensure that after deleting the Push Credential you remove or replace the Push Credential SID when generating new access tokens. -------------------------------------------------------------------------------- /Docs/migration-guide-2.x-3.x.md: -------------------------------------------------------------------------------- 1 | ## 2.x to 3.x Migration Guide 2 | 3 | This section describes API or behavioral changes when upgrading from Voice Android 2.x to Voice Android 3.x. Each section provides code snippets to assist in transitioning to the new API. 4 | 5 | 1. [Making a Call](#migration1) 6 | 2. [Call State](#migration2) 7 | 3. [CallInvite Changes](#migration3) 8 | 4. [Specifying a Media Region](#migration4) 9 | 5. [ConnectOptions & AcceptOptions](#migration5) 10 | 6. [Media Establishment & Connectivity](#migration6) 11 | 7. [ProGuard Configuration](#migration7) 12 | 13 | #### Making a Call 14 | 15 | In Voice 3.x, the API to make a call has changed from `Voice.call(...)` to `Voice.connect(...)`. 16 | 17 | ```Java 18 | Call call = Voice.connect(context, accessToken, listener); 19 | ``` 20 | 21 | #### Call State 22 | 23 | The call state `CallState` has moved to the Call class. It can be referenced as follows: 24 | 25 | ```Java 26 | Call.State 27 | ``` 28 | 29 | #### CallInvite Changes 30 | 31 | In Voice Android 3.x, the `MessageListener` no longer raises errors if an invalid message is provided, instead a `boolean` value is returned when `boolean Voice.handleMessage(context, data, listener)` is called. The `boolean` value returns `true` when the provided data resulted in a `CallInvite` or `CancelledCallInvite` raised by the `MessageListener`. If `boolean Voice.handleMessage(context, data, listener)` returns `false` it means the data provided was not a Twilio Voice push message. 32 | 33 | The `MessageListener` now raises callbacks for a `CallInvite` or `CancelledCallInvite` as follows: 34 | 35 | ```Java 36 | boolean valid = handleMessage(context, data, new MessageListener() { 37 | @Override 38 | void onCallInvite(CallInvite callInvite) { 39 | // Show notification to answer or reject call 40 | } 41 | 42 | @Override 43 | void onCancelledCallInvite(CancelledCallInvite callInvite) { 44 | // Hide notification 45 | } 46 | }); 47 | ``` 48 | 49 | The `CallInvite` has an `accept()` and `reject()` method. The `getState()` method has been removed from the `CallInvite` in favor of distinguishing between call invites and call invite cancellations with discrete stateless objects. While the `CancelledCallInvite` simply provides the `to`, `from`, and `callSid` fields also available in the `CallInvite`. The class method `getCallSid()` can be used to associate a `CallInvite` with a `CancelledCallInvite`. 50 | 51 | In Voice Android 2.x passing a `cancel` message into `void Voice.handleMessage(...)` would not raise a callback in the following two cases: 52 | 53 | - This callee accepted the call 54 | - This callee rejected the call 55 | 56 | However, in Voice Android 3.x passing a `cancel` data message into `handleMessage(...)` will always result in a callback. A callback is raised whenever a valid message is provided to `Voice.handleMessage(...)`. 57 | 58 | Note that Twilio will send a `cancel` message to every registered device of the identity that accepts or rejects a call, even the device that accepted or rejected the call. 59 | 60 | #### Specifying a media region 61 | 62 | Previously, a media region could be specified via `Voice.setRegion(String)`. Now this configuration can be provided as part of `ConnectOptions` or `AcceptOptions` as shown below: 63 | 64 | ```Java 65 | ConnectOptions connectOptions = new ConnectOptions.Builder() 66 | .region(String) 67 | .build(); 68 | 69 | AcceptOptions acceptOptions = new AcceptOptions.Builder() 70 | .region(String) 71 | .build(); 72 | ``` 73 | 74 | #### ConnectOptions & AcceptOptions 75 | 76 | To support configurability upon making or accepting a call new classes have been added. To create `ConnectOptions` the `ConnectOptions.Builder` must be used. Once `ConnectOptions` is created it can be provided when connecting a `Call` as shown below: 77 | 78 | ```Java 79 | ConnectOptions connectOptions = new ConnectOptions.Builder(accessToken) 80 | .setParams(params) 81 | .build(); 82 | Call call = Voice.connect(context, connectOptions, listener); 83 | ``` 84 | 85 | A `CallInvite` can also be accepted using `AcceptOptions` as shown below: 86 | 87 | ```Java 88 | AcceptOptions acceptOptions = new AcceptOptions.Builder() 89 | .build(); 90 | CallInvite.accept(context, acceptOptions, listener); 91 | ``` 92 | 93 | #### Media Establishment & Connectivity 94 | 95 | The Voice Android 3.x SDK uses WebRTC. The exchange of real-time media requires the use of Interactive Connectivity Establishment(ICE) to establish a media connection between the client and the media server. In some network environments where network access is restricted it may be necessary to provide ICE servers to establish a media connection. We reccomend using the [Network Traversal Service (NTS)](https://www.twilio.com/stun-turn) to obtain ICE servers. ICE servers can be provided when making or accepting a call by passing them into `ConnectOptions` or `AcceptOptions` in the following way: 96 | 97 | ```Java 98 | // Obtain the set of ICE servers from your preferred ICE server provider 99 | Set iceServers = new HashSet<>(); 100 | iceServers.add(new IceServer("stun:global.stun.twilio.com:3478?transport=udp")); 101 | iceServers.add(new IceServer("turn:global.turn.twilio.com:3478?transport=udp")); 102 | ... 103 | 104 | IceOptions iceOptions = new IceOptions.Builder() 105 | .iceServers(iceServers) 106 | .build(); 107 | 108 | ConnectOptions.Builder connectOptionsBuilder = new ConnectOptions.Builder(accessToken) 109 | .iceOptions(iceOptions) 110 | .build(); 111 | ``` 112 | 113 | #### ProGuard Configuration 114 | 115 | To enable ProGuard, follow the [official instructions](https://developer.android.com/studio/build/shrink-code#enabling-gradle) first. 116 | 117 | * Open your app module's ProGuard configuration (`proguard-rules.pro` in your app's module in Android Studio) 118 | * Add the following lines at the end of your existing configuration 119 | * Use `tvo.webrtc...` instead of `org.webrtc...` when using Voice Android 3.2+ (see: https://www.twilio.com/docs/voice/voip-sdk/android#voice-android-320) 120 | 121 | ``` 122 | -keep class com.twilio.** { *; } 123 | -keep class org.webrtc.** { *; } 124 | -dontwarn org.webrtc.** 125 | -keep class com.twilio.voice.** { *; } 126 | -keepattributes InnerClasses 127 | 128 | ``` 129 | 130 | -------------------------------------------------------------------------------- /Docs/migration-guide-3.x-4.x.md: -------------------------------------------------------------------------------- 1 | ## 3.x to 4.x Migration Guide 2 | 3 | 4.0 SDK introduced a new call state `RECONNECTING`. You will need to update any logic you have implemented that relies on the call state. The simplest approach is to treat a `RECONNECTING` just like a `CONNECTED` and keep the current behavior. 4 | 5 | 4.0 has a new state `RECONNECTING` in `Call.State` and two new callbacks `onReconnecting(...)`, `onReconnected(...)` in `Call.Listener()`. Any prior implementation of `Call.Listener()` will need to be updated with the new callbacks. 6 | 7 | ``` 8 | private Call.Listener callListener() { 9 | return new Call.Listener() { 10 | 11 | @Override 12 | public void onRinging(@NonNull Call call) { 13 | Log.d(TAG, "Ringing"); 14 | } 15 | 16 | @Override 17 | public void onConnectFailure(@NonNull Call call, @NonNull CallException error) { 18 | Log.d(TAG, "Connect failure"); 19 | } 20 | 21 | @Override 22 | public void onConnected(@NonNull Call call) { 23 | Log.d(TAG, "Connected"); 24 | } 25 | 26 | /** 27 | * `onReconnecting()` callback is raised when a network change is detected and Call is already in `CONNECTED` ` 28 | * Call.State`. If the call is in `CONNECTING` or `RINGING` when network change happened the SDK will continue 29 | * attempting to connect, but a reconnect event will not be raised. 30 | */ 31 | @Override 32 | public void onReconnecting(@NonNull Call call, @NonNull CallException callException) { 33 | Log.d(TAG, "Reconnecting"); 34 | } 35 | 36 | /** 37 | * The call is successfully reconnected after reconnecting attempt. 38 | * / 39 | @Override 40 | public void onReconnected(@NonNull Call call) { 41 | Log.d(TAG, "Reconnected"); 42 | } 43 | 44 | @Override 45 | public void onDisconnected(@NonNull Call call, @Nullable CallException error) { 46 | Log.d(TAG, "Disconnected"); 47 | } 48 | }; 49 | } 50 | ``` -------------------------------------------------------------------------------- /Docs/migration-guide-4.x-5.x.md: -------------------------------------------------------------------------------- 1 | ## 4.x to 5.x Migration Guide 2 | 3 | This document provides migration steps to 5.x release. `5.0.0` introduces an update to the Programmable Voice call model. Prior to `5.0.0`, when `Voice.register(...)` was invoked, the Voice SDK registered for two push notifications: a call and cancel push notification. The SDK now handles incoming call cancellations via a dedicated signaling mechanism. The `cancel` push notification is no longer required or supported by new releases of the SDK. 4 | 5 | If your application supports incoming calls, you MUST perform the following steps to comply with the new call model in 5.x: 6 | 7 | 1. Upgrade Twilio Voice Android SDK to 5.0.0 8 | 2. You must register via `Voice.register` when your App starts. This ensures that your App no longer receives “cancel” push notifications. A valid call push notification, when passed to `Voice.handleMessage(...)`, will still result in a `CallInvite` being raise to the provided `MessageListener`. A `CancelledCallInvite` will be raised to the provided `MessageListener` if any of the following events occur: 9 | - The call is prematurely disconnected by the caller. 10 | - The callee does not accept or reject the call within 30 seconds. 11 | - The Voice SDK is unable to establish a connection to Twilio. 12 | 13 | 14 | A `CancelledCallInvite` will not be raised if a `CallInvite` is accepted or rejected. 15 | 16 | 17 | To register with the new SDK when the app is launched: 18 | 19 | ``` 20 | private RegistrationListener registrationListener() { 21 | return new RegistrationListener() { 22 | @Override 23 | public void onRegistered(String accessToken, String fcmToken) { 24 | Log.d(TAG, "Successfully registered FCM " + fcmToken); 25 | } 26 | 27 | @Override 28 | public void onError(RegistrationException error, String accessToken, String fcmToken) { 29 | String message = String.format("Registration Error: %d, %s", error.getErrorCode(), error.getMessage()); 30 | Log.e(TAG, message); 31 | } 32 | }; 33 | } 34 | 35 | private void registerForCallInvites() { 36 | final String fcmToken = FirebaseInstanceId.getInstance().getToken(); 37 | if (fcmToken != null) { 38 | Log.i(TAG, "Registering with FCM"); 39 | Voice.register(accessToken, Voice.RegistrationChannel.FCM, fcmToken, registrationListener); 40 | } 41 | } 42 | 43 | ``` 44 | 45 | Please note that if the app is updated to use 5.0.0 release but never launched to perform the registration, the mobile client will still receive "cancel" notifications. If “cancel” notification is passed to `Voice.handleMessage(…)`, it will return `false`. 46 | 3. Both `Voice.handleMessage(...)` methods require an Android `Context` as the first argument. You must update the method call to match the new method signature. 47 | 4. `MessageListener.onCancelledCallInvite` has been updated to include `@Nullable` `CallException callException` as the second argument. A `CallException` will be provided if a network or server error resulted in the cancellation. You need to update `MessageListener` implementation in your code to the following: 48 | 49 | ``` 50 | boolean valid = Voice.handleMessage(context, remoteMessage.getData(), new MessageListener() { 51 | @Override 52 | public void onCallInvite(@NonNull CallInvite callInvite) { 53 | // Handle CallInvite 54 | } 55 | 56 | @Override 57 | public void onCancelledCallInvite(@NonNull CancelledCallInvite cancelledCallInvite, @Nullable CallException callException) { 58 | // Handle CancelledCallInvite 59 | } 60 | }); 61 | ``` 62 | 63 | 5. If you were previously toggling `enableInsights` or specifying a `region` via `ConnectOptions`, you must now set the `insights` and `region` property via `Voice.enableInsights(…)` and `Voice.setRegion(…)` respectively. You must do so before calling `Voice.connect(…)` or `Voice.handleMessage(…)`. 64 | Please note : 65 | - Sending stats data to Insights is enabled by default 66 | - The default region uses Global Low Latency routing, which establishes a connection with the closest region to the user 67 | 68 | ``` 69 | Voice.enableInsights(true); 70 | Voice.setRegion(region); 71 | ConnectOptions connectOptions = new ConnectOptions.Builder(accessToken) 72 | .build(); 73 | call = Voice.connect(getApplicationContext(), connectOptions, outgoingCallListener()); 74 | ``` 75 | 76 | You can reference the 5.0.0 quickstart when migrating your application. 77 | A summary of the API changes and new Insights events can be found in the 5.0.0 changelog. 78 | -------------------------------------------------------------------------------- /Docs/new-features-3.0.md: -------------------------------------------------------------------------------- 1 | ## 3.0 New Features 2 | 3 | Voice Android 3.0 has a number of new features listed below: 4 | 5 | 1. [WebRTC](#feature1) 6 | 2. [Custom Parameters](#feature2) 7 | 3. [Hold](#feature3) 8 | 4. [Ringing](#feature4) 9 | 5. [Stats](#feature5) 10 | 6. [Preferred Audio Codec](#feature6) 11 | 12 | #### WebRTC 13 | 14 | The SDK is built using Chromium WebRTC for Android. This ensures that over time developers will get the best real-time media streaming capabilities available for Android. Additionally, upgrades to new versions of Chromium WebRTC will happen without changing the public API whenever possible. 15 | 16 | #### Custom Parameters 17 | 18 | You can now send parameters from the caller to the callee when you make a call. The key/value data is sent from the Voice SDK to your TwiML Server Application, and passed into TwiML to reach the callee. 19 | 20 | ##### Sending parameters to your TwiML Server Application for outgoing calls 21 | 22 | Parameters can be sent to your TwiML Server Application by specifying them in the `ConnectOptions` builder as follows: 23 | 24 | ```Java 25 | final Map params = new HashMap<>(); 26 | params.put("caller_first_name", "alice"); 27 | params.put("caller_last_name", "smith"); 28 | 29 | ConnectOptions connectOptions = new ConnectOptions.Builder(accessToken) 30 | .params(params) 31 | .build(); 32 | 33 | call = Voice.connect(context, connectOptions, listener); 34 | ``` 35 | 36 | These will arrive as either POST parameters or URL query parameters, depending on which HTTP method you configured for your TwiML Server Application in the [console](https://www.twilio.com/console/voice/twiml/apps). 37 | 38 | Once available on your TwiML Server Application you can use them to populate your TwiML response as described in the next section. 39 | 40 | ##### Getting parameters from your TwiML Server Application for incoming calls 41 | 42 | Parameters can be sent to a callee by initiating a TwiML [\](https://www.twilio.com/docs/voice/twiml/dial). Use the `` attribute to specify your key/value parameters as shown below: 43 | 44 | ```Java 45 | // Pass custom parameters in TwiML 46 | 47 | 48 | 49 | 50 | bob 51 | 52 | 53 | 54 | 55 | 56 | ``` 57 | 58 | When the call invite push message arrives to the callee it will have the specified parameters. The key/value parameters can then be retrieved as a Map from the `CallInvite.getCustomParameters()` method. 59 | 60 | #### Hold 61 | 62 | Previously, there was no way to hold a call. Hold can now be called on the `Call` object as follows: 63 | 64 | ```Java 65 | call.hold(boolean); 66 | ``` 67 | 68 | #### Ringing 69 | 70 | Ringing is now provided as a call state. A callback corresponding to this state transition is emitted once before the `Call.Listener.onConnected(...)` callback when the callee is being alerted of a Call. The behavior of this callback is determined by the `answerOnBridge` flag provided in the `Dial` verb of your TwiML application associated with this client. If the `answerOnBridge` flag is `false`, which is the default, the `Call.Listener.onConnected(...)` callback will be emitted immediately after `Call.Listener.onRinging(...)`. If the `answerOnBridge` flag is `true`, this will cause the call to emit the onConnected callback only after the call is answered. See [answerOnBridge](https://www.twilio.com/docs/voice/twiml/dial#answeronbridge) for more details on how to use it with the Dial TwiML verb. If the TwiML response contains a Say verb, then the call will emit the `Call.Listener.onConnected(...)` callback immediately after `Call.Listener.onRinging(...)` is raised, irrespective of the value of `answerOnBridge` being set to `true` or `false`. 71 | 72 | These changes are added as follows: 73 | 74 | ```Java 75 | public class Call { 76 | 77 | public enum State { 78 | CONNECTING, 79 | RINGING, // State addition 80 | CONNECTED, 81 | DISCONNECTED 82 | } 83 | 84 | public interface Listener { 85 | void onConnectFailure(@NonNull Call call, @NonNull CallException callException); 86 | void onRinging(@NonNull Call call); // Callback addition 87 | void onConnected(@NonNull Call call); 88 | void onDisconnected(@NonNull Call call, @Nullable CallException callException); 89 | } 90 | } 91 | ``` 92 | 93 | #### Stats 94 | 95 | Statistics related to the media in the call can now be retrieved by calling `Call.getStats(StatsListener listener)`. The `StatsListener` returns a `StatsReport` that provides statistics about the local and remote audio in the call. 96 | 97 | #### Preferred Audio Codec 98 | 99 | You can provide your preferred audio codecs in the `ConnectOptions` and the `AcceptOptions`. Opus is the default codec used by the mobile infrastructure. To use PCMU as the negotiated audio codec instead of Opus you can add it as the first codec in the preferAudioCodecs list. 100 | 101 | ```Java 102 | ConnectOptions connectOptions = new ConnectOptions.Builder(accessToken) 103 | .params(params) 104 | .preferAudioCodecs(Arrays.asList(new PcmuCodec(), new OpusCodec())) 105 | .build(); 106 | Call call = Voice.connect(VoiceActivity.this, connectOptions, callListener); 107 | ``` 108 | -------------------------------------------------------------------------------- /Docs/new-features-4.0.md: -------------------------------------------------------------------------------- 1 | ## 4.0 New Features 2 | 3 | Voice Android 4.0 has the following new features listed below: 4 | 5 | 1. [Reconnecting State and Callbacks](#feature1) 6 | 7 | 8 | #### Reconnecting State and Callbacks 9 | 10 | `RECONNECTING` is now provided as a call state. A callback `onReconnecting(...)` corresponding to this state transition is emitted after the `Call.Listener.onConnected(...)` callback when a network change is detected and Call is already in `CONNECTED` state. If the call is in `CONNECTING`or in `RINGING` state when network change happened, the SDK will continue attempting to connect and will not raise a callback. If a `Call` is reconnected after reconnectiong attempts, `onReconnected(...)` callback is raised and call state transitions to `CONNECTED`. 11 | 12 | These changes are added as follows: 13 | 14 | ```Java 15 | public class Call { 16 | 17 | public enum State { 18 | CONNECTING, 19 | RINGING, 20 | CONNECTED, 21 | RECONNECTING, // State addition 22 | DISCONNECTED 23 | } 24 | 25 | public interface Listener { 26 | void onConnectFailure(@NonNull Call call, @NonNull CallException callException); 27 | void onRinging(@NonNull Call call); 28 | void onConnected(@NonNull Call call); 29 | void onReconnecting(@NonNull Call call, @NonNull CallException callException); // Callback addition 30 | void onReconnected(@NonNull Call call); // Callback addition 31 | void onDisconnected(@NonNull Call call, @Nullable CallException callException); 32 | } 33 | } 34 | ``` -------------------------------------------------------------------------------- /Docs/playing-custom-ringtone.md: -------------------------------------------------------------------------------- 1 | ## Playing Custom Ringtone 2 | 3 | When [answerOnBridge](https://www.twilio.com/docs/voice/twiml/dial#answeronbridge) is enabled in the `` TwiML verb, the caller will not hear the ringback while the call is ringing and awaiting to be accepted on the callee's side. The application can use the `SoundPoolManager` to play custom audio files between the `Call.Listener.onRinging()` and the `Call.Listener.onConnected()` callbacks. To enable this behavior, add `playCustomRingback` as an environment variable or a property in `local.properties` file and set it to `true`. 4 | 5 | ``` 6 | playCustomRingback=true 7 | ``` -------------------------------------------------------------------------------- /Docs/push-credentials-via-conversations-api.md: -------------------------------------------------------------------------------- 1 | ## Create Push Credentials via the Conversations Credential Resource API 2 | 3 | Voice SDK users can manage their Push Credentials in the developer console (**Console > Account > Keys & Credentials > Credentials**). Currently the Push Credential management page only supports the default region (US1). To create or update Push Credentials for other regional (i.e. Australia) usage, developers can use the Conversations public API to manage their Push Credentials. Follow the instructions of the [Credential Resource API](https://www.twilio.com/docs/conversations/api/credential-resource) and replace the endpoint with the regional endpoint, for example https://conversations.dublin.ie1.twilio.com for the Australia region. 4 | 5 | You will also need: 6 | - FCM key: follow the [instructions]((https://github.com/twilio/voice-quickstart-android#1-generate-google-servicesjson)) to get the FCM key. 7 | - Twilio account credentials: find your API auth token for the specific region in the developer console. Go to **Console > Account > Keys & Credentials > API keys & tokens** and select the region in the dropdown menu. 8 | 9 | Example of creating an `IE1` Push Credential for FCM: 10 | 11 | ``` 12 | curl -X POST https://conversations.dublin.ie1.twilio.com/v1/Credentials \ 13 | --data-urlencode "Type=fcm" \ 14 | --data-urlencode "Secret=$FCM_SERVER_KEY" \ 15 | -u $TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN 16 | ``` 17 | 18 | To update a Push Credential (CR****) in `IE1`: 19 | 20 | ``` 21 | curl -X POST https://conversations.dublin.ie1.twilio.com/v1/Credentials/CR**** \ 22 | --data-urlencode "Type=fcm" \ 23 | --data-urlencode "Secret=$FCM_SERVER_KEY" \ 24 | -u $TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN 25 | ``` 26 | 27 | # Note that currently the Conversations Credential Resource API is not available for the Australia region. Please reach out to the [Twilio Help Center](https://help.twilio.com/) if you need help managing your Push Credentials for the Australia region. 28 | -------------------------------------------------------------------------------- /Docs/reducing-apk-size.md: -------------------------------------------------------------------------------- 1 | ## Reducing APK Size 2 | 3 | Our library is built using native libraries. As a result, if you use the default gradle build you will generate an APK with all four architectures(armeabi-v7a, arm64-v8a, x86, x86_64) in your APK. 4 | 5 | [APK splits](https://developer.android.com/studio/build/configure-apk-splits.html) allow developers to build multiple APKs for different screen sizes and ABIs. Enabling APK splits ensures that the minimum amount of files required to support a particular device are packaged into an APK. 6 | 7 | The following snippet shows an example `build.gradle` with APK splits enabled. 8 | 9 | apply plugin: 'com.android.application' 10 | 11 | android { 12 | // Specify that we want to split up the APK based on ABI 13 | splits { 14 | abi { 15 | // Enable ABI split 16 | enable true 17 | 18 | // Clear list of ABIs 19 | reset() 20 | 21 | // Specify each architecture currently supported by the Voice SDK 22 | include "armeabi-v7a", "arm64-v8a", "x86", "x86_64" 23 | 24 | // Specify that we do not want an additional universal SDK 25 | universalApk false 26 | } 27 | } 28 | } 29 | 30 | The adoption of APK splits requires developers to submit multiple APKs to the Play Store. Refer to [Google’s documentation](https://developer.android.com/google/play/publishing/multiple-apks.html) for how to support this in your application. -------------------------------------------------------------------------------- /Docs/troubleshooting.md: -------------------------------------------------------------------------------- 1 | ## Troubleshooting 2 | 3 | ### Enabling Debug Logging 4 | 5 | To enable debug level logging, add the following code in your application: 6 | 7 | ``` 8 | /* 9 | * Set the log level of the Voice Android SDK 10 | */ 11 | Voice.setLogLevel(LogLevel.DEBUG); 12 | 13 | /* 14 | * If your application is experiencing an issue related to a specific 15 | * module, you can set the log level of each of the following modules. 16 | */ 17 | Voice.setModuleLogLevel(LogModule.CORE, LogLevel.DEBUG); 18 | Voice.setModuleLogLevel(LogModule.PLATFORM, LogLevel.DEBUG); 19 | Voice.setModuleLogLevel(LogModule.SIGNALING, LogLevel.DEBUG); 20 | Voice.setModuleLogLevel(LogModule.WEBRTC, LogLevel.DEBUG); 21 | 22 | ``` 23 | 24 | ### Troubleshooting Audio 25 | The following sections provide guidance on how to ensure optimal audio quality in your applications. 26 | 27 | ### Managing Audio Devices with AudioSwitch 28 | The quickstart uses [AudioSwitch](https://github.com/twilio/audioswitch) to control [audio focus](https://developer.android.com/guide/topics/media-apps/audio-focus) and manage audio devices within the application. If you have an issue or question related to audio management, please open an issue in the [AudioSwitch](https://github.com/twilio/audioswitch) project. 29 | 30 | ### Configuring Hardware Audio Effects 31 | 32 | #### Voice Android SDK Version 5.2.x+ 33 | Our library performs acoustic echo cancellation (AEC) and noise suppression (NS) using device hardware by default. Using device hardware is more efficient, but some devices do not implement these audio effects well. If you are experiencing echo or background noise on certain devices reference the following snippet for enabling software implementations of AEC and NS. 34 | 35 | /* 36 | * Execute any time before invoking `Voice.connect(...)` or `CallInvite.accept(...)`. 37 | */ 38 | 39 | // Use software AEC 40 | DefaultAudioDevice defaultAudioDevice = new DefaultAudioDevice(); 41 | defaultAudioDevice.setUseHardwareAcousticEchoCanceler(false); 42 | Voice.setAudioDevice(defaultAudioDevice); 43 | 44 | // Use sofware NS 45 | DefaultAudioDevice defaultAudioDevice = new DefaultAudioDevice(); 46 | defaultAudioDevice.setUseHardwareNoiseSuppressor(false); 47 | Voice.setAudioDevice(defaultAudioDevice); 48 | 49 | 50 | #### Voice Android SDK Version below 5.1.x 51 | Our library performs acoustic echo cancellation (AEC), noise suppression (NS), and auto gain 52 | control (AGC) using device hardware by default. Using device hardware is more efficient, but some 53 | devices do not implement these audio effects well. If you are experiencing echo, background noise, 54 | or unexpected volume levels on certain devices reference the following snippet for enabling 55 | software implementations of AEC, NS, and AGC. 56 | 57 | /* 58 | * Execute any time before invoking `Voice.connect(...)` or `CallInvite.accept(...)`. 59 | */ 60 | // Use software AEC 61 | tvo.webrtc.voiceengine.WebRtcAudioUtils.setWebRtcBasedAcousticEchoCanceler(true); 62 | 63 | // Use sofware NS 64 | tvo.webrtc.voiceengine.WebRtcAudioUtils.setWebRtcBasedNoiseSuppressor(true); 65 | 66 | // Use software AGC 67 | tvo.webrtc.voiceengine.WebRtcAudioUtils.setWebRtcBasedAutomaticGainControl(true); 68 | 69 | ### Configuring OpenSL ES 70 | Starting with Voice SDK 4.3.0, our library does not use [OpenSL ES](https://developer.android.com/ndk/guides/audio/opensl/index.html) 71 | for audio playback by default. Prior versions starting with Voice SDK 3.0.0 did use OpenSL ES by default. Using OpenSL ES is more efficient, but can cause 72 | problems with other audio effects. For example, we found on the Nexus 6P that OpenSL ES affected 73 | the device's hardware echo canceller so we blacklisted the Nexus 6P from using OpenSL ES. If you 74 | are experiencing audio problems with a device that cannot be resolved using software audio effects, 75 | reference the following snippet for enabling OpenSL ES: 76 | 77 | /* 78 | * Execute any time before invoking `Voice.connect(...)` or `CallInvite.accept(...)`. 79 | */ 80 | 81 | // Enable OpenSL ES 82 | tvo.webrtc.voiceengine.WebRtcAudioManager.setBlacklistDeviceForOpenSLESUsage(false); 83 | 84 | // Check if OpenSL ES is disabled 85 | tvo.webrtc.voiceengine.WebRtcAudioUtils.deviceIsBlacklistedForOpenSLESUsage(); 86 | 87 | ### Managing Device Specific Configurations 88 | The Voice Android SDK does not maintain a list of devices for which hardware effects or OpenSL ES are disabled. We recommend maintaining a list in your own application and disabling these effects as needed. The [Signal App provides a great example](https://github.com/signalapp/Signal-Android/blob/master/src/org/thoughtcrime/securesms/ApplicationContext.java#L250) of how to maintain a list and disable the effects as needed. 89 | 90 | ### Handling Low Headset Volume 91 | If your application experiences low playback volume, we recommend the following snippets: 92 | 93 | #### Android N and Below 94 | ``` 95 | int focusRequestResult = audioManager.requestAudioFocus(new AudioManager.OnAudioFocusChangeListener() { 96 | 97 | @Override 98 | public void onAudioFocusChange(int focusChange) { 99 | } 100 | }, AudioManager.STREAM_VOICE_CALL, 101 | AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); 102 | ``` 103 | 104 | 105 | #### Android O and Up : 106 | ``` 107 | AudioAttributes playbackAttributes = new AudioAttributes.Builder() 108 | .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) 109 | .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) 110 | .build(); 111 | AudioFocusRequest focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT) 112 | .setAudioAttributes(playbackAttributes) 113 | .setAcceptsDelayedFocusGain(true) 114 | .setOnAudioFocusChangeListener(new AudioManager.OnAudioFocusChangeListener() { 115 | @Override 116 | public void onAudioFocusChange(int i) { 117 | } 118 | }) 119 | .build(); 120 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Twilio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Twilio Voice Quickstart for Android 2 | 3 | > NOTE: This sample application uses the Programmable Voice Android 6.x APIs. If you are using prior versions of the SDK, we highly recommend planning your migration to the latest version as soon as possible. 4 | 5 | ## Get started with Voice on Android 6 | 7 | - [Quickstart](#quickstart) - Run the quickstart app 8 | - [Examples](#examples) - Customize your voice experience with these examples 9 | 10 | ## References 11 | - [Access Tokens](https://github.com/twilio/voice-quickstart-android/blob/master/Docs/access-token.md) - Using access tokens 12 | - [Managing Push Credentials](https://github.com/twilio/voice-quickstart-android/blob/master/Docs/manage-push-credentials.md) - Managing Push Credentials 13 | - [Managing Regional Push Credentials using Conversations Credential Resource API](https://github.com/twilio/voice-quickstart-android/blob/master/Docs/push-credentials-via-conversations-api.md) - Create or update push credentials for regional usage 14 | - [Troubleshooting](https://github.com/twilio/voice-quickstart-android/blob/master/Docs/troubleshooting.md) - Troubleshooting 15 | - [More Documentation](#more-documentation) - More documentation related to the Voice Android SDK 16 | - [Emulator Support](#emulator-support) - Android emulator support 17 | - [Reducing APK Size](https://github.com/twilio/voice-quickstart-android/blob/master/Docs/reducing-apk-size.md) - Use ABI splits to reduce APK size 18 | - [Twilio Helper Libraries](#twilio-helper-libraries) - TwiML quickstarts. 19 | - [Issues & Support](#issues-and-support) - Filing issues and general support 20 | 21 | ## Voice Android SDK Versions 22 | - [Migration Guide 4.x to 5.x](https://github.com/twilio/voice-quickstart-android/blob/master/Docs/migration-guide-4.x-5.x.md) - Migrating from 4.x to 5.x 23 | - [New Features 4.0](https://github.com/twilio/voice-quickstart-android/blob/master/Docs/new-features-4.0.md) - New features in 4.0 24 | - [Migration Guide 3.x to 4.x](https://github.com/twilio/voice-quickstart-android/blob/master/Docs/migration-guide-3.x-4.x.md) - Migrating from 3.x to 4.x 25 | - [New Features 3.0](https://github.com/twilio/voice-quickstart-android/blob/master/Docs/new-features-3.0.md) - New features in 3.0 26 | - [Migration Guide 2.x to 3.x](https://github.com/twilio/voice-quickstart-android/blob/master/Docs/migration-guide-2.x-3.x.md) - Migrating from 2.x to 3.x 27 | 28 | ## Quickstart 29 | 30 | The quickstart is broken into two flavors, "standard" & "connection_service", the latter showing how to integrate with the Android Telecom subsystem but requiring Android API 26. To get started with the Quickstart application follow these steps. Steps 1-5 will enable the application to make a call. The remaining steps 7-10 will enable the application to receive incoming calls in the form of push notifications using FCM. 31 | 32 | 1. [Generate google-services.json](#bullet1) 33 | 2. [Open this project in Android Studio](#bullet2) 34 | 3. [Use Twilio CLI to deploy access token and TwiML application to Twilio Serverless](#bullet3) 35 | 4. [Create a TwiML application for the access token](#bullet4) 36 | 5. [Generate an access token for the quickstart](#bullet5) 37 | 6. [Run the app](#bullet6) 38 | 7. [Create a Push Credential using your FCM Server API Key](#bullet7) 39 | 8. [Receive an incoming call](#bullet8) 40 | 9. [Make client to client call](#bullet9) 41 | 10. [Make client to PSTN call](#bullet10) 42 | 43 | 44 | ### 1. Generate `google-services.json` 45 | 46 | The Programmable Voice Android SDK uses Firebase Cloud Messaging push notifications to let your application know when it is receiving an incoming call. If you want your users to receive incoming calls, you’ll need to enable FCM in your application. 47 | 48 | Follow the steps under **Use the Firebase Assistant** in the [Firebase Developers Guide](https://firebase.google.com/docs/android/setup). Once you connect and sync to Firebase successfully, you will be able to download the `google-services.json` for your application. 49 | 50 | Login to Firebase console and make a note of generated `Server Key`. You will need them in [step 7](#bullet7). 51 | 52 | Make sure the generated `google-services.json` is downloaded to the `app` directory of the quickstart project to replace the existing `app/google-services.json` stub json file. If you are using the Firebase plugin make sure to remove the stub `google-services.json` file first. 53 | 54 | Missing valid `google-services.json` will result in a build failure with the following error message : 55 | " 56 | 57 | ### 2. Open the project in Android Studio 58 | 59 | 60 | 61 | ### 3. Use Twilio CLI to deploy access token and TwiML application to Twilio Serverless 62 | 63 | You must have the following installed: 64 | 65 | * [Node.js v10+](https://nodejs.org/en/download/) 66 | * NPM v6+ (comes installed with newer Node versions) 67 | 68 | Run `npm install` to install all dependencies from NPM. 69 | 70 | Install [twilio-cli](https://www.twilio.com/docs/twilio-cli/quickstart) with: 71 | 72 | $ npm install -g twilio-cli 73 | 74 | Login to the Twilio CLI. You will be prompted for your Account SID and Auth Token, both of which you can find on the dashboard of your [Twilio console](https://twilio.com/console). 75 | 76 | $ twilio login 77 | 78 | Once successfully logged in, an API Key, a secret get created and stored in your keychain as the twilio-cli password in `SKxxxx|secret` format. Please make a note of these values to use them in the `Server/.env` file. 79 | 80 | 81 | 82 | This app requires the [Serverless plug-in](https://github.com/twilio-labs/plugin-serverless). Install the CLI plugin with: 83 | 84 | $ twilio plugins:install @twilio-labs/plugin-serverless 85 | 86 | Before deploying, create a `Server/.env` by copying from `Server/.env.example` 87 | 88 | $ cp Server/.env.example Server/.env 89 | 90 | Update `Server/.env` with your Account SID, auth token, API Key and secret. 91 | 92 | ACCOUNT_SID=ACxxxx 93 | AUTH_TOKEN=xxxxxx 94 | API_KEY_SID=SKxxxx 95 | API_SECRET=xxxxxx 96 | APP_SID=APxxxx(available in step 4) 97 | PUSH_CREDENTIAL_SID=CRxxxx(available in step 7) 98 | 99 | The `Server` folder contains a basic server component which can be used to vend access tokens or generate TwiML response for making call to a number or another client. The app is deployed to Twilio Serverless with the `serverless` plug-in: 100 | 101 | $ cd Server 102 | $ twilio serverless:deploy 103 | 104 | The server component that's baked into this quickstart is in Node.js. If you’d like to roll your own or better understand the Twilio Voice server side implementations, please see the list of starter projects in the following supported languages below: 105 | 106 | * [voice-quickstart-server-java](https://github.com/twilio/voice-quickstart-server-java) 107 | * [voice-quickstart-server-node](https://github.com/twilio/voice-quickstart-server-node) 108 | * [voice-quickstart-server-php](https://github.com/twilio/voice-quickstart-server-php) 109 | * [voice-quickstart-server-python](https://github.com/twilio/voice-quickstart-server-python) 110 | 111 | Follow the instructions in the project's README to get the application server up and running locally and accessible via the public Internet. 112 | 113 | ### 4. Create a TwiML application for the Access Token 114 | 115 | Next, we need to create a TwiML application. A TwiML application identifies a public URL for retrieving [TwiML call control instructions](https://www.twilio.com/docs/voice/twiml). When your QS app makes a call to the Twilio cloud, Twilio will make a webhook request to this URL, your application server will respond with generated TwiML, and Twilio will execute the instructions you’ve provided. 116 | 117 | Use Twilio CLI to create a TwiML app with the `make-call` endpoint you have just deployed (**Note: replace the value of `--voice-url` parameter with your `make-call` endpoint you just deployed to Twilio Serverless**) 118 | 119 | $ twilio api:core:applications:create \ 120 | --friendly-name=my-twiml-app \ 121 | --voice-method=POST \ 122 | --voice-url="https://my-quickstart-dev.twil.io/make-call" 123 | 124 | You should receive an Appliciation SID that looks like this 125 | 126 | APxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 127 | 128 | ### 5. Generate an access token for the quickstart 129 | 130 | Install the `token` plug-in 131 | 132 | $ twilio plugins:install @twilio-labs/plugin-token 133 | 134 | Use the TwiML App SID you just created to generate an access token 135 | 136 | $ twilio token:voice --identity=alice --voice-app-sid=APxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 137 | 138 | Copy the access token string. Your Android app will use this token to connect to Twilio. 139 | 140 | 141 | ### 6. Run the app 142 | 143 | Now let’s go back to the `app`, update the placeholder of `accessToken` with access token string you just copied in `VoiceActivity.java`. 144 | 145 | ``` 146 | private String accessToken = "PASTE_YOUR_ACCESS_TOKEN_HERE"; 147 | ``` 148 | 149 | Build and run the quickstart app on an Android device. 150 | 151 | 152 | 153 | Press the call button to open the call dialog. 154 | 155 | 156 | 157 | Leave the dialog text field empty and press the call button to start a call. You will hear the congratulatory message. Support for dialing another client or number is described in steps 9 and 10. 158 | 159 | 160 | 161 | 162 | ### 7. Create a Push Credential using your FCM Server Key 163 | 164 | You will need to store the FCM Server key(The **Server key** of your project from the Firebase console, found under Settings/Cloud messaging) with Twilio so that we can send push notifications to your app on your behalf. Once you store the Server key with Twilio, it will get assigned a Push Credential SID so that you can later specify which key we should use to send push notifications. 165 | 166 | A FCMv1 server key can be generated from a Firebase Service account by selecting `Create New Key` and subsequently selecting a 'JSON' key type. Keep track of this generated key due to its limited accessibility. For more information on how to create a FCMv1 token, please follow this [document](https://help.twilio.com/articles/20768292997147-Updating-Twilio-Push-for-FCM-HTTP-v1-API). 167 | 168 | 169 | 170 | Once that token is created, go to your Twilio Console and from the "Account" drop-down on the upper right, select "Credentials". 171 | 172 | 173 | 174 | From the within the "Credentials" page, select the tab labeled "Push Credentials" and then click the large "+" button. 175 | 176 | 177 | 178 | After providing a friendly name, from the drop-down menu labeled "Type" select "FCM Push Credentials" and paste the key you generated in Firebase in the third box labeled "FCM Secret". Under the list of created "Push Credentials" you should now find your new push credential SID. 179 | 180 | 181 | 182 | The newly created Push Credential SID should look like this 183 | 184 | CRxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 185 | 186 | Now let's generate another access token and add the Push Credential to the Voice Grant. 187 | 188 | $ twilio token:voice \ 189 | --identity=alice \ 190 | --voice-app-sid=APxxxx \ 191 | --push-credential-sid=CRxxxxs 192 | 193 | 194 | ### 8. Receiving an Incoming Notification 195 | 196 | You are now ready to receive incoming calls. Update your app with the access token generated from step 7 and rebuild your app. The `Voice.register()` method will register your mobile application with the FCM device token as well as the access token. Once registered, hit your application server's **/place-call** endpoint: `https://my-quickstart-dev.twil.io/place-call?to=alice`. This will trigger a Twilio REST API request that will make an inbound call to your mobile app. 197 | 198 | Your application will be brought to the foreground and you will see an alert dialog. The app will be brought to foreground even when your screen is locked. 199 | 200 | " 201 | 202 | Once your app accepts the call, you should hear a congratulatory message. 203 | 204 | ### 9. Make client to client call 205 | 206 | To make client to client calls, you need the application running on two devices. To run the application on an additional device, make sure you use a different identity in your access token when registering the new device. 207 | 208 | Press the call button to open the call dialog. 209 | 210 | 211 | 212 | Enter the client identity of the newly registered device to initiate a client to client call from the first device. 213 | 214 | 215 | 216 | 217 | ### 10. Make client to PSTN call 218 | 219 | A verified phone number is one that you can use as your Caller ID when making outbound calls with Twilio. This number has not been ported into Twilio and you do not pay Twilio for this phone number. 220 | 221 | To make client to number calls, first get a valid Twilio number to your account via https://www.twilio.com/console/phone-numbers/verified. Update your server code and replace the `callerNumber` with the verified number. Restart the server so that it uses the new value. 222 | 223 | Press the call button to open the call dialog. 224 | 225 | 226 | 227 | Enter a PSTN number and press the call button to place a call. 228 | 229 | 230 | 231 | ## Examples 232 | In addition to the quickstart we've also added an example that shows how to create and customize media experience in your app: 233 | 234 | - [Custom Audio Device](https://github.com/twilio/voice-quickstart-android/tree/master/exampleCustomAudioDevice) - Demonstrates how to use Twilio's Programmable Voice SDK with audio playback and recording functionality provided by a custom `AudioDevice`. 235 | 236 | ## More Documentation 237 | 238 | You can find more documentation on getting started as well as our latest Javadoc below: 239 | 240 | 241 | * [Getting Started](https://www.twilio.com/docs/voice/sdks/android/get-started) 242 | * [Javadoc](https://media.twiliocdn.com/sdk/android/voice/latest/docs/) 243 | 244 | ## Twilio Helper Libraries 245 | 246 | To learn more about how to use TwiML and the Programmable Voice Calls API, check out our TwiML quickstarts: 247 | 248 | * [TwiML Quickstart for Python](https://www.twilio.com/docs/voice/quickstart/python) 249 | * [TwiML Quickstart for Ruby](https://www.twilio.com/docs/voice/quickstart/ruby) 250 | * [TwiML Quickstart for PHP](https://www.twilio.com/docs/voice/quickstart/php) 251 | * [TwiML Quickstart for Java](https://www.twilio.com/docs/voice/quickstart/java) 252 | * [TwiML Quickstart for C#](https://www.twilio.com/docs/voice/quickstart/csharp) 253 | 254 | ## Issues and Support 255 | 256 | Please file any issues you find here on Github. 257 | For general inquiries related to the Voice SDK you can file a support ticket. 258 | Please ensure that you are not sharing any 259 | [Personally Identifiable Information(PII)](https://www.twilio.com/docs/glossary/what-is-personally-identifiable-information-pii) 260 | or sensitive account information (API keys, credentials, etc.) when reporting an issue. 261 | 262 | ## License 263 | MIT 264 | -------------------------------------------------------------------------------- /Server/.env.example: -------------------------------------------------------------------------------- 1 | ACCOUNT_SID=ACxxxx 2 | AUTH_TOKEN=xxxxxx 3 | API_KEY_SID=SKxxxx 4 | API_SECRET=xxxxxx 5 | APP_SID=APxxxx 6 | PUSH_CREDENTIAL_SID=CRxxxx 7 | -------------------------------------------------------------------------------- /Server/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | npm-debug.log 4 | node_modules 5 | .twilio-functions 6 | -------------------------------------------------------------------------------- /Server/.nvmrc: -------------------------------------------------------------------------------- 1 | v16.20.1 2 | -------------------------------------------------------------------------------- /Server/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Twilio Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Server/assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Hello Twilio Serverless! 9 | 10 | 11 |

Hello Twilio Serverless!

12 | 13 |
14 |

15 | Congratulations you just started a new Twilio 16 | Serverless project. 17 |

18 | 19 |

Assets

20 |

21 | Assets are static files, like HTML, CSS, JavaScript, images or audio 22 | files. 23 |

24 | 25 |

26 | This HTML page is an example of a public asset, you can 27 | access this by loading it in the browser. The HTML also refers to 28 | another public asset for CSS styles. 29 |

30 |

31 | You can also have private assets, there is an example 32 | private asset called message.private.js in the 33 | /assets directory. This file cannot be loaded in the 34 | browser, but you can load it as part of a function by finding its path 35 | using Runtime.getAssets() and then requiring the file. 36 | There is an example of this in 37 | /functions/private-message.js. 38 |

39 | 40 |

Functions

41 |

42 | Functions are JavaScript files that will respond to incoming HTTP 43 | requests. There are public and 44 | protected functions. 45 |

46 | 47 |

48 | Public functions respond to all HTTP requests. There is 49 | an example of a public function in 50 | /functions/hello-world.js. 51 |

52 | 53 |

54 | Protected functions will only respond to HTTP requests 55 | with a valid Twilio signature in the header. You can read more about 56 | validating requests from Twilio in the documentation. There is an example of a protected function in 59 | /functions/sms/reply.protected.js 60 |

61 | 62 |

twilio-run

63 | 64 |

65 | Functions and assets are served, deployed and debugged using 66 | twilio-run. You can serve the project locally with the command 69 | npm start which is really running 70 | twilio-run --env under the hood. If you want to see what 71 | else you can do with twilio-run enter 72 | npx twilio-run --help on the command line or check out 73 | the project documentation on GitHub. 76 |

77 |
78 | 79 |
80 |

81 | Made with 💖 by your friends at 82 | Twilio 83 |

84 |
85 | 86 | 87 | -------------------------------------------------------------------------------- /Server/assets/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | ::selection { 8 | background: #f22f46; 9 | color: white; 10 | } 11 | 12 | body { 13 | font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, 14 | 'Helvetica Neue', Arial, sans-serif; 15 | color: #0d122b; 16 | border-top: 5px solid #f22f46; 17 | } 18 | 19 | header { 20 | padding: 2em; 21 | margin-bottom: 2em; 22 | max-width: 800px; 23 | margin: 0 auto; 24 | } 25 | 26 | header h1 { 27 | padding-bottom: 14px; 28 | border-bottom: 1px solid rgba(148, 151, 155, 0.2); 29 | } 30 | 31 | a { 32 | color: #008cff; 33 | } 34 | 35 | main { 36 | margin: 0 auto 6em; 37 | padding: 0 2em; 38 | max-width: 800px; 39 | } 40 | 41 | main p { 42 | margin-bottom: 2em; 43 | } 44 | 45 | main p code { 46 | font-size: 16px; 47 | font-family: 'Fira Mono', monospace; 48 | color: #f22f46; 49 | background-color: #f9f9f9; 50 | box-shadow: inset 0 0 0 1px #e8e8e8; 51 | font-size: inherit; 52 | line-height: 1.2; 53 | padding: 0.15em 0.4em; 54 | border-radius: 4px; 55 | display: inline-block; 56 | white-space: pre-wrap; 57 | } 58 | 59 | main h2 { 60 | margin-bottom: 1em; 61 | } 62 | 63 | footer { 64 | margin: 0 auto; 65 | max-width: 800px; 66 | text-align: center; 67 | } 68 | 69 | footer p { 70 | border-top: 1px solid rgba(148, 151, 155, 0.2); 71 | padding-top: 2em; 72 | margin: 0 2em; 73 | } 74 | -------------------------------------------------------------------------------- /Server/functions/access-token.js: -------------------------------------------------------------------------------- 1 | exports.handler = function(context, event, callback) { 2 | const AccessToken = require('twilio').jwt.AccessToken; 3 | const VoiceGrant = AccessToken.VoiceGrant; 4 | 5 | const twilioAccountSid = context.ACCOUNT_SID; 6 | const twilioApiKey = context.API_KEY_SID; 7 | const twilioApiSecret = context.API_SECRET; 8 | 9 | const outgoingApplicationSid = context.APP_SID; 10 | const pushCredentialSid = context.PUSH_CREDENTIAL_SID; 11 | const identity = 'user'; 12 | 13 | const voiceGrant = new VoiceGrant({ 14 | outgoingApplicationSid: outgoingApplicationSid, 15 | pushCredentialSid: pushCredentialSid 16 | }); 17 | 18 | const token = new AccessToken( 19 | twilioAccountSid, 20 | twilioApiKey, 21 | twilioApiSecret, 22 | { identity } 23 | ); 24 | token.addGrant(voiceGrant); 25 | 26 | callback(null, token.toJwt()); 27 | }; 28 | -------------------------------------------------------------------------------- /Server/functions/hello-world.js: -------------------------------------------------------------------------------- 1 | exports.handler = function(context, event, callback) { 2 | const twiml = new Twilio.twiml.VoiceResponse(); 3 | twiml.say('Hello World!'); 4 | callback(null, twiml); 5 | }; 6 | -------------------------------------------------------------------------------- /Server/functions/incoming.js: -------------------------------------------------------------------------------- 1 | exports.handler = function(context, event, callback) { 2 | const twiml = new Twilio.twiml.VoiceResponse(); 3 | twiml.say("Congratulations! You have received your first inbound call! Good bye."); 4 | 5 | callback(null, twiml.toString()); 6 | }; -------------------------------------------------------------------------------- /Server/functions/make-call.js: -------------------------------------------------------------------------------- 1 | const callerNumber = '1234567890'; 2 | const callerId = 'client:alice'; 3 | 4 | exports.handler = function(context, event, callback) { 5 | const twiml = new Twilio.twiml.VoiceResponse(); 6 | 7 | var to = (event.to) ? event.to : event.To; 8 | if (!to) { 9 | twiml.say('Congratulations! You have made your first call! Good bye.'); 10 | } else if (isNumber(to)) { 11 | const dial = twiml.dial({callerId : callerNumber}); 12 | dial.number(to); 13 | } else { 14 | const dial = twiml.dial({callerId : callerId}); 15 | dial.client(to); 16 | } 17 | 18 | callback(null, twiml); 19 | }; 20 | 21 | function isNumber(to) { 22 | if(to.length == 1) { 23 | if(!isNaN(to)) { 24 | console.log("It is a 1 digit long number" + to); 25 | return true; 26 | } 27 | } else if(String(to).charAt(0) == '+') { 28 | number = to.substring(1); 29 | if(!isNaN(number)) { 30 | console.log("It is a number " + to); 31 | return true; 32 | }; 33 | } else { 34 | if(!isNaN(to)) { 35 | console.log("It is a number " + to); 36 | return true; 37 | } 38 | } 39 | console.log("not a number"); 40 | return false; 41 | } -------------------------------------------------------------------------------- /Server/functions/place-call.js: -------------------------------------------------------------------------------- 1 | const callerNumber = '1234567890'; 2 | const callerId = 'client:alice'; 3 | const defaultIdentity = 'alice'; 4 | 5 | exports.handler = function(context, event, callback) { 6 | var url = 'https://' + context.DOMAIN_NAME + '/incoming'; 7 | 8 | const client = context.getTwilioClient(); 9 | 10 | var to = (event.to) ? event.to : event.To; 11 | if (!to) { 12 | client.calls.create({ 13 | url: url, 14 | to: 'client:' + defaultIdentity, 15 | from: callerId, 16 | }, function(err, result) { 17 | // End our function 18 | if (err) { 19 | callback(err, null); 20 | } else { 21 | callback(null, result); 22 | } 23 | }); 24 | } else if (isNumber(to)) { 25 | console.log("Calling number:" + to); 26 | client.calls.create({ 27 | url: url, 28 | to: to, 29 | from: callerNumber, 30 | }, function(err, result) { 31 | // End our function 32 | if (err) { 33 | callback(err, null); 34 | } else { 35 | callback(null, result); 36 | } 37 | }); 38 | } else { 39 | client.calls.create({ 40 | url: url, 41 | to: 'client:' + to, 42 | from: callerId, 43 | }, function(err, result) { 44 | // End our function 45 | if (err) { 46 | callback(err, null); 47 | } else { 48 | callback(null, result); 49 | } 50 | }); 51 | } 52 | }; 53 | 54 | function isNumber(to) { 55 | if(to.length == 1) { 56 | if(!isNaN(to)) { 57 | console.log("It is a 1 digit long number" + to); 58 | return true; 59 | } 60 | } else if(String(to).charAt(0) == '+') { 61 | number = to.substring(1); 62 | if(!isNaN(number)) { 63 | console.log("It is a number " + to); 64 | return true; 65 | }; 66 | } else { 67 | if(!isNaN(to)) { 68 | console.log("It is a number " + to); 69 | return true; 70 | } 71 | } 72 | console.log("not a number"); 73 | return false; 74 | } -------------------------------------------------------------------------------- /Server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quickstart", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "twilio-run", 9 | "deploy": "twilio-run deploy" 10 | }, 11 | "devDependencies": { 12 | "body-parser": "^1.20.3", 13 | "dotenv": "^4.0.0", 14 | "express": "^4.21.1", 15 | "twilio": "^5.1.0", 16 | "twilio-run": "^2.0.0" 17 | }, 18 | "engines": { 19 | "node": "16.20.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | ext.playCustomRingback = { 4 | def playCustomRingback = System.getenv("playCustomRingback"); 5 | 6 | if (playCustomRingback == null) { 7 | logger.log(LogLevel.INFO, "Could not locate playCustomRingback environment variable. " + 8 | "Trying local.properties") 9 | Properties properties = new Properties() 10 | if (project.rootProject.file('local.properties').exists()) { 11 | properties.load(project.rootProject.file('local.properties').newDataInputStream()) 12 | playCustomRingback = properties.getProperty('playCustomRingback') 13 | } 14 | } 15 | 16 | if (playCustomRingback == null) { 17 | playCustomRingback = false 18 | } 19 | 20 | return playCustomRingback; 21 | } 22 | 23 | android { 24 | namespace 'com.twilio.voice.quickstart' 25 | compileSdkVersion versions.compileSdk 26 | 27 | compileOptions { 28 | sourceCompatibility versions.java 29 | targetCompatibility versions.java 30 | } 31 | 32 | defaultConfig { 33 | applicationId "com.twilio.voice.quickstart" 34 | minSdkVersion versions.minSdk 35 | targetSdkVersion versions.targetSdk 36 | versionCode 1 37 | versionName "1.0" 38 | } 39 | 40 | buildFeatures { 41 | buildConfig = true 42 | } 43 | 44 | buildTypes { 45 | debug { 46 | buildConfigField("boolean", "playCustomRingback", "${playCustomRingback()}") 47 | } 48 | release { 49 | minifyEnabled true 50 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 51 | signingConfig signingConfigs.debug 52 | buildConfigField("boolean", "playCustomRingback", "${playCustomRingback()}") 53 | } 54 | } 55 | 56 | flavorDimensions.add("editions") 57 | productFlavors { 58 | standard { 59 | dimension "editions" 60 | getIsDefault().set(true) 61 | } 62 | connection_service { 63 | dimension "editions" 64 | applicationIdSuffix ".connection_service" 65 | versionNameSuffix "-connection_service" 66 | 67 | minSdkVersion 26 68 | targetSdkVersion versions.targetSdk 69 | } 70 | } 71 | 72 | // Specify that we want to split up the APK based on ABI 73 | splits { 74 | abi { 75 | // Enable ABI split 76 | enable true 77 | 78 | // Clear list of ABIs 79 | reset() 80 | 81 | // Specify each architecture currently supported by the Video SDK 82 | include "armeabi-v7a", "arm64-v8a", "x86", "x86_64" 83 | 84 | // Specify that we do not want an additional universal SDK 85 | universalApk false 86 | } 87 | } 88 | } 89 | 90 | dependencies { 91 | implementation "com.twilio:audioswitch:${versions.audioSwitch}" 92 | implementation "com.twilio:voice-android:${versions.voiceAndroid}" 93 | implementation "com.google.android.material:material:${versions.material}" 94 | implementation "com.google.firebase:firebase-messaging:${versions.firebase}" 95 | implementation "androidx.lifecycle:lifecycle-extensions:${versions.androidxLifecycle}" 96 | implementation 'com.google.firebase:firebase-auth:23.1.0' 97 | androidTestImplementation "androidx.test.ext:junit:${versions.junit}" 98 | 99 | // Import the BoM 100 | implementation platform('com.google.firebase:firebase-bom:33.7.0') 101 | } 102 | 103 | apply plugin: 'com.google.gms.google-services' -------------------------------------------------------------------------------- /app/google-services.json: -------------------------------------------------------------------------------- 1 | // Follow the steps in https://developers.google.com/mobile/add to generate your own google-services.json file. 2 | { 3 | "project_info": { 4 | "project_number": "your_project_number", 5 | "project_id": "your_project_id", 6 | "storage_bucket": "your_storage_bucket" 7 | }, 8 | "client": [ 9 | { 10 | "client_info": { 11 | "mobilesdk_app_id": "your_mobilesdk_app_id", 12 | "android_client_info": { 13 | "package_name": "com.twilio.voice.quickstart" 14 | } 15 | }, 16 | "oauth_client": [], 17 | "api_key": [ 18 | { 19 | "current_key": "your_current_key" 20 | } 21 | ], 22 | "services": { 23 | "appinvite_service": { 24 | "other_platform_oauth_client": [] 25 | } 26 | } 27 | }, 28 | { 29 | "client_info": { 30 | "mobilesdk_app_id": "your_mobilesdk_app_id", 31 | "android_client_info": { 32 | "package_name": "com.twilio.voice.quickstart.connection_service" 33 | } 34 | }, 35 | "oauth_client": [], 36 | "api_key": [ 37 | { 38 | "current_key": "your_current_key" 39 | } 40 | ], 41 | "services": { 42 | "appinvite_service": { 43 | "other_platform_oauth_client": [] 44 | } 45 | } 46 | } 47 | ], 48 | "configuration_version": "1" 49 | } -------------------------------------------------------------------------------- /app/gradle.properties: -------------------------------------------------------------------------------- 1 | android.useAndroidX=true 2 | android.enableJetifier=true -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Twilio Programmable Voice 2 | -keep class com.twilio.** { *; } 3 | -keep class tvo.webrtc.** { *; } 4 | -dontwarn tvo.webrtc.** 5 | -keep class com.twilio.voice.** { *; } 6 | -keepattributes InnerClasses 7 | # needed with AGP 8.x 8 | -dontwarn android.content.pm.PackageManager$ApplicationInfoFlags 9 | -dontwarn android.content.pm.PackageManager$PackageInfoFlags -------------------------------------------------------------------------------- /app/src/connection_service/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/connection_service/java/com/twilio/voice/quickstart/VoiceConnectionService.java: -------------------------------------------------------------------------------- 1 | package com.twilio.voice.quickstart; 2 | 3 | import static com.twilio.voice.quickstart.VoiceApplication.voiceService; 4 | 5 | import android.Manifest; 6 | import android.annotation.SuppressLint; 7 | import android.content.ComponentName; 8 | import android.content.Context; 9 | import android.content.pm.PackageManager; 10 | import android.net.Uri; 11 | import android.os.Bundle; 12 | import android.telecom.CallAudioState; 13 | import android.telecom.Connection; 14 | import android.telecom.ConnectionRequest; 15 | import android.telecom.ConnectionService; 16 | import android.telecom.DisconnectCause; 17 | import android.telecom.PhoneAccount; 18 | import android.telecom.PhoneAccountHandle; 19 | import android.telecom.TelecomManager; 20 | import android.telecom.VideoProfile; 21 | 22 | import androidx.annotation.CallSuper; 23 | import androidx.annotation.NonNull; 24 | import androidx.annotation.Nullable; 25 | import androidx.core.app.ActivityCompat; 26 | 27 | import com.twilio.voice.Call; 28 | import com.twilio.voice.CallException; 29 | import com.twilio.voice.CallInvite; 30 | import com.twilio.voice.ConnectOptions; 31 | import com.twilio.voice.RegistrationException; 32 | 33 | import java.util.HashMap; 34 | import java.util.HashSet; 35 | import java.util.Map; 36 | import java.util.Objects; 37 | import java.util.Set; 38 | import java.util.UUID; 39 | 40 | 41 | public class VoiceConnectionService extends ConnectionService { 42 | private static final Logger log = new Logger(VoiceConnectionService.class); 43 | private static final Map connectionDatabase = new HashMap<>(); 44 | private static final String CALL_RECIPIENT = "to"; 45 | 46 | private static class VoiceConnection extends Connection { 47 | private static final Map stateMappingTbl = new HashMap<>() {{ 48 | put(STATE_INITIALIZING, "STATE_INITIALIZING"); 49 | put(STATE_NEW, "STATE_NEW"); 50 | put(STATE_RINGING, "STATE_RINGING"); 51 | put(STATE_DIALING, "STATE_DIALING"); 52 | put(STATE_ACTIVE, "STATE_ACTIVE"); 53 | put(STATE_HOLDING, "STATE_HOLDING"); 54 | put(STATE_DISCONNECTED, "STATE_DISCONNECTED"); 55 | put(STATE_PULLING_CALL, "STATE_PULLING_CALL"); 56 | }}; 57 | 58 | private final UUID callId; 59 | 60 | public VoiceConnection(final UUID callID) { 61 | this.callId = callID; 62 | } 63 | 64 | @Override 65 | public void onStateChanged(int state) { 66 | log.debug("Connection:onStateChanged " + stateMappingTbl.get(state)); 67 | if (STATE_DISCONNECTED == state) { 68 | // remove from db 69 | VoiceConnectionService.connectionDatabase.remove(callId); 70 | 71 | // destroy/release 72 | this.destroy(); 73 | } 74 | } 75 | 76 | @Override 77 | public void onCallAudioStateChanged(CallAudioState state) { 78 | log.debug("Connection:onCallAudioStateChanged " + state); 79 | } 80 | 81 | @Override 82 | public void onPlayDtmfTone(char c) { 83 | log.debug("Connection:onPlayDtmfTone " + c); 84 | } 85 | 86 | @Override 87 | public void onDisconnect() { 88 | log.debug("Connection:onDisconnect"); 89 | voiceService(voiceService -> voiceService.disconnectCall(callId)); 90 | } 91 | 92 | @Override 93 | public void onSeparate() { 94 | log.debug("Connection:onSeparate"); 95 | } 96 | 97 | @Override 98 | public void onAbort() { 99 | log.debug("Connection:onAbort"); 100 | voiceService(voiceService -> voiceService.disconnectCall(callId)); 101 | } 102 | 103 | @CallSuper 104 | @Override 105 | public void onAnswer() { 106 | log.debug("Connection:onAnswer"); 107 | voiceService(voiceService -> voiceService.acceptCall(callId)); 108 | } 109 | 110 | @Override 111 | public void onReject() { 112 | log.debug("Connection:onReject"); 113 | voiceService(voiceService -> voiceService.rejectIncomingCall(callId)); 114 | } 115 | 116 | @Override 117 | public void onHold() { 118 | log.debug("Connection:onHold"); 119 | voiceService(voiceService -> voiceService.holdCall(callId)); 120 | } 121 | 122 | @Override 123 | public void onUnhold() { 124 | log.debug("Connection:onUnhold"); 125 | voiceService(voiceService -> voiceService.holdCall(callId)); 126 | } 127 | 128 | @Override 129 | public void onPostDialContinue(boolean proceed) { 130 | log.debug("Connection:proceed " + proceed); 131 | } 132 | } 133 | 134 | private static class VoiceObserver implements VoiceService.Observer { 135 | private final Set localDisconnectSet = new HashSet<>(); 136 | private final PhoneAccountHandle phoneAccountHandle; 137 | private final Context appContext; 138 | 139 | public VoiceObserver(final Context context) { 140 | // register telecom account info 141 | appContext = context.getApplicationContext(); 142 | String appName = appContext.getString(R.string.connection_service_name); 143 | phoneAccountHandle = new PhoneAccountHandle( 144 | new ComponentName(appContext, VoiceConnectionService.class), 145 | appName); 146 | TelecomManager telecomManager = 147 | (TelecomManager)appContext.getSystemService(TELECOM_SERVICE); 148 | PhoneAccount phoneAccount = new PhoneAccount.Builder(phoneAccountHandle, appName) 149 | .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER) 150 | .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED) 151 | .build(); 152 | telecomManager.registerPhoneAccount(phoneAccount); 153 | } 154 | 155 | @Override 156 | public void connectCall(@NonNull UUID callId, @NonNull ConnectOptions options) { 157 | VoiceConnectionService.placeCall( 158 | appContext, 159 | callId, 160 | Objects.requireNonNull(options.getParams().get("to")), 161 | phoneAccountHandle); 162 | } 163 | 164 | @Override 165 | public void disconnectCall(@NonNull final UUID callId) { 166 | // mark that request was local 167 | localDisconnectSet.add(callId); 168 | } 169 | 170 | @Override 171 | public void acceptIncomingCall(@NonNull final UUID callId) { 172 | // does nothing 173 | } 174 | 175 | @Override 176 | public void rejectIncomingCall(@NonNull UUID callId) { 177 | VoiceConnectionService 178 | .getConnection(callId) 179 | .setDisconnected(new DisconnectCause(DisconnectCause.REJECTED)); 180 | } 181 | 182 | @Override 183 | public void incomingCall(@NonNull UUID callId, @NonNull CallInvite callInvite) { 184 | VoiceConnectionService.incomingCall( 185 | appContext, 186 | callId, 187 | Objects.requireNonNull(callInvite.getFrom()), 188 | phoneAccountHandle); 189 | } 190 | 191 | @Override 192 | public void cancelledCall(@NonNull UUID callId) { 193 | VoiceConnectionService 194 | .getConnection(callId) 195 | .setDisconnected(new DisconnectCause(DisconnectCause.CANCELED)); 196 | } 197 | 198 | @Override 199 | public void muteCall(@NonNull final UUID callId, boolean isMuted) { 200 | // does nothing 201 | } 202 | 203 | @Override 204 | public void holdCall(@NonNull final UUID callId, boolean isOnHold) { 205 | // does nothing 206 | } 207 | 208 | @Override 209 | public void registrationSuccessful(@NonNull String fcmToken) { 210 | // does nothing 211 | } 212 | 213 | @Override 214 | public void registrationFailed(@NonNull RegistrationException registrationException) { 215 | // does nothing 216 | } 217 | 218 | @Override 219 | public void onRinging(@NonNull UUID callId) { 220 | // does nothing 221 | } 222 | 223 | @Override 224 | public void onConnectFailure(@NonNull UUID callId, @NonNull CallException callException) { 225 | VoiceConnectionService 226 | .getConnection(callId) 227 | .setDisconnected(new DisconnectCause( 228 | DisconnectCause.ERROR, 229 | callException.getMessage())); 230 | } 231 | 232 | @Override 233 | public void onConnected(@NonNull UUID callId) { 234 | VoiceConnectionService.getConnection(callId).setActive(); 235 | } 236 | 237 | @Override 238 | public void onReconnecting(@NonNull UUID callId, @NonNull CallException callException) { 239 | // does nothing 240 | } 241 | 242 | @Override 243 | public void onReconnected(@NonNull UUID callId) { 244 | // does nothing 245 | } 246 | 247 | @Override 248 | public void onDisconnected(@NonNull UUID callId, @Nullable CallException callException) { 249 | if (null == callException) { 250 | if (localDisconnectSet.contains(callId)) { 251 | VoiceConnectionService 252 | .getConnection(callId) 253 | .setDisconnected(new DisconnectCause(DisconnectCause.LOCAL)); 254 | localDisconnectSet.remove(callId); 255 | } else { 256 | VoiceConnectionService 257 | .getConnection(callId) 258 | .setDisconnected(new DisconnectCause(DisconnectCause.REMOTE)); 259 | } 260 | } else { 261 | VoiceConnectionService 262 | .getConnection(callId) 263 | .setDisconnected(new DisconnectCause( 264 | DisconnectCause.ERROR, 265 | callException.getMessage())); 266 | } 267 | } 268 | 269 | @Override 270 | public void onCallQualityWarningsChanged(@NonNull UUID callId, @NonNull Set currentWarnings, @NonNull Set previousWarnings) { 271 | // does nothing 272 | } 273 | } 274 | 275 | public enum AudioDevices { 276 | Earpiece, 277 | Speaker, 278 | Headset, 279 | Bluetooth 280 | } 281 | 282 | public static VoiceObserver getObserver(final Context context) { 283 | return new VoiceObserver(context); 284 | } 285 | 286 | public static void selectAudioDevice(@NonNull final UUID callId, 287 | @NonNull final AudioDevices audioDevice) { 288 | // find connection 289 | final Connection connection = Objects.requireNonNull(connectionDatabase.get(callId)); 290 | 291 | // set audio routing 292 | switch (audioDevice) { 293 | case Speaker: 294 | connection.setAudioRoute(CallAudioState.ROUTE_SPEAKER); 295 | break; 296 | case Earpiece: 297 | connection.setAudioRoute(CallAudioState.ROUTE_EARPIECE); 298 | break; 299 | case Headset: 300 | connection.setAudioRoute(CallAudioState.ROUTE_WIRED_HEADSET); 301 | break; 302 | case Bluetooth: 303 | connection.setAudioRoute(CallAudioState.ROUTE_BLUETOOTH); 304 | break; 305 | } 306 | } 307 | 308 | @Override 309 | public Connection onCreateOutgoingConnection(PhoneAccountHandle connectionManagerPhoneAccount, 310 | ConnectionRequest request) { 311 | // make android telephony connection 312 | Connection outgoingCallConnection = createConnection(request); 313 | outgoingCallConnection.setAddress( 314 | request.getExtras().getParcelable(CALL_RECIPIENT), 315 | TelecomManager.PRESENTATION_ALLOWED); 316 | outgoingCallConnection.setDialing(); 317 | 318 | // store in db 319 | final UUID callId = (UUID) request.getExtras().getSerializable(Constants.CALL_UUID); 320 | connectionDatabase.put(callId, outgoingCallConnection); 321 | 322 | return outgoingCallConnection; 323 | } 324 | 325 | @Override 326 | public Connection onCreateIncomingConnection(PhoneAccountHandle connectionManagerPhoneAccount, 327 | ConnectionRequest request) { 328 | // make android telephony connection 329 | Connection incomingCallConnection = createConnection(request); 330 | incomingCallConnection.setAddress(request.getAddress(), TelecomManager.PRESENTATION_ALLOWED); 331 | incomingCallConnection.setRinging(); 332 | 333 | // store in db 334 | final UUID callId = (UUID) Objects.requireNonNull( 335 | request.getExtras().getSerializable(Constants.CALL_UUID)); 336 | connectionDatabase.put(callId, incomingCallConnection); 337 | 338 | // make android telephony connection 339 | return incomingCallConnection; 340 | } 341 | 342 | @SuppressLint("MissingPermission") 343 | protected static void placeCall(@NonNull final Context context, 344 | @NonNull final UUID callId, 345 | @NonNull final String recipient, 346 | @NonNull final PhoneAccountHandle phoneAccountHandle) { 347 | log.debug("placeCall"); 348 | 349 | if (arePermissionsGranted(context)) { 350 | final Uri recipientUri = 351 | Uri.fromParts(PhoneAccount.SCHEME_TEL, recipient, null); 352 | 353 | // invoke service 354 | Bundle extra = new Bundle(); 355 | extra.putSerializable(Constants.CALL_UUID, callId); 356 | extra.putParcelable(CALL_RECIPIENT, recipientUri); 357 | 358 | Bundle telecomInfo = createTelephonyServiceBundle(phoneAccountHandle); 359 | telecomInfo.putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, extra); 360 | 361 | TelecomManager telecomMgr = (TelecomManager) context.getSystemService(TELECOM_SERVICE); 362 | telecomMgr.placeCall(recipientUri, telecomInfo); 363 | } 364 | } 365 | 366 | @SuppressLint("MissingPermission") 367 | protected static void incomingCall(@NonNull final Context context, 368 | @NonNull final UUID callId, 369 | @NonNull final String sender, 370 | @NonNull final PhoneAccountHandle phoneAccountHandle) { 371 | log.debug("incomingCall"); 372 | 373 | if (arePermissionsGranted(context)) { 374 | // invoke service 375 | Bundle telecomInfo = createTelephonyServiceBundle(phoneAccountHandle); 376 | telecomInfo.putSerializable(Constants.CALL_UUID, callId); 377 | telecomInfo.putParcelable( 378 | TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, 379 | Uri.fromParts(PhoneAccount.SCHEME_TEL, sender, null)); 380 | 381 | TelecomManager telecomMgr = (TelecomManager) context.getSystemService(TELECOM_SERVICE); 382 | telecomMgr.addNewIncomingCall(phoneAccountHandle, telecomInfo); 383 | } 384 | } 385 | 386 | protected static Connection getConnection(@NonNull final UUID callId) { 387 | return Objects.requireNonNull(connectionDatabase.get(callId)); 388 | } 389 | 390 | private Connection createConnection(ConnectionRequest request) { 391 | final UUID callId = (UUID) request.getExtras().getSerializable(Constants.CALL_UUID); 392 | Connection connection = new VoiceConnection(callId); 393 | 394 | // self managed isn't available before version O 395 | connection.setConnectionProperties(Connection.PROPERTY_SELF_MANAGED); 396 | 397 | // set mute & hold capability 398 | connection.setConnectionCapabilities(Connection.CAPABILITY_MUTE); 399 | connection.setConnectionCapabilities(Connection.CAPABILITY_HOLD); 400 | return connection; 401 | } 402 | 403 | private static boolean arePermissionsGranted(Context context) { 404 | return (PackageManager.PERMISSION_GRANTED == 405 | ActivityCompat.checkSelfPermission(context, Manifest.permission.MANAGE_OWN_CALLS)); 406 | } 407 | 408 | private static Bundle createTelephonyServiceBundle( 409 | @NonNull final PhoneAccountHandle phoneAccountHandle) { 410 | final Bundle telBundle = new Bundle(); 411 | telBundle.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle); 412 | telBundle.putInt(TelecomManager.EXTRA_INCOMING_VIDEO_STATE, VideoProfile.STATE_AUDIO_ONLY); 413 | return telBundle; 414 | } 415 | } 416 | -------------------------------------------------------------------------------- /app/src/connection_service/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Call phone permission needed. Please allow in your application settings. 4 | Manage Own Calls permission needed. Please allow in your application settings. 5 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 18 | 22 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/com/twilio/voice/quickstart/Constants.java: -------------------------------------------------------------------------------- 1 | package com.twilio.voice.quickstart; 2 | 3 | public class Constants { 4 | public static final String CALL_UUID = "CALL_UUID"; 5 | public static final String ACCESS_TOKEN = "ACCESS_TOKEN"; 6 | public static final String CUSTOM_RINGBACK = "CUSTOM_RINGBACK"; 7 | public static final String VOICE_CHANNEL_LOW_IMPORTANCE = "notification-channel-low-importance"; 8 | public static final String VOICE_CHANNEL_HIGH_IMPORTANCE = "notification-channel-high-importance"; 9 | public static final String INCOMING_CALL_INVITE = "INCOMING_CALL_INVITE"; 10 | public static final String CANCELLED_CALL_INVITE = "CANCELLED_CALL_INVITE"; 11 | public static final String ACTION_INCOMING_CALL_NOTIFICATION = "ACTION_INCOMING_CALL_NOTIFICATION"; 12 | public static final String ACTION_INCOMING_CALL = "ACTION_INCOMING_CALL"; 13 | public static final String ACTION_CANCEL_CALL = "ACTION_CANCEL_CALL"; 14 | public static final String ACTION_ACCEPT_CALL = "ACTION_ACCEPT"; 15 | public static final String ACTION_REJECT_CALL = "ACTION_REJECT"; 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/twilio/voice/quickstart/IncomingCallService.java: -------------------------------------------------------------------------------- 1 | package com.twilio.voice.quickstart; 2 | 3 | import static com.twilio.voice.quickstart.Constants.ACTION_INCOMING_CALL; 4 | import static com.twilio.voice.quickstart.Constants.INCOMING_CALL_INVITE; 5 | import static com.twilio.voice.quickstart.Constants.ACTION_CANCEL_CALL; 6 | import static com.twilio.voice.quickstart.Constants.CANCELLED_CALL_INVITE; 7 | import static java.lang.String.format; 8 | 9 | import android.content.Intent; 10 | 11 | import androidx.annotation.CallSuper; 12 | import androidx.annotation.NonNull; 13 | import androidx.annotation.Nullable; 14 | 15 | import android.os.Parcelable; 16 | import android.util.Pair; 17 | 18 | import com.google.firebase.messaging.FirebaseMessagingService; 19 | import com.google.firebase.messaging.RemoteMessage; 20 | import com.twilio.voice.CallException; 21 | import com.twilio.voice.CallInvite; 22 | import com.twilio.voice.CancelledCallInvite; 23 | import com.twilio.voice.MessageListener; 24 | import com.twilio.voice.Voice; 25 | 26 | public class IncomingCallService extends FirebaseMessagingService implements MessageListener { 27 | private static final Logger log = new Logger(IncomingCallService.class); 28 | 29 | @Override 30 | public void onMessageReceived(RemoteMessage remoteMessage) { 31 | log.debug(format( 32 | "Received firebase message\n\tmessage data: %s\n\tfrom: %s", 33 | remoteMessage.getData(), 34 | remoteMessage.getFrom())); 35 | 36 | // Check if message contains a data payload. 37 | if (!remoteMessage.getData().isEmpty() && !Voice.handleMessage(this, remoteMessage.getData(), this)) { 38 | log.error(format("Received message was not a valid Twilio Voice SDK payload: %s", remoteMessage.getData())); 39 | } 40 | } 41 | 42 | @CallSuper 43 | @Override 44 | public void onNewToken(@NonNull String token) { 45 | log.debug("[debug] onNewToken"); 46 | } 47 | 48 | @Override 49 | public void onCallInvite(@NonNull CallInvite callInvite) { 50 | startVoiceService( 51 | ACTION_INCOMING_CALL, 52 | new Pair<>(INCOMING_CALL_INVITE, callInvite)); 53 | } 54 | 55 | @Override 56 | public void onCancelledCallInvite(@NonNull CancelledCallInvite cancelledCallInvite, 57 | @Nullable CallException callException) { 58 | startVoiceService( 59 | ACTION_CANCEL_CALL, 60 | new Pair<>(CANCELLED_CALL_INVITE, cancelledCallInvite)); 61 | } 62 | 63 | @SafeVarargs 64 | private void startVoiceService(@NonNull final String action, 65 | @NonNull final Pair...data) { 66 | final Intent intent = new Intent(this, VoiceService.class); 67 | intent.setAction(action); 68 | for (Pair pair: data) { 69 | if (pair.second instanceof String) { 70 | intent.putExtra(pair.first, (String)pair.second); 71 | } else if (pair.second instanceof Parcelable) { 72 | intent.putExtra(pair.first, (Parcelable)pair.second); 73 | } 74 | } 75 | startService(intent); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/com/twilio/voice/quickstart/Logger.java: -------------------------------------------------------------------------------- 1 | package com.twilio.voice.quickstart; 2 | 3 | import android.util.Log; 4 | 5 | import java.io.IOException; 6 | import java.io.OutputStream; 7 | import java.io.PrintStream; 8 | import java.nio.charset.Charset; 9 | import java.util.Vector; 10 | 11 | class Logger extends OutputStream { 12 | private final String logTag; 13 | private final Vector logInfoBuffer = new Vector<>(); 14 | public Logger(Class clazz) { 15 | logTag = clazz.getSimpleName(); 16 | } 17 | 18 | public void debug(final String message) { 19 | if (BuildConfig.DEBUG) { 20 | Log.d(logTag, message); 21 | } 22 | } 23 | 24 | public void log(final String message) { 25 | Log.i(logTag, message); 26 | } 27 | 28 | public void warning(final String message) { 29 | try { 30 | write(message.getBytes()); 31 | flush(); 32 | } catch (Exception ignore) {} 33 | } 34 | 35 | public void error(final String message) { 36 | Log.e(logTag, message); 37 | } 38 | 39 | public void warning(final Exception e, final String message) { 40 | PrintStream printStream = new PrintStream(this); 41 | printStream.println(message); 42 | e.printStackTrace(printStream); 43 | printStream.flush(); 44 | } 45 | 46 | @Override 47 | public synchronized void write(int i) throws IOException { 48 | logInfoBuffer.add((char)i); 49 | } 50 | 51 | @Override 52 | public synchronized void write(byte[] b) throws IOException { 53 | for (char c: (new String(b, Charset.defaultCharset()).toCharArray())) { 54 | logInfoBuffer.add(c); 55 | } 56 | } 57 | 58 | @Override 59 | public void write(byte[] b, int off, int len) throws IOException { 60 | for (char c: (new String(b, off, len, Charset.defaultCharset()).toCharArray())) { 61 | logInfoBuffer.add(c); 62 | } 63 | } 64 | 65 | @Override 66 | public synchronized void flush() throws IOException { 67 | char [] output = new char[logInfoBuffer.size()]; 68 | for (int i = 0; i < logInfoBuffer.size(); ++i) { 69 | output[i] = logInfoBuffer.get(i); 70 | } 71 | logInfoBuffer.clear(); 72 | Log.w(logTag, String.valueOf(output)); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/twilio/voice/quickstart/SoundPoolManager.java: -------------------------------------------------------------------------------- 1 | package com.twilio.voice.quickstart; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.media.AudioManager; 6 | import android.media.SoundPool; 7 | 8 | 9 | import static android.content.Context.AUDIO_SERVICE; 10 | 11 | import static java.lang.String.format; 12 | 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | import java.util.Objects; 16 | 17 | @SuppressLint("DefaultLocale") 18 | class SoundPoolManager { 19 | enum Sound { 20 | RINGER, 21 | DISCONNECT 22 | } 23 | 24 | enum SoundState { 25 | LOADING, 26 | READY, 27 | PLAYING, 28 | ERROR 29 | } 30 | 31 | private static class SoundRecord { 32 | final int id; 33 | final boolean loop; 34 | SoundState state; 35 | 36 | public SoundRecord(Context context, 37 | SoundPool soundPool, 38 | final int resource, 39 | final boolean loop) { 40 | this.id = soundPool.load(context, resource, 1); 41 | this.state = SoundState.LOADING; 42 | this.loop = loop; 43 | } 44 | } 45 | 46 | private static final Logger log = new Logger(SoundPoolManager.class); 47 | private final float volume; 48 | private final SoundPool soundPool; 49 | 50 | private Map soundBank; 51 | private int lastActiveAudioStreamId; 52 | 53 | SoundPoolManager(final Context context) { 54 | // construct sound pool 55 | soundPool = new SoundPool.Builder().setMaxStreams(1).build(); 56 | soundPool.setOnLoadCompleteListener((soundPool, sampleId, status) -> { 57 | for (Map.Entry entry : soundBank.entrySet()) { 58 | final SoundRecord record = entry.getValue(); 59 | if (record.id == sampleId) { 60 | record.state = (0 == status) ? SoundState.READY : SoundState.ERROR; 61 | if (0 != status) { 62 | log.error( 63 | format("Failed to load sound %s, error: %d", 64 | entry.getKey().name(), status)); 65 | } 66 | } 67 | } 68 | }); 69 | 70 | // construct sound bank & load 71 | soundBank = new HashMap<>() {{ 72 | put(Sound.RINGER, new SoundRecord(context, soundPool, R.raw.incoming, true)); 73 | put(Sound.DISCONNECT, new SoundRecord(context, soundPool, R.raw.disconnect, false)); 74 | }}; 75 | 76 | // no active stream 77 | lastActiveAudioStreamId = -1; 78 | 79 | // AudioManager audio settings for adjusting the volume 80 | AudioManager audioManager = (AudioManager) context.getSystemService(AUDIO_SERVICE); 81 | float actualVolume = (float) audioManager.getStreamVolume(AudioManager.STREAM_MUSIC); 82 | float maxVolume = (float) audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); 83 | volume = actualVolume / maxVolume; 84 | } 85 | 86 | void playSound(final Sound sound) { 87 | final SoundRecord soundRecord = Objects.requireNonNull(soundBank.get(sound)); 88 | if (!isSoundPlaying() && SoundState.READY == soundRecord.state) { 89 | lastActiveAudioStreamId = soundPool.play( 90 | soundRecord.id, volume, volume, 1, soundRecord.loop ? -1 : 0, 1f); 91 | soundRecord.state = soundRecord.loop ? SoundState.PLAYING : SoundState.READY; 92 | } else if (isSoundPlaying()) { 93 | log.warning( 94 | format("cannot play sound %s: %d sound stream already active", 95 | sound.name(), lastActiveAudioStreamId)); 96 | } else { 97 | log.warning(format("cannot play sound %s: invalid state", sound.name())); 98 | } 99 | } 100 | 101 | void stopSound(final Sound sound) { 102 | final SoundRecord soundRecord = Objects.requireNonNull(soundBank.get(sound)); 103 | if (SoundState.PLAYING == soundRecord.state) { 104 | soundPool.stop(lastActiveAudioStreamId); 105 | } else { 106 | log.warning(format("cannot stop sound %s: invalid state", sound.name())); 107 | } 108 | } 109 | 110 | @Override 111 | protected void finalize() throws Throwable { 112 | for (SoundRecord record : soundBank.values()) { 113 | switch (record.state) { 114 | case PLAYING: 115 | soundPool.stop(lastActiveAudioStreamId); 116 | // intentionally fall through 117 | case READY: 118 | soundPool.unload(record.id); 119 | break; 120 | } 121 | } 122 | soundPool.release(); 123 | super.finalize(); 124 | } 125 | 126 | private boolean isSoundPlaying() { 127 | boolean playbackActive = false; 128 | for (SoundRecord record : soundBank.values()) { 129 | playbackActive |= (SoundState.PLAYING == record.state); 130 | } 131 | return playbackActive; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /app/src/main/java/com/twilio/voice/quickstart/VoiceApplication.java: -------------------------------------------------------------------------------- 1 | package com.twilio.voice.quickstart; 2 | 3 | import android.app.Application; 4 | import android.content.ComponentName; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.ServiceConnection; 8 | import android.os.IBinder; 9 | import android.os.Looper; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | public class VoiceApplication extends Application { 15 | 16 | private static VoiceApplication instance; 17 | private ServiceConnectionManager serviceConnectionManager; 18 | 19 | public interface VoiceServiceTask { 20 | void run(final VoiceService voiceService); 21 | } 22 | 23 | public static void voiceService(VoiceServiceTask task) { 24 | instance.serviceConnectionManager.invoke(task); 25 | } 26 | 27 | public VoiceApplication() { 28 | instance = this; 29 | } 30 | 31 | @Override 32 | public void onCreate() { 33 | super.onCreate(); 34 | // bind to voice service to keep it active 35 | serviceConnectionManager = new ServiceConnectionManager(this, VoiceActivity.accessToken); 36 | } 37 | 38 | @Override 39 | public void onTerminate() { 40 | // Note: this method is not called when running on device, devices just kill the process. 41 | serviceConnectionManager.unbind(); 42 | 43 | super.onTerminate(); 44 | } 45 | 46 | private static class ServiceConnectionManager { 47 | private VoiceService voiceService = null; 48 | private final List pendingTasks = new ArrayList<>(); 49 | private final String accessToken; 50 | private final Context context; 51 | private final ServiceConnection serviceConnection = new ServiceConnection() { 52 | 53 | @Override 54 | public void onServiceConnected(ComponentName name, IBinder service) { 55 | // verify is main thread, all Voice SDK calls must be made on the same Looper thread 56 | assert(Looper.myLooper() == Looper.getMainLooper()); 57 | // link to voice service 58 | voiceService = ((VoiceService.VideoServiceBinder)service).getService(); 59 | // run tasks 60 | synchronized(ServiceConnectionManager.this) { 61 | for (VoiceServiceTask task : pendingTasks) { 62 | task.run(voiceService); 63 | } 64 | pendingTasks.clear(); 65 | } 66 | } 67 | 68 | @Override 69 | public void onServiceDisconnected(ComponentName name) { 70 | voiceService = null; 71 | } 72 | }; 73 | 74 | public ServiceConnectionManager(final Context context, 75 | final String accessToken) { 76 | this.context = context; 77 | this.accessToken = accessToken; 78 | } 79 | 80 | public void unbind() { 81 | if (null != voiceService) { 82 | context.unbindService(serviceConnection); 83 | } 84 | } 85 | 86 | public void invoke(VoiceServiceTask task) { 87 | if (null != voiceService) { 88 | // verify is main thread, all Voice SDK calls must be made on the same Looper thread 89 | assert(Looper.myLooper() == Looper.getMainLooper()); 90 | // run task 91 | synchronized (this) { 92 | task.run(voiceService); 93 | } 94 | } else { 95 | // queue runnable 96 | pendingTasks.add(task); 97 | // bind to service 98 | Intent intent = new Intent(context, VoiceService.class); 99 | intent.putExtra(Constants.ACCESS_TOKEN, accessToken); 100 | intent.putExtra(Constants.CUSTOM_RINGBACK, BuildConfig.playCustomRingback); 101 | context.bindService( 102 | intent, 103 | serviceConnection, 104 | BIND_AUTO_CREATE); 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_bluetooth_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_call_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_call_end_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_call_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_headset_mic_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 8 | 15 | 21 | 27 | 33 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_mic_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_mic_white_off_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 10 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_pause_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_phonelink_ring_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_volume_up_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_voice.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 20 | 21 | 31 | 32 | 38 | 39 | 49 | 50 | 60 | 61 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_call.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 14 | 15 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /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/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/raw/disconnect.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/app/src/main/res/raw/disconnect.wav -------------------------------------------------------------------------------- /app/src/main/res/raw/incoming.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/app/src/main/res/raw/incoming.wav -------------------------------------------------------------------------------- /app/src/main/res/raw/outgoing.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/app/src/main/res/raw/outgoing.wav -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #f10028 4 | #a3090e 5 | #b0bec5 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Voice Quickstart 3 | Audio Device 4 | client identity or phone number 5 | Answer 6 | Decline 7 | Twilio Voice 8 | Dial a client name or phone number. Leaving the field empty results in an automated response. 9 | Select Audio Device 10 | Audio recording permission needed. Please allow in your application settings. 11 | Without bluetooth permission app will fail to use bluetooth. 12 | Notification permissions needed for receiving incoming phone calls. Please allow in your application settings. 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/standard/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 11 | 12 | 13 | 14 | 15 | 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 | ext.versions = [ 6 | 'java' : JavaVersion.VERSION_11, 7 | 'androidGradlePlugin': '8.3.1', 8 | 'googleServices' : '4.3.14', 9 | 'compileSdk' : 34, 10 | 'buildTools' : '34.0.0', 11 | 'minSdk' : 23, 12 | 'targetSdk' : 34, 13 | 'material' : '1.12.0', 14 | 'firebase' : '24.1.0', 15 | 'voiceAndroid' : '6.9.+', 16 | 'audioSwitch' : '1.2.0', 17 | 'androidxLifecycle' : '2.2.0', 18 | 'junit' : '1.2.1' 19 | ] 20 | 21 | repositories { 22 | mavenCentral() 23 | google() 24 | } 25 | dependencies { 26 | classpath "com.android.tools.build:gradle:${versions.androidGradlePlugin}" 27 | classpath "com.google.gms:google-services:${versions.googleServices}" 28 | 29 | // NOTE: Do not place your application dependencies here; they belong 30 | // in the individual module build.gradle files 31 | } 32 | } 33 | 34 | allprojects { 35 | repositories { 36 | google() 37 | mavenCentral() 38 | } 39 | } 40 | 41 | task clean(type: Delete) { 42 | delete rootProject.buildDir 43 | } 44 | -------------------------------------------------------------------------------- /exampleCustomAudioDevice/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /exampleCustomAudioDevice/README.md: -------------------------------------------------------------------------------- 1 | # Twilio Voice AudioDevice Example 2 | 3 | The project demonstrates how to use Twilio's Programmable Voice SDK with audio playback and recording functionality provided by a custom `AudioDevice`. 4 | 5 | The example demonstrates the custom audio device **exampleCustomAudioDevice**, which uses android audio subsystem to playback and record audio at 44.1KHz with built-in echo and noise cancellation. 6 | 7 | 1. The upstream audio subsystem receives remote participant's playout audio samples from the code audio device module and plays them in the speaker. 8 | 2. The downstream audio subsystem is capable to switch audio source between the local participant's microphone audio and audio from a file. The Voice SDK receives and delivers the recorded audio samples to the core audio device module. 9 | 10 | This diagram describes how **exampleCustomAudioDevice** uses `AudioDevice` to receive and deliver audio samples from/to the core audio device. 11 | 12 | 13 | 14 | ### Setup 15 | 16 | Refer to the [README](https://github.com/twilio/voice-quickstart-android/blob/master/README.md) for instructions on how to generate an access token and make an outbound `Call`. 17 | In [step 6](https://github.com/twilio/voice-quickstart-android#6-run-the-app), update the placeholder of `accessToken` with access token string you copied in `CustomDeviceActivity.java`. 18 | 19 | 20 | ### Running 21 | 22 | Once you have configured your access token, build and run the example. Press the call button to open the call dialog and make an outbound call to a [client](https://github.com/twilio/voice-quickstart-android#bullet10) or to a [PSTN](https://github.com/twilio/voice-quickstart-android#11-make-client-to-pstn-call) number. 23 | 24 | 25 | 26 | Audio from a file is selected by default. Once the Call is connected, music starts to play. 27 | 28 | 29 | 30 | You can switch to microphone by clicking the `♫` button. 31 | 32 | 33 | 34 | Note: The switch between audio file and microphone always starts the music from the begining of the file. 35 | 36 | ## Troubleshooting Audio 37 | The following sections provide guidance on how to ensure optimal audio quality in your applications using default audio device. 38 | 39 | ### Configuring AudioManager 40 | 41 | Follow the [README](https://github.com/twilio/voice-quickstart-android#configuring-audiomanager) to configure audio manager. 42 | 43 | ### Configuring Hardware Audio Effects Using Custom Audio Device 44 | Our library performs acoustic echo cancellation (AEC) and noise suppression (NS) using device hardware by default. Using device hardware is more efficient, but some devices do not implement these audio effects well. If you are experiencing echo or background noise on certain devices reference the following snippet for enabling software implementations of AEC and NS. 45 | 46 | /* 47 | * Execute any time before invoking `Voice.connect(...)` or `CallInvite.accept(...)`. 48 | */ 49 | 50 | // Use software AEC 51 | DefaultAudioDevice defaultAudioDevice = new DefaultAudioDevice(); 52 | defaultAudioDevice.setUseHardwareAcousticEchoCanceler(false); 53 | Voice.setAudioDevice(defaultAudioDevice); 54 | 55 | // Use sofware NS 56 | DefaultAudioDevice defaultAudioDevice = new DefaultAudioDevice(); 57 | defaultAudioDevice.setUseHardwareNoiseSuppressor(false); 58 | Voice.setAudioDevice(defaultAudioDevice); 59 | 60 | ### Configuring OpenSL ES 61 | Follow the [README](https://github.com/twilio/voice-quickstart-android#configuring-opensl-es) to configure OpenSL ES. 62 | 63 | ### Managing Device Specific Configurations 64 | Follow the [README](https://github.com/twilio/voice-quickstart-android#managing-device-specific-configurations) to manage device specific configurations. 65 | 66 | ### Handling Low Headset Volume 67 | Follow the [README](https://github.com/twilio/voice-quickstart-android#handling-low-headset-volume) to handle low handset volume. 68 | -------------------------------------------------------------------------------- /exampleCustomAudioDevice/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | namespace 'com.twilio.examplecustomaudiodevice' 5 | compileSdkVersion versions.compileSdk 6 | buildToolsVersion versions.buildTools 7 | 8 | compileOptions { 9 | sourceCompatibility versions.java 10 | targetCompatibility versions.java 11 | } 12 | 13 | defaultConfig { 14 | applicationId "com.twilio.examplecustomaudiodevice" 15 | minSdkVersion versions.minSdk 16 | targetSdkVersion versions.targetSdk 17 | versionCode 1 18 | versionName "1.0" 19 | } 20 | 21 | buildFeatures { 22 | buildConfig = true 23 | } 24 | 25 | buildTypes { 26 | release { 27 | minifyEnabled true 28 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 29 | signingConfig signingConfigs.debug 30 | } 31 | } 32 | 33 | // Specify that we want to split up the APK based on ABI 34 | splits { 35 | abi { 36 | // Enable ABI split 37 | enable true 38 | 39 | // Clear list of ABIs 40 | reset() 41 | 42 | // Specify each architecture currently supported by the Video SDK 43 | include "armeabi-v7a", "arm64-v8a", "x86", "x86_64" 44 | 45 | // Specify that we do not want an additional universal SDK 46 | universalApk false 47 | } 48 | } 49 | } 50 | 51 | dependencies { 52 | implementation "com.twilio:voice-android:${versions.voiceAndroid}" 53 | implementation "com.google.android.material:material:${versions.material}" 54 | implementation "androidx.lifecycle:lifecycle-extensions:${versions.androidxLifecycle}" 55 | androidTestImplementation "androidx.test.ext:junit:${versions.junit}" 56 | } -------------------------------------------------------------------------------- /exampleCustomAudioDevice/gradle.properties: -------------------------------------------------------------------------------- 1 | android.useAndroidX=true 2 | android.enableJetifier=true -------------------------------------------------------------------------------- /exampleCustomAudioDevice/libs/voice-release.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/exampleCustomAudioDevice/libs/voice-release.aar -------------------------------------------------------------------------------- /exampleCustomAudioDevice/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 | -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/java/com/twilio/examplecustomaudiodevice/FileAndMicAudioDevice.java: -------------------------------------------------------------------------------- 1 | package com.twilio.examplecustomaudiodevice; 2 | 3 | import android.content.Context; 4 | import android.media.AudioManager; 5 | import android.media.AudioRecord; 6 | import android.media.AudioTrack; 7 | import android.media.MediaRecorder; 8 | import android.os.Build; 9 | import android.os.Handler; 10 | import android.os.HandlerThread; 11 | import android.os.Process; 12 | import android.util.Log; 13 | 14 | import androidx.annotation.NonNull; 15 | import androidx.annotation.Nullable; 16 | import androidx.annotation.RequiresApi; 17 | 18 | import com.twilio.voice.AudioDevice; 19 | import com.twilio.voice.AudioDeviceContext; 20 | import com.twilio.voice.AudioFormat; 21 | 22 | import java.io.DataInputStream; 23 | import java.io.IOException; 24 | import java.io.InputStream; 25 | import java.nio.ByteBuffer; 26 | 27 | public class FileAndMicAudioDevice implements AudioDevice { 28 | private static final String TAG = FileAndMicAudioDevice.class.getSimpleName(); 29 | // TIMEOUT for rendererThread and capturerThread to wait for successful call to join() 30 | private static final long THREAD_JOIN_TIMEOUT_MS = 2000; 31 | 32 | private Context context; 33 | private boolean keepAliveRendererRunnable = true; 34 | // We want to get as close to 10 msec buffers as possible because this is what the media engine prefers. 35 | private static final int CALLBACK_BUFFER_SIZE_MS = 10; 36 | // Default audio data format is PCM 16 bit per sample. Guaranteed to be supported by all devices. 37 | private static final int BITS_PER_SAMPLE = 16; 38 | // Average number of callbacks per second. 39 | private int BUFFERS_PER_SECOND = 1000 / CALLBACK_BUFFER_SIZE_MS; 40 | // Ask for a buffer size of BUFFER_SIZE_FACTOR * (minimum required buffer size). The extra space 41 | // is allocated to guard against glitches under high load. 42 | private static final int BUFFER_SIZE_FACTOR = 2; 43 | private static final int WAV_FILE_HEADER_SIZE = 44; 44 | 45 | private ByteBuffer fileWriteByteBuffer; 46 | private int writeBufferSize; 47 | private InputStream inputStream; 48 | private DataInputStream dataInputStream; 49 | 50 | private AudioRecord audioRecord; 51 | private ByteBuffer micWriteBuffer; 52 | 53 | private ByteBuffer readByteBuffer; 54 | private AudioTrack audioTrack = null; 55 | 56 | // Handlers and Threads 57 | private Handler capturerHandler; 58 | private HandlerThread capturerThread; 59 | private Handler rendererHandler; 60 | private HandlerThread rendererThread; 61 | 62 | private AudioDeviceContext renderingAudioDeviceContext; 63 | private AudioDeviceContext capturingAudioDeviceContext; 64 | // By default music capturer is enabled 65 | private boolean isMusicPlaying = true; 66 | 67 | /* 68 | * This Runnable reads a music file and provides the audio frames to the AudioDevice API via 69 | * AudioDevice.audioDeviceWriteCaptureData(..) until there is no more data to be read, the 70 | * capturer input switches to the microphone, or the call ends. 71 | */ 72 | private final Runnable fileCapturerRunnable = () -> { 73 | Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO); 74 | int bytesRead; 75 | try { 76 | if (dataInputStream != null && (bytesRead = dataInputStream.read(fileWriteByteBuffer.array(), 0, writeBufferSize)) > -1) { 77 | if (bytesRead == fileWriteByteBuffer.capacity()) { 78 | AudioDevice.audioDeviceWriteCaptureData(capturingAudioDeviceContext, fileWriteByteBuffer); 79 | } else { 80 | processRemaining(fileWriteByteBuffer, fileWriteByteBuffer.capacity()); 81 | AudioDevice.audioDeviceWriteCaptureData(capturingAudioDeviceContext, fileWriteByteBuffer); 82 | } 83 | } 84 | } catch (IOException e) { 85 | e.printStackTrace(); 86 | } 87 | capturerHandler.postDelayed(this.fileCapturerRunnable, CALLBACK_BUFFER_SIZE_MS); 88 | }; 89 | 90 | /* 91 | * This Runnable reads data from the microphone and provides the audio frames to the AudioDevice 92 | * API via AudioDevice.audioDeviceWriteCaptureData(..) until the capturer input switches to 93 | * microphone or the call ends. 94 | */ 95 | private final Runnable microphoneCapturerRunnable = () -> { 96 | Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO); 97 | audioRecord.startRecording(); 98 | while (true) { 99 | int bytesRead = audioRecord.read(micWriteBuffer, micWriteBuffer.capacity()); 100 | if (bytesRead == micWriteBuffer.capacity()) { 101 | AudioDevice.audioDeviceWriteCaptureData(capturingAudioDeviceContext, micWriteBuffer); 102 | } else { 103 | String errorMessage = "AudioRecord.read failed: " + bytesRead; 104 | Log.e(TAG, errorMessage); 105 | if (bytesRead == AudioRecord.ERROR_INVALID_OPERATION) { 106 | stopRecording(); 107 | Log.e(TAG, errorMessage); 108 | } 109 | break; 110 | } 111 | } 112 | }; 113 | 114 | /* 115 | * This Runnable reads audio data from the callee perspective via AudioDevice.audioDeviceReadRenderData(...) 116 | * and plays out the audio data using AudioTrack.write(). 117 | */ 118 | private final Runnable speakerRendererRunnable = () -> { 119 | Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO); 120 | 121 | try { 122 | audioTrack.play(); 123 | } catch (IllegalStateException e) { 124 | Log.e(TAG, "AudioTrack.play failed: " + e.getMessage()); 125 | this.releaseAudioResources(); 126 | return; 127 | } 128 | try { 129 | while (keepAliveRendererRunnable) { 130 | // Get 10ms of PCM data from the SDK. Audio data is written into the ByteBuffer provided. 131 | AudioDevice.audioDeviceReadRenderData(renderingAudioDeviceContext, readByteBuffer); 132 | 133 | int bytesWritten = 0; 134 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 135 | bytesWritten = writeOnLollipop(audioTrack, readByteBuffer, readByteBuffer.capacity()); 136 | } else { 137 | bytesWritten = writePreLollipop(audioTrack, readByteBuffer, readByteBuffer.capacity()); 138 | } 139 | if (bytesWritten != readByteBuffer.capacity()) { 140 | Log.e(TAG, "AudioTrack.write failed: " + bytesWritten); 141 | if (bytesWritten == AudioTrack.ERROR_INVALID_OPERATION) { 142 | keepAliveRendererRunnable = false; 143 | break; 144 | } 145 | } 146 | // The byte buffer must be rewinded since byteBuffer.position() is increased at each 147 | // call to AudioTrack.write(). If we don't do this, will fail the next AudioTrack.write(). 148 | readByteBuffer.rewind(); 149 | } 150 | } catch (IllegalStateException error) { 151 | error.printStackTrace(); 152 | releaseAudioResources(); 153 | } 154 | }; 155 | 156 | public FileAndMicAudioDevice(Context context) { 157 | this.context = context; 158 | } 159 | 160 | /* 161 | * This method enables a capturer switch between a file and the microphone. 162 | * @param playMusic 163 | */ 164 | public void switchInput(boolean playMusic) { 165 | isMusicPlaying = playMusic; 166 | if (playMusic) { 167 | initializeStreams(); 168 | capturerHandler.removeCallbacks(microphoneCapturerRunnable); 169 | stopRecording(); 170 | capturerHandler.post(fileCapturerRunnable); 171 | } else { 172 | capturerHandler.removeCallbacks(fileCapturerRunnable); 173 | capturerHandler.post(microphoneCapturerRunnable); 174 | } 175 | } 176 | 177 | public boolean isMusicPlaying() { 178 | return isMusicPlaying; 179 | } 180 | 181 | /* 182 | * Return the AudioFormat used the capturer. This custom device uses 44.1kHz sample rate and 183 | * STEREO channel configuration both for microphone and the music file. 184 | */ 185 | @Nullable 186 | @Override 187 | public AudioFormat getCapturerFormat() { 188 | return new AudioFormat(AudioFormat.AUDIO_SAMPLE_RATE_44100, 189 | AudioFormat.AUDIO_SAMPLE_STEREO); 190 | } 191 | 192 | /* 193 | * Init the capturer using the AudioFormat return by getCapturerFormat(). 194 | */ 195 | @Override 196 | public boolean onInitCapturer() { 197 | int bytesPerFrame = 2 * (BITS_PER_SAMPLE / 8); 198 | int framesPerBuffer = getCapturerFormat().getSampleRate() / BUFFERS_PER_SECOND; 199 | // Calculate the minimum buffer size required for the successful creation of 200 | // an AudioRecord object, in byte units. 201 | int channelConfig = channelCountToConfiguration(getCapturerFormat().getChannelCount()); 202 | int minBufferSize = 203 | AudioRecord.getMinBufferSize(getCapturerFormat().getSampleRate(), 204 | channelConfig, android.media.AudioFormat.ENCODING_PCM_16BIT); 205 | micWriteBuffer = ByteBuffer.allocateDirect(bytesPerFrame * framesPerBuffer); 206 | int bufferSizeInBytes = Math.max(BUFFER_SIZE_FACTOR * minBufferSize, micWriteBuffer.capacity()); 207 | audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, getCapturerFormat().getSampleRate(), 208 | android.media.AudioFormat.CHANNEL_OUT_STEREO, android.media.AudioFormat.ENCODING_PCM_16BIT, bufferSizeInBytes); 209 | 210 | fileWriteByteBuffer = ByteBuffer.allocateDirect(bytesPerFrame * framesPerBuffer); 211 | writeBufferSize = fileWriteByteBuffer.capacity(); 212 | // Initialize the streams. 213 | initializeStreams(); 214 | return true; 215 | } 216 | 217 | @Override 218 | public boolean onStartCapturing(@NonNull AudioDeviceContext audioDeviceContext) { 219 | // Initialize the AudioDeviceContext 220 | this.capturingAudioDeviceContext = audioDeviceContext; 221 | // Create the capturer thread and start 222 | capturerThread = new HandlerThread("CapturerThread"); 223 | capturerThread.start(); 224 | // Create the capturer handler that processes the capturer Runnables. 225 | capturerHandler = new Handler(capturerThread.getLooper()); 226 | capturerHandler.post(fileCapturerRunnable); 227 | return true; 228 | } 229 | 230 | @Override 231 | public boolean onStopCapturing() { 232 | if (isMusicPlaying) { 233 | closeStreams(); 234 | } else { 235 | stopRecording(); 236 | } 237 | /* 238 | * When onStopCapturing is called, the AudioDevice API expects that at the completion 239 | * of the callback the capturer has completely stopped. As a result, quit the capturer 240 | * thread and explicitly wait for the thread to complete. 241 | */ 242 | capturerThread.quit(); 243 | if (!tvo.webrtc.ThreadUtils.joinUninterruptibly(capturerThread, THREAD_JOIN_TIMEOUT_MS)) { 244 | Log.e(TAG, "Join of capturerThread timed out"); 245 | return false; 246 | } 247 | return true; 248 | } 249 | 250 | /* 251 | * Return the AudioFormat used the renderer. This custom device uses 44.1kHz sample rate and 252 | * STEREO channel configuration for audio track. 253 | */ 254 | @Nullable 255 | @Override 256 | public AudioFormat getRendererFormat() { 257 | return new AudioFormat(AudioFormat.AUDIO_SAMPLE_RATE_44100, 258 | AudioFormat.AUDIO_SAMPLE_STEREO); 259 | } 260 | 261 | @Override 262 | public boolean onInitRenderer() { 263 | int bytesPerFrame = getRendererFormat().getChannelCount() * (BITS_PER_SAMPLE / 8); 264 | readByteBuffer = ByteBuffer.allocateDirect(bytesPerFrame * (getRendererFormat().getSampleRate() / BUFFERS_PER_SECOND)); 265 | int channelConfig = channelCountToConfiguration(getRendererFormat().getChannelCount()); 266 | int minBufferSize = AudioRecord.getMinBufferSize(getRendererFormat().getSampleRate(), channelConfig, android.media.AudioFormat.ENCODING_PCM_16BIT); 267 | audioTrack = new AudioTrack(AudioManager.STREAM_VOICE_CALL, getRendererFormat().getSampleRate(), channelConfig, 268 | android.media.AudioFormat.ENCODING_PCM_16BIT, minBufferSize, AudioTrack.MODE_STREAM); 269 | keepAliveRendererRunnable = true; 270 | return true; 271 | } 272 | 273 | @Override 274 | public boolean onStartRendering(@NonNull AudioDeviceContext audioDeviceContext) { 275 | this.renderingAudioDeviceContext = audioDeviceContext; 276 | // Create the renderer thread and start 277 | rendererThread = new HandlerThread("RendererThread"); 278 | rendererThread.start(); 279 | // Create the capturer handler that processes the renderer Runnables. 280 | rendererHandler = new Handler(rendererThread.getLooper()); 281 | rendererHandler.post(speakerRendererRunnable); 282 | return true; 283 | } 284 | 285 | @Override 286 | public boolean onStopRendering() { 287 | stopAudioTrack(); 288 | // Quit the rendererThread's looper to stop processing any further messages. 289 | rendererThread.quit(); 290 | /* 291 | * When onStopRendering is called, the AudioDevice API expects that at the completion 292 | * of the callback the renderer has completely stopped. As a result, quit the renderer 293 | * thread and explicitly wait for the thread to complete. 294 | */ 295 | if (!tvo.webrtc.ThreadUtils.joinUninterruptibly(rendererThread, THREAD_JOIN_TIMEOUT_MS)) { 296 | Log.e(TAG, "Join of rendererThread timed out"); 297 | return false; 298 | } 299 | return true; 300 | } 301 | 302 | // Capturer helper methods 303 | private void initializeStreams() { 304 | inputStream = null; 305 | dataInputStream = null; 306 | inputStream = context.getResources().openRawResource(context.getResources().getIdentifier("music", 307 | "raw", context.getPackageName())); 308 | dataInputStream = new DataInputStream(inputStream); 309 | try { 310 | int bytes = dataInputStream.skipBytes(WAV_FILE_HEADER_SIZE); 311 | Log.d(TAG, "Number of bytes skipped : " + bytes); 312 | } catch (IOException e) { 313 | e.printStackTrace(); 314 | } 315 | } 316 | 317 | private void closeStreams() { 318 | Log.d(TAG, "Remove any pending posts of fileCapturerRunnable that are in the message queue "); 319 | capturerHandler.removeCallbacks(fileCapturerRunnable); 320 | try { 321 | dataInputStream.close(); 322 | inputStream.close(); 323 | } catch (IOException e) { 324 | e.printStackTrace(); 325 | } 326 | } 327 | 328 | private void stopRecording() { 329 | Log.d(TAG, "Remove any pending posts of microphoneCapturerRunnable that are in the message queue "); 330 | capturerHandler.removeCallbacks(microphoneCapturerRunnable); 331 | try { 332 | if (audioRecord != null) { 333 | audioRecord.stop(); 334 | } 335 | } catch (IllegalStateException e) { 336 | Log.e(TAG, "AudioRecord.stop failed: " + e.getMessage()); 337 | } 338 | } 339 | 340 | private int channelCountToConfiguration(int channels) { 341 | return (channels == 1 ? android.media.AudioFormat.CHANNEL_IN_MONO : android.media.AudioFormat.CHANNEL_IN_STEREO); 342 | } 343 | 344 | private void processRemaining(ByteBuffer bb, int chunkSize) { 345 | bb.position(bb.limit()); // move at the end 346 | bb.limit(chunkSize); // get ready to pad with longs 347 | while (bb.position() < chunkSize) { 348 | bb.putLong(0); 349 | } 350 | bb.limit(chunkSize); 351 | bb.flip(); 352 | } 353 | 354 | // Renderer helper methods 355 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 356 | private int writeOnLollipop(AudioTrack audioTrack, ByteBuffer byteBuffer, int sizeInBytes) { 357 | return audioTrack.write(byteBuffer, sizeInBytes, AudioTrack.WRITE_BLOCKING); 358 | } 359 | 360 | private int writePreLollipop(AudioTrack audioTrack, ByteBuffer byteBuffer, int sizeInBytes) { 361 | return audioTrack.write(byteBuffer.array(), byteBuffer.arrayOffset(), sizeInBytes); 362 | } 363 | 364 | void stopAudioTrack() { 365 | keepAliveRendererRunnable = false; 366 | Log.d(TAG, "Remove any pending posts of speakerRendererRunnable that are in the message queue "); 367 | rendererHandler.removeCallbacks(speakerRendererRunnable); 368 | try { 369 | audioTrack.stop(); 370 | } catch (IllegalStateException e) { 371 | Log.e(TAG, "AudioTrack.stop failed: " + e.getMessage()); 372 | } 373 | releaseAudioResources(); 374 | } 375 | 376 | private void releaseAudioResources() { 377 | if (audioTrack != null) { 378 | audioTrack.flush(); 379 | audioTrack.release(); 380 | audioTrack = null; 381 | } 382 | } 383 | } 384 | -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/java/com/twilio/examplecustomaudiodevice/SoundPoolManager.java: -------------------------------------------------------------------------------- 1 | package com.twilio.examplecustomaudiodevice; 2 | 3 | import android.content.Context; 4 | import android.media.AudioManager; 5 | import android.media.SoundPool; 6 | import android.os.Build; 7 | 8 | import static android.content.Context.AUDIO_SERVICE; 9 | 10 | public class SoundPoolManager { 11 | 12 | private boolean playing = false; 13 | private boolean loaded = false; 14 | private boolean playingCalled = false; 15 | private float volume; 16 | private SoundPool soundPool; 17 | private int ringingSoundId; 18 | private int ringingStreamId; 19 | private int disconnectSoundId; 20 | private static SoundPoolManager instance; 21 | 22 | private SoundPoolManager(Context context) { 23 | // AudioManager audio settings for adjusting the volume 24 | AudioManager audioManager = (AudioManager) context.getSystemService(AUDIO_SERVICE); 25 | float actualVolume = (float) audioManager.getStreamVolume(AudioManager.STREAM_MUSIC); 26 | float maxVolume = (float) audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); 27 | volume = actualVolume / maxVolume; 28 | 29 | // Load the sounds 30 | int maxStreams = 1; 31 | soundPool = new SoundPool.Builder() 32 | .setMaxStreams(maxStreams) 33 | .build(); 34 | 35 | soundPool.setOnLoadCompleteListener((soundPool, sampleId, status) -> { 36 | loaded = true; 37 | if (playingCalled) { 38 | playRinging(); 39 | playingCalled = false; 40 | } 41 | }); 42 | ringingSoundId = soundPool.load(context, R.raw.incoming, 1); 43 | disconnectSoundId = soundPool.load(context, R.raw.disconnect, 1); 44 | } 45 | 46 | public static SoundPoolManager getInstance(Context context) { 47 | if (instance == null) { 48 | instance = new SoundPoolManager(context); 49 | } 50 | return instance; 51 | } 52 | 53 | public void playRinging() { 54 | if (loaded && !playing) { 55 | ringingStreamId = soundPool.play(ringingSoundId, volume, volume, 1, -1, 1f); 56 | playing = true; 57 | } else { 58 | playingCalled = true; 59 | } 60 | } 61 | 62 | public void stopRinging() { 63 | if (playing) { 64 | soundPool.stop(ringingStreamId); 65 | playing = false; 66 | } 67 | } 68 | 69 | public void playDisconnect() { 70 | if (loaded && !playing) { 71 | soundPool.play(disconnectSoundId, volume, volume, 1, 0, 1f); 72 | playing = false; 73 | } 74 | } 75 | 76 | public void release() { 77 | if (soundPool != null) { 78 | soundPool.unload(ringingSoundId); 79 | soundPool.unload(disconnectSoundId); 80 | soundPool.release(); 81 | soundPool = null; 82 | } 83 | instance = null; 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/drawable/ic_call_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/drawable/ic_call_end_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/drawable/ic_call_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 8 | 15 | 21 | 27 | 33 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/drawable/ic_mic_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 15 | -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/drawable/ic_mic_white_off_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 10 | 17 | -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/drawable/ic_music_white.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/drawable/ic_pause_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/drawable/ic_phonelink_ring_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/drawable/ic_volume_up_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/layout/activity_custom_audio_device.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 20 | 21 | 31 | 32 | 38 | 39 | 49 | 50 | 60 | 61 | 71 | 72 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/layout/dialog_call.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 14 | 15 | 23 | 24 | -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/menu/menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/exampleCustomAudioDevice/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/exampleCustomAudioDevice/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/exampleCustomAudioDevice/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/exampleCustomAudioDevice/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/exampleCustomAudioDevice/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/exampleCustomAudioDevice/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/exampleCustomAudioDevice/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/exampleCustomAudioDevice/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/exampleCustomAudioDevice/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/exampleCustomAudioDevice/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/raw/disconnect.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/exampleCustomAudioDevice/src/main/res/raw/disconnect.wav -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/raw/incoming.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/exampleCustomAudioDevice/src/main/res/raw/incoming.wav -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/raw/music.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/exampleCustomAudioDevice/src/main/res/raw/music.wav -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/raw/outgoing.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/exampleCustomAudioDevice/src/main/res/raw/outgoing.wav -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #f10028 4 | #a3090e 5 | #b0bec5 6 | 7 | -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Voice Quickstart 3 | Speaker ON 4 | client identity or phone number 5 | Answer 6 | Decline 7 | Dial a client name or phone number. Leaving the field empty results in an automated response. 8 | 9 | -------------------------------------------------------------------------------- /exampleCustomAudioDevice/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 16 | 17 | -------------------------------------------------------------------------------- /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 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | #org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | org.gradle.jvmargs=-Xmx2048m 15 | 16 | # When configured, Gradle will run in incubating parallel mode. 17 | # This option should only be used with decoupled projects. More details, visit 18 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 19 | # org.gradle.parallel=true 20 | android.useAndroidX=true 21 | android.enableJetifier=true 22 | android.nonTransitiveRClass=false 23 | android.nonFinalResIds=false 24 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Feb 22 17:01:57 EET 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 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 | -------------------------------------------------------------------------------- /images/exampleCustomAudioDevice/audio-device-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/images/exampleCustomAudioDevice/audio-device-example.png -------------------------------------------------------------------------------- /images/exampleCustomAudioDevice/audio_device_microphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/images/exampleCustomAudioDevice/audio_device_microphone.png -------------------------------------------------------------------------------- /images/exampleCustomAudioDevice/audio_device_music_file_plays.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/images/exampleCustomAudioDevice/audio_device_music_file_plays.png -------------------------------------------------------------------------------- /images/exampleCustomAudioDevice/make_call_custom_audio_device.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/images/exampleCustomAudioDevice/make_call_custom_audio_device.png -------------------------------------------------------------------------------- /images/quickstart/account-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/images/quickstart/account-menu.png -------------------------------------------------------------------------------- /images/quickstart/credentials-sid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/images/quickstart/credentials-sid.png -------------------------------------------------------------------------------- /images/quickstart/credentials-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/images/quickstart/credentials-tab.png -------------------------------------------------------------------------------- /images/quickstart/firebase-fcm-token-creation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/images/quickstart/firebase-fcm-token-creation.png -------------------------------------------------------------------------------- /images/quickstart/import_project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/images/quickstart/import_project.png -------------------------------------------------------------------------------- /images/quickstart/incoming_call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/images/quickstart/incoming_call.png -------------------------------------------------------------------------------- /images/quickstart/incoming_call_from_alice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/images/quickstart/incoming_call_from_alice.png -------------------------------------------------------------------------------- /images/quickstart/invalid_google_service_json_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/images/quickstart/invalid_google_service_json_error.png -------------------------------------------------------------------------------- /images/quickstart/make_call_to_client.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/images/quickstart/make_call_to_client.png -------------------------------------------------------------------------------- /images/quickstart/make_call_to_number.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/images/quickstart/make_call_to_number.png -------------------------------------------------------------------------------- /images/quickstart/twilio_cli_key_chain_access.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/images/quickstart/twilio_cli_key_chain_access.png -------------------------------------------------------------------------------- /images/quickstart/voice_activity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/images/quickstart/voice_activity.png -------------------------------------------------------------------------------- /images/quickstart/voice_make_call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/images/quickstart/voice_make_call.png -------------------------------------------------------------------------------- /images/quickstart/voice_make_call_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/voice-quickstart-android/7b21f077e5ea2b5e2619c759960278f9922b4397/images/quickstart/voice_make_call_dialog.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':exampleCustomAudioDevice' --------------------------------------------------------------------------------