Implemented Android Demo Application for VoIP API

The app showcased the ability to send real-time voice data between two endpoints using the VoIP API.
Users can also configure session parameters such as the endpoint information and codec used.

Bug: webrtc:11723
Change-Id: I682f4aa743b707759536bce59e598789a77b7ec6
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/178467
Reviewed-by: Kári Helgason <kthelgason@webrtc.org>
Reviewed-by: Sami Kalliomäki <sakal@webrtc.org>
Reviewed-by: Tim Na <natim@webrtc.org>
Commit-Queue: Tim Na <natim@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#31775}
diff --git a/examples/BUILD.gn b/examples/BUILD.gn
index 805a59b..f0c5fa8 100644
--- a/examples/BUILD.gn
+++ b/examples/BUILD.gn
@@ -27,6 +27,7 @@
       ":AppRTCMobile",
       ":AppRTCMobile_test_apk",
       ":libwebrtc_unity",
+      "androidvoip",
     ]
 
     # TODO(sakal): We include some code from the tests. Remove this dependency
diff --git a/examples/androidvoip/AndroidManifest.xml b/examples/androidvoip/AndroidManifest.xml
new file mode 100644
index 0000000..106f711
--- /dev/null
+++ b/examples/androidvoip/AndroidManifest.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ *  Copyright 2020 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+  package="org.webrtc.examples.androidvoip">
+
+  <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="27" />
+
+  <uses-permission android:name="android.permission.INTERNET" />
+  <uses-permission android:name="android.permission.RECORD_AUDIO" />
+  <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
+
+  <uses-feature android:name="android.hardware.microphone" android:required="true" />
+  <uses-feature android:name="android.hardware.telephony" android:required="false" />
+
+  <application
+    android:allowBackup="true"
+    android:label="@string/app_name"
+    android:supportsRtl="true">
+    <activity android:name=".MainActivity"
+              android:windowSoftInputMode="stateHidden">
+      <intent-filter>
+        <action android:name="android.intent.action.MAIN" />
+        <category android:name="android.intent.category.LAUNCHER" />
+      </intent-filter>
+    </activity>
+  </application>
+
+</manifest>
diff --git a/examples/androidvoip/BUILD.gn b/examples/androidvoip/BUILD.gn
new file mode 100644
index 0000000..74341a7
--- /dev/null
+++ b/examples/androidvoip/BUILD.gn
@@ -0,0 +1,88 @@
+# Copyright (c) 2020 The WebRTC project authors. All Rights Reserved.
+#
+# Use of this source code is governed by a BSD-style license
+# that can be found in the LICENSE file in the root of the source
+# tree. An additional intellectual property rights grant can be found
+# in the file PATENTS.  All contributing project authors may
+# be found in the AUTHORS file in the root of the source tree.
+
+import("//webrtc.gni")
+
+if (is_android) {
+  rtc_android_apk("androidvoip") {
+    testonly = true
+    apk_name = "androidvoip"
+    android_manifest = "AndroidManifest.xml"
+    min_sdk_version = 21
+    target_sdk_version = 27
+
+    sources = [
+      "java/org/webrtc/examples/androidvoip/MainActivity.java",
+      "java/org/webrtc/examples/androidvoip/OnVoipClientTaskCompleted.java",
+      "java/org/webrtc/examples/androidvoip/VoipClient.java",
+    ]
+
+    deps = [
+      ":resources",
+      "//modules/audio_device:audio_device_java",
+      "//rtc_base:base_java",
+      "//sdk/android:java_audio_device_module_java",
+      "//sdk/android:video_java",
+      "//third_party/android_deps:androidx_core_core_java",
+      "//third_party/android_deps:androidx_legacy_legacy_support_v4_java",
+    ]
+
+    shared_libraries = [ ":examples_androidvoip_jni" ]
+  }
+
+  generate_jni("generated_jni") {
+    testonly = true
+    sources = [ "java/org/webrtc/examples/androidvoip/VoipClient.java" ]
+    namespace = "webrtc_examples"
+    jni_generator_include = "//sdk/android/src/jni/jni_generator_helper.h"
+  }
+
+  rtc_shared_library("examples_androidvoip_jni") {
+    testonly = true
+    sources = [
+      "jni/android_voip_client.cc",
+      "jni/android_voip_client.h",
+      "jni/onload.cc",
+    ]
+
+    suppressed_configs += [ "//build/config/android:hide_all_but_jni_onload" ]
+    configs += [ "//build/config/android:hide_all_but_jni" ]
+
+    deps = [
+      ":generated_jni",
+      "//api:transport_api",
+      "//api/audio_codecs:audio_codecs_api",
+      "//api/audio_codecs:builtin_audio_decoder_factory",
+      "//api/audio_codecs:builtin_audio_encoder_factory",
+      "//api/task_queue:default_task_queue_factory",
+      "//api/voip:voip_api",
+      "//api/voip:voip_engine_factory",
+      "//modules/utility:utility",
+      "//rtc_base",
+      "//rtc_base/third_party/sigslot:sigslot",
+      "//sdk/android:native_api_audio_device_module",
+      "//sdk/android:native_api_base",
+      "//sdk/android:native_api_jni",
+      "//third_party/abseil-cpp/absl/memory:memory",
+    ]
+  }
+
+  android_resources("resources") {
+    testonly = true
+    custom_package = "org.webrtc.examples.androidvoip"
+    sources = [
+      "res/layout/activity_main.xml",
+      "res/values/colors.xml",
+      "res/values/strings.xml",
+    ]
+
+    # Needed for Bazel converter.
+    resource_dirs = [ "res" ]
+    assert(resource_dirs != [])  # Mark as used.
+  }
+}
diff --git a/examples/androidvoip/DEPS b/examples/androidvoip/DEPS
new file mode 100644
index 0000000..edb714d
--- /dev/null
+++ b/examples/androidvoip/DEPS
@@ -0,0 +1,3 @@
+include_rules = [
+  "+sdk/android/native_api",
+]
diff --git a/examples/androidvoip/OWNERS b/examples/androidvoip/OWNERS
new file mode 100644
index 0000000..0fe5182
--- /dev/null
+++ b/examples/androidvoip/OWNERS
@@ -0,0 +1,2 @@
+natim@webrtc.org
+sakal@webrtc.org
diff --git a/examples/androidvoip/java/org/webrtc/examples/androidvoip/MainActivity.java b/examples/androidvoip/java/org/webrtc/examples/androidvoip/MainActivity.java
new file mode 100644
index 0000000..d787de5
--- /dev/null
+++ b/examples/androidvoip/java/org/webrtc/examples/androidvoip/MainActivity.java
@@ -0,0 +1,339 @@
+/*
+ *  Copyright (c) 2020 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+package org.webrtc.examples.androidvoip;
+
+import android.Manifest.permission;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.RelativeLayout;
+import android.widget.ScrollView;
+import android.widget.Spinner;
+import android.widget.Switch;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.ToggleButton;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.webrtc.ContextUtils;
+
+public class MainActivity extends Activity implements OnVoipClientTaskCompleted {
+  private static final int NUM_SUPPORTED_CODECS = 6;
+
+  private VoipClient voipClient;
+  private List<String> supportedCodecs;
+  private boolean[] isDecoderSelected;
+  private Set<Integer> selectedDecoders;
+
+  private Toast toast;
+  private ScrollView scrollView;
+  private TextView localIPAddressTextView;
+  private EditText localPortNumberEditText;
+  private EditText remoteIPAddressEditText;
+  private EditText remotePortNumberEditText;
+  private Spinner encoderSpinner;
+  private Button decoderSelectionButton;
+  private TextView decodersTextView;
+  private ToggleButton sessionButton;
+  private RelativeLayout switchLayout;
+  private Switch sendSwitch;
+  private Switch playoutSwitch;
+
+  @Override
+  protected void onCreate(Bundle savedInstance) {
+    ContextUtils.initialize(getApplicationContext());
+
+    super.onCreate(savedInstance);
+    setContentView(R.layout.activity_main);
+
+    System.loadLibrary("examples_androidvoip_jni");
+
+    voipClient = new VoipClient(getApplicationContext(), this);
+    voipClient.getAndSetUpLocalIPAddress();
+    voipClient.getAndSetUpSupportedCodecs();
+
+    isDecoderSelected = new boolean[NUM_SUPPORTED_CODECS];
+    selectedDecoders = new HashSet<>();
+
+    toast = Toast.makeText(this, "", Toast.LENGTH_SHORT);
+
+    scrollView = (ScrollView) findViewById(R.id.scroll_view);
+    localIPAddressTextView = (TextView) findViewById(R.id.local_ip_address_text_view);
+    localPortNumberEditText = (EditText) findViewById(R.id.local_port_number_edit_text);
+    remoteIPAddressEditText = (EditText) findViewById(R.id.remote_ip_address_edit_text);
+    remotePortNumberEditText = (EditText) findViewById(R.id.remote_port_number_edit_text);
+    encoderSpinner = (Spinner) findViewById(R.id.encoder_spinner);
+    decoderSelectionButton = (Button) findViewById(R.id.decoder_selection_button);
+    decodersTextView = (TextView) findViewById(R.id.decoders_text_view);
+    sessionButton = (ToggleButton) findViewById(R.id.session_button);
+    switchLayout = (RelativeLayout) findViewById(R.id.switch_layout);
+    sendSwitch = (Switch) findViewById(R.id.start_send_switch);
+    playoutSwitch = (Switch) findViewById(R.id.start_playout_switch);
+
+    setUpSessionButton();
+    setUpSendAndPlayoutSwitch();
+  }
+
+  private void setUpEncoderSpinner(List<String> supportedCodecs) {
+    ArrayAdapter<String> encoderAdapter =
+        new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, supportedCodecs);
+    encoderAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+    encoderSpinner.setAdapter(encoderAdapter);
+    encoderSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+      @Override
+      public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+        voipClient.setEncoder((String) parent.getSelectedItem());
+      }
+      @Override
+      public void onNothingSelected(AdapterView<?> parent) {}
+    });
+  }
+
+  private List<String> getSelectedDecoders() {
+    List<String> decoders = new ArrayList<>();
+    for (int i = 0; i < supportedCodecs.size(); i++) {
+      if (selectedDecoders.contains(i)) {
+        decoders.add(supportedCodecs.get(i));
+      }
+    }
+    return decoders;
+  }
+
+  private void setUpDecoderSelectionButton(List<String> supportedCodecs) {
+    decoderSelectionButton.setOnClickListener((view) -> {
+      AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
+      dialogBuilder.setTitle(R.string.dialog_title);
+
+      // Populate multi choice items with supported decoders.
+      String[] supportedCodecsArray = supportedCodecs.toArray(new String[0]);
+      dialogBuilder.setMultiChoiceItems(
+          supportedCodecsArray, isDecoderSelected, (dialog, position, isChecked) -> {
+            if (isChecked) {
+              selectedDecoders.add(position);
+            } else if (!isChecked) {
+              selectedDecoders.remove(position);
+            }
+          });
+
+      // "Ok" button.
+      dialogBuilder.setPositiveButton(R.string.ok_label, (dialog, position) -> {
+        List<String> decoders = getSelectedDecoders();
+        String result = decoders.stream().collect(Collectors.joining(", "));
+        if (result.isEmpty()) {
+          decodersTextView.setText(R.string.decoders_text_view_default);
+        } else {
+          decodersTextView.setText(result);
+        }
+        voipClient.setDecoders(decoders);
+      });
+
+      // "Dismiss" button.
+      dialogBuilder.setNegativeButton(
+          R.string.dismiss_label, (dialog, position) -> { dialog.dismiss(); });
+
+      // "Clear All" button.
+      dialogBuilder.setNeutralButton(R.string.clear_all_label, (dialog, position) -> {
+        Arrays.fill(isDecoderSelected, false);
+        selectedDecoders.clear();
+        decodersTextView.setText(R.string.decoders_text_view_default);
+      });
+
+      AlertDialog dialog = dialogBuilder.create();
+      dialog.show();
+    });
+  }
+
+  private void setUpSessionButton() {
+    sessionButton.setOnCheckedChangeListener((button, isChecked) -> {
+      // Ask for permission on RECORD_AUDIO if not granted.
+      if (ContextCompat.checkSelfPermission(this, permission.RECORD_AUDIO)
+          != PackageManager.PERMISSION_GRANTED) {
+        String[] sList = {permission.RECORD_AUDIO};
+        ActivityCompat.requestPermissions(this, sList, 1);
+      }
+
+      if (isChecked) {
+        // Order matters here, addresses have to be set before starting session
+        // before setting codec.
+        voipClient.setLocalAddress(localIPAddressTextView.getText().toString(),
+            Integer.parseInt(localPortNumberEditText.getText().toString()));
+        voipClient.setRemoteAddress(remoteIPAddressEditText.getText().toString(),
+            Integer.parseInt(remotePortNumberEditText.getText().toString()));
+        voipClient.startSession();
+        voipClient.setEncoder((String) encoderSpinner.getSelectedItem());
+        voipClient.setDecoders(getSelectedDecoders());
+      } else {
+        voipClient.stopSession();
+      }
+    });
+  }
+
+  private void setUpSendAndPlayoutSwitch() {
+    sendSwitch.setOnCheckedChangeListener((button, isChecked) -> {
+      if (isChecked) {
+        voipClient.startSend();
+      } else {
+        voipClient.stopSend();
+      }
+    });
+
+    playoutSwitch.setOnCheckedChangeListener((button, isChecked) -> {
+      if (isChecked) {
+        voipClient.startPlayout();
+      } else {
+        voipClient.stopPlayout();
+      }
+    });
+  }
+
+  private void setUpIPAddressEditTexts(String localIPAddress) {
+    if (localIPAddress.isEmpty()) {
+      showToast("Please check your network configuration");
+    } else {
+      localIPAddressTextView.setText(localIPAddress);
+      // By default remote IP address is the same as local IP address.
+      remoteIPAddressEditText.setText(localIPAddress);
+    }
+  }
+
+  private void showToast(String message) {
+    toast.cancel();
+    toast = Toast.makeText(this, message, Toast.LENGTH_SHORT);
+    toast.setGravity(Gravity.TOP, 0, 200);
+    toast.show();
+  }
+
+  @Override
+  protected void onDestroy() {
+    voipClient.close();
+    voipClient = null;
+
+    super.onDestroy();
+  }
+
+  @Override
+  public void onGetLocalIPAddressCompleted(String localIPAddress) {
+    runOnUiThread(() -> { setUpIPAddressEditTexts(localIPAddress); });
+  }
+
+  @Override
+  public void onGetSupportedCodecsCompleted(List<String> supportedCodecs) {
+    runOnUiThread(() -> {
+      this.supportedCodecs = supportedCodecs;
+      setUpEncoderSpinner(supportedCodecs);
+      setUpDecoderSelectionButton(supportedCodecs);
+    });
+  }
+
+  @Override
+  public void onVoipClientInitializationCompleted(boolean isSuccessful) {
+    runOnUiThread(() -> {
+      if (!isSuccessful) {
+        showToast("Error initializing audio device");
+      }
+    });
+  }
+
+  @Override
+  public void onStartSessionCompleted(boolean isSuccessful) {
+    runOnUiThread(() -> {
+      if (isSuccessful) {
+        showToast("Session started");
+        switchLayout.setVisibility(View.VISIBLE);
+        scrollView.post(() -> { scrollView.fullScroll(ScrollView.FOCUS_DOWN); });
+      } else {
+        showToast("Failed to start session");
+      }
+    });
+  }
+
+  @Override
+  public void onStopSessionCompleted(boolean isSuccessful) {
+    runOnUiThread(() -> {
+      if (isSuccessful) {
+        showToast("Session stopped");
+        // Set listeners to null so the checked state can be changed programmatically.
+        sendSwitch.setOnCheckedChangeListener(null);
+        playoutSwitch.setOnCheckedChangeListener(null);
+        sendSwitch.setChecked(false);
+        playoutSwitch.setChecked(false);
+        // Redo the switch listener setup.
+        setUpSendAndPlayoutSwitch();
+        switchLayout.setVisibility(View.GONE);
+      } else {
+        showToast("Failed to stop session");
+      }
+    });
+  }
+
+  @Override
+  public void onStartSendCompleted(boolean isSuccessful) {
+    runOnUiThread(() -> {
+      if (isSuccessful) {
+        showToast("Started sending");
+      } else {
+        showToast("Error initializing microphone");
+      }
+    });
+  }
+
+  @Override
+  public void onStopSendCompleted(boolean isSuccessful) {
+    runOnUiThread(() -> {
+      if (isSuccessful) {
+        showToast("Stopped sending");
+      } else {
+        showToast("Microphone termination failed");
+      }
+    });
+  }
+
+  @Override
+  public void onStartPlayoutCompleted(boolean isSuccessful) {
+    runOnUiThread(() -> {
+      if (isSuccessful) {
+        showToast("Started playout");
+      } else {
+        showToast("Error initializing speaker");
+      }
+    });
+  }
+
+  @Override
+  public void onStopPlayoutCompleted(boolean isSuccessful) {
+    runOnUiThread(() -> {
+      if (isSuccessful) {
+        showToast("Stopped playout");
+      } else {
+        showToast("Speaker termination failed");
+      }
+    });
+  }
+
+  @Override
+  public void onUninitializedVoipClient() {
+    runOnUiThread(() -> { showToast("Voip client is uninitialized"); });
+  }
+}
diff --git a/examples/androidvoip/java/org/webrtc/examples/androidvoip/OnVoipClientTaskCompleted.java b/examples/androidvoip/java/org/webrtc/examples/androidvoip/OnVoipClientTaskCompleted.java
new file mode 100644
index 0000000..bb85e04
--- /dev/null
+++ b/examples/androidvoip/java/org/webrtc/examples/androidvoip/OnVoipClientTaskCompleted.java
@@ -0,0 +1,26 @@
+/*
+ *  Copyright (c) 2020 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+package org.webrtc.examples.androidvoip;
+
+import java.util.List;
+
+public interface OnVoipClientTaskCompleted {
+  void onGetLocalIPAddressCompleted(String localIPAddress);
+  void onGetSupportedCodecsCompleted(List<String> supportedCodecs);
+  void onVoipClientInitializationCompleted(boolean isSuccessful);
+  void onStartSessionCompleted(boolean isSuccessful);
+  void onStopSessionCompleted(boolean isSuccessful);
+  void onStartSendCompleted(boolean isSuccessful);
+  void onStopSendCompleted(boolean isSuccessful);
+  void onStartPlayoutCompleted(boolean isSuccessful);
+  void onStopPlayoutCompleted(boolean isSuccessful);
+  void onUninitializedVoipClient();
+}
diff --git a/examples/androidvoip/java/org/webrtc/examples/androidvoip/VoipClient.java b/examples/androidvoip/java/org/webrtc/examples/androidvoip/VoipClient.java
new file mode 100644
index 0000000..2dcbd99
--- /dev/null
+++ b/examples/androidvoip/java/org/webrtc/examples/androidvoip/VoipClient.java
@@ -0,0 +1,188 @@
+/*
+ *  Copyright (c) 2020 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+package org.webrtc.examples.androidvoip;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import java.util.ArrayList;
+import java.util.List;
+
+public class VoipClient {
+  private static final String TAG = "VoipClient";
+
+  private final HandlerThread thread;
+  private final Handler handler;
+
+  private long nativeClient;
+  private OnVoipClientTaskCompleted listener;
+
+  public VoipClient(Context applicationContext, OnVoipClientTaskCompleted listener) {
+    this.listener = listener;
+    thread = new HandlerThread(TAG + "Thread");
+    thread.start();
+    handler = new Handler(thread.getLooper());
+
+    handler.post(() -> {
+      nativeClient = nativeCreateClient(applicationContext);
+      listener.onVoipClientInitializationCompleted(/* isSuccessful */ nativeClient != 0);
+    });
+  }
+
+  private boolean isInitialized() {
+    return nativeClient != 0;
+  }
+
+  public void getAndSetUpSupportedCodecs() {
+    handler.post(() -> {
+      if (isInitialized()) {
+        listener.onGetSupportedCodecsCompleted(nativeGetSupportedCodecs(nativeClient));
+      } else {
+        listener.onUninitializedVoipClient();
+      }
+    });
+  }
+
+  public void getAndSetUpLocalIPAddress() {
+    handler.post(() -> {
+      if (isInitialized()) {
+        listener.onGetLocalIPAddressCompleted(nativeGetLocalIPAddress(nativeClient));
+      } else {
+        listener.onUninitializedVoipClient();
+      }
+    });
+  }
+
+  public void setEncoder(String encoder) {
+    handler.post(() -> {
+      if (isInitialized()) {
+        nativeSetEncoder(nativeClient, encoder);
+      } else {
+        listener.onUninitializedVoipClient();
+      }
+    });
+  }
+
+  public void setDecoders(List<String> decoders) {
+    handler.post(() -> {
+      if (isInitialized()) {
+        nativeSetDecoders(nativeClient, decoders);
+      } else {
+        listener.onUninitializedVoipClient();
+      }
+    });
+  }
+
+  public void setLocalAddress(String ipAddress, int portNumber) {
+    handler.post(() -> {
+      if (isInitialized()) {
+        nativeSetLocalAddress(nativeClient, ipAddress, portNumber);
+      } else {
+        listener.onUninitializedVoipClient();
+      }
+    });
+  }
+
+  public void setRemoteAddress(String ipAddress, int portNumber) {
+    handler.post(() -> {
+      if (isInitialized()) {
+        nativeSetRemoteAddress(nativeClient, ipAddress, portNumber);
+      } else {
+        listener.onUninitializedVoipClient();
+      }
+    });
+  }
+
+  public void startSession() {
+    handler.post(() -> {
+      if (isInitialized()) {
+        listener.onStartSessionCompleted(nativeStartSession(nativeClient));
+      } else {
+        listener.onUninitializedVoipClient();
+      }
+    });
+  }
+
+  public void stopSession() {
+    handler.post(() -> {
+      if (isInitialized()) {
+        listener.onStopSessionCompleted(nativeStopSession(nativeClient));
+      } else {
+        listener.onUninitializedVoipClient();
+      }
+    });
+  }
+
+  public void startSend() {
+    handler.post(() -> {
+      if (isInitialized()) {
+        listener.onStartSendCompleted(nativeStartSend(nativeClient));
+      } else {
+        listener.onUninitializedVoipClient();
+      }
+    });
+  }
+
+  public void stopSend() {
+    handler.post(() -> {
+      if (isInitialized()) {
+        listener.onStopSendCompleted(nativeStopSend(nativeClient));
+      } else {
+        listener.onUninitializedVoipClient();
+      }
+    });
+  }
+
+  public void startPlayout() {
+    handler.post(() -> {
+      if (isInitialized()) {
+        listener.onStartPlayoutCompleted(nativeStartPlayout(nativeClient));
+      } else {
+        listener.onUninitializedVoipClient();
+      }
+    });
+  }
+
+  public void stopPlayout() {
+    handler.post(() -> {
+      if (isInitialized()) {
+        listener.onStopPlayoutCompleted(nativeStopPlayout(nativeClient));
+      } else {
+        listener.onUninitializedVoipClient();
+      }
+    });
+  }
+
+  public void close() {
+    handler.post(() -> {
+      nativeDelete(nativeClient);
+      nativeClient = 0;
+    });
+    thread.quitSafely();
+  }
+
+  private static native long nativeCreateClient(Context applicationContext);
+  private static native List<String> nativeGetSupportedCodecs(long nativeAndroidVoipClient);
+  private static native String nativeGetLocalIPAddress(long nativeAndroidVoipClient);
+  private static native void nativeSetEncoder(long nativeAndroidVoipClient, String encoder);
+  private static native void nativeSetDecoders(long nativeAndroidVoipClient, List<String> decoders);
+  private static native void nativeSetLocalAddress(
+      long nativeAndroidVoipClient, String ipAddress, int portNumber);
+  private static native void nativeSetRemoteAddress(
+      long nativeAndroidVoipClient, String ipAddress, int portNumber);
+  private static native boolean nativeStartSession(long nativeAndroidVoipClient);
+  private static native boolean nativeStopSession(long nativeAndroidVoipClient);
+  private static native boolean nativeStartSend(long nativeAndroidVoipClient);
+  private static native boolean nativeStopSend(long nativeAndroidVoipClient);
+  private static native boolean nativeStartPlayout(long nativeAndroidVoipClient);
+  private static native boolean nativeStopPlayout(long nativeAndroidVoipClient);
+  private static native void nativeDelete(long nativeAndroidVoipClient);
+}
diff --git a/examples/androidvoip/jni/android_voip_client.cc b/examples/androidvoip/jni/android_voip_client.cc
new file mode 100644
index 0000000..13cadf2
--- /dev/null
+++ b/examples/androidvoip/jni/android_voip_client.cc
@@ -0,0 +1,405 @@
+/*
+ *  Copyright 2020 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#include "examples/androidvoip/jni/android_voip_client.h"
+
+#include <errno.h>
+#include <sys/socket.h>
+#include <algorithm>
+#include <map>
+#include <memory>
+#include <unordered_map>
+#include <unordered_set>
+#include <utility>
+#include <vector>
+
+#include "absl/memory/memory.h"
+#include "api/audio_codecs/builtin_audio_decoder_factory.h"
+#include "api/audio_codecs/builtin_audio_encoder_factory.h"
+#include "api/task_queue/default_task_queue_factory.h"
+#include "api/voip/voip_codec.h"
+#include "api/voip/voip_engine_factory.h"
+#include "api/voip/voip_network.h"
+#include "examples/androidvoip/generated_jni/VoipClient_jni.h"
+#include "rtc_base/logging.h"
+#include "rtc_base/network.h"
+#include "rtc_base/socket_server.h"
+#include "sdk/android/native_api/audio_device_module/audio_device_android.h"
+#include "sdk/android/native_api/jni/java_types.h"
+
+namespace {
+
+// Connects a UDP socket to a public address and returns the local
+// address associated with it. Since it binds to the "any" address
+// internally, it returns the default local address on a multi-homed
+// endpoint. Implementation copied from
+// BasicNetworkManager::QueryDefaultLocalAddress.
+rtc::IPAddress QueryDefaultLocalAddress(int family) {
+  const char kPublicIPv4Host[] = "8.8.8.8";
+  const char kPublicIPv6Host[] = "2001:4860:4860::8888";
+  const int kPublicPort = 53;
+  std::unique_ptr<rtc::Thread> thread = rtc::Thread::CreateWithSocketServer();
+
+  RTC_DCHECK(thread->socketserver() != nullptr);
+  RTC_DCHECK(family == AF_INET || family == AF_INET6);
+
+  std::unique_ptr<rtc::AsyncSocket> socket(
+      thread->socketserver()->CreateAsyncSocket(family, SOCK_DGRAM));
+  if (!socket) {
+    RTC_LOG_ERR(LERROR) << "Socket creation failed";
+    return rtc::IPAddress();
+  }
+
+  auto host = family == AF_INET ? kPublicIPv4Host : kPublicIPv6Host;
+  if (socket->Connect(rtc::SocketAddress(host, kPublicPort)) < 0) {
+    if (socket->GetError() != ENETUNREACH &&
+        socket->GetError() != EHOSTUNREACH) {
+      RTC_LOG(LS_INFO) << "Connect failed with " << socket->GetError();
+    }
+    return rtc::IPAddress();
+  }
+  return socket->GetLocalAddress().ipaddr();
+}
+
+// Assigned payload type for supported built-in codecs. PCMU, PCMA,
+// and G722 have set payload types. Whereas opus, ISAC, and ILBC
+// have dynamic payload types.
+enum class PayloadType : int {
+  kPcmu = 0,
+  kPcma = 8,
+  kG722 = 9,
+  kOpus = 96,
+  kIsac = 97,
+  kIlbc = 98,
+};
+
+// Returns the payload type corresponding to codec_name. Only
+// supports the built-in codecs.
+int GetPayloadType(const std::string& codec_name) {
+  RTC_DCHECK(codec_name == "PCMU" || codec_name == "PCMA" ||
+             codec_name == "G722" || codec_name == "opus" ||
+             codec_name == "ISAC" || codec_name == "ILBC");
+
+  if (codec_name == "PCMU") {
+    return static_cast<int>(PayloadType::kPcmu);
+  } else if (codec_name == "PCMA") {
+    return static_cast<int>(PayloadType::kPcma);
+  } else if (codec_name == "G722") {
+    return static_cast<int>(PayloadType::kG722);
+  } else if (codec_name == "opus") {
+    return static_cast<int>(PayloadType::kOpus);
+  } else if (codec_name == "ISAC") {
+    return static_cast<int>(PayloadType::kIsac);
+  } else if (codec_name == "ILBC") {
+    return static_cast<int>(PayloadType::kIlbc);
+  }
+
+  RTC_NOTREACHED();
+  return -1;
+}
+
+}  // namespace
+
+namespace webrtc_examples {
+
+AndroidVoipClient::AndroidVoipClient(
+    JNIEnv* env,
+    const webrtc::JavaParamRef<jobject>& application_context) {
+  voip_thread_ = rtc::Thread::CreateWithSocketServer();
+  voip_thread_->Start();
+
+  webrtc::VoipEngineConfig config;
+  config.encoder_factory = webrtc::CreateBuiltinAudioEncoderFactory();
+  config.decoder_factory = webrtc::CreateBuiltinAudioDecoderFactory();
+  config.task_queue_factory = webrtc::CreateDefaultTaskQueueFactory();
+  config.audio_device_module =
+      webrtc::CreateJavaAudioDeviceModule(env, application_context.obj());
+  config.audio_processing = webrtc::AudioProcessingBuilder().Create();
+
+  supported_codecs_ = config.encoder_factory->GetSupportedEncoders();
+
+  // Due to consistent thread requirement on
+  // modules/audio_device/android/audio_device_template.h,
+  // code is invoked in the context of voip_thread_.
+  voip_thread_->Invoke<void>(RTC_FROM_HERE, [&] {
+    voip_engine_ = webrtc::CreateVoipEngine(std::move(config));
+    if (!voip_engine_) {
+      RTC_LOG(LS_ERROR) << "VoipEngine creation failed";
+    }
+  });
+}
+
+AndroidVoipClient::~AndroidVoipClient() {
+  voip_thread_->Stop();
+}
+
+AndroidVoipClient* AndroidVoipClient::Create(
+    JNIEnv* env,
+    const webrtc::JavaParamRef<jobject>& application_context) {
+  // Using `new` to access a non-public constructor.
+  auto voip_client =
+      absl::WrapUnique(new AndroidVoipClient(env, application_context));
+  if (!voip_client->voip_engine_) {
+    return nullptr;
+  }
+  return voip_client.release();
+}
+
+webrtc::ScopedJavaLocalRef<jobject> AndroidVoipClient::GetSupportedCodecs(
+    JNIEnv* env) {
+  std::vector<std::string> names;
+  for (const webrtc::AudioCodecSpec& spec : supported_codecs_) {
+    names.push_back(spec.format.name);
+  }
+  webrtc::ScopedJavaLocalRef<jstring> (*convert_function)(
+      JNIEnv*, const std::string&) = &webrtc::NativeToJavaString;
+  return NativeToJavaList(env, names, convert_function);
+}
+
+webrtc::ScopedJavaLocalRef<jstring> AndroidVoipClient::GetLocalIPAddress(
+    JNIEnv* env) {
+  rtc::IPAddress ipv4_address = QueryDefaultLocalAddress(AF_INET);
+  if (!ipv4_address.IsNil()) {
+    return webrtc::NativeToJavaString(env, ipv4_address.ToString());
+  }
+  rtc::IPAddress ipv6_address = QueryDefaultLocalAddress(AF_INET6);
+  if (!ipv6_address.IsNil()) {
+    return webrtc::NativeToJavaString(env, ipv6_address.ToString());
+  }
+  return webrtc::NativeToJavaString(env, "");
+}
+
+void AndroidVoipClient::SetEncoder(
+    JNIEnv* env,
+    const webrtc::JavaRef<jstring>& j_encoder_string) {
+  if (!channel_) {
+    RTC_LOG(LS_ERROR) << "Channel has not been created";
+    return;
+  }
+  const std::string& chosen_encoder =
+      webrtc::JavaToNativeString(env, j_encoder_string);
+  for (const webrtc::AudioCodecSpec& encoder : supported_codecs_) {
+    if (encoder.format.name == chosen_encoder) {
+      voip_engine_->Codec().SetSendCodec(
+          *channel_, GetPayloadType(encoder.format.name), encoder.format);
+      break;
+    }
+  }
+}
+
+void AndroidVoipClient::SetDecoders(
+    JNIEnv* env,
+    const webrtc::JavaParamRef<jobject>& j_decoder_strings) {
+  if (!channel_) {
+    RTC_LOG(LS_ERROR) << "Channel has not been created";
+    return;
+  }
+  std::vector<std::string> chosen_decoders =
+      webrtc::JavaListToNativeVector<std::string, jstring>(
+          env, j_decoder_strings, &webrtc::JavaToNativeString);
+  std::map<int, webrtc::SdpAudioFormat> decoder_specs;
+
+  for (const webrtc::AudioCodecSpec& decoder : supported_codecs_) {
+    if (std::find(chosen_decoders.begin(), chosen_decoders.end(),
+                  decoder.format.name) != chosen_decoders.end()) {
+      decoder_specs.insert(
+          {GetPayloadType(decoder.format.name), decoder.format});
+    }
+  }
+
+  voip_engine_->Codec().SetReceiveCodecs(*channel_, decoder_specs);
+}
+
+void AndroidVoipClient::SetLocalAddress(
+    JNIEnv* env,
+    const webrtc::JavaRef<jstring>& j_ip_address_string,
+    jint j_port_number_int) {
+  const std::string& ip_address =
+      webrtc::JavaToNativeString(env, j_ip_address_string);
+  rtp_local_address_ = rtc::SocketAddress(ip_address, j_port_number_int);
+  rtcp_local_address_ = rtc::SocketAddress(ip_address, j_port_number_int + 1);
+}
+
+void AndroidVoipClient::SetRemoteAddress(
+    JNIEnv* env,
+    const webrtc::JavaRef<jstring>& j_ip_address_string,
+    jint j_port_number_int) {
+  const std::string& ip_address =
+      webrtc::JavaToNativeString(env, j_ip_address_string);
+  rtp_remote_address_ = rtc::SocketAddress(ip_address, j_port_number_int);
+  rtcp_remote_address_ = rtc::SocketAddress(ip_address, j_port_number_int + 1);
+}
+
+jboolean AndroidVoipClient::StartSession(JNIEnv* env) {
+  // Due to consistent thread requirement on
+  // modules/utility/source/process_thread_impl.cc,
+  // code is invoked in the context of voip_thread_.
+  channel_ = voip_thread_->Invoke<absl::optional<webrtc::ChannelId>>(
+      RTC_FROM_HERE,
+      [this] { return voip_engine_->Base().CreateChannel(this, 0); });
+  if (!channel_) {
+    RTC_LOG(LS_ERROR) << "Channel creation failed";
+    return false;
+  }
+
+  rtp_socket_.reset(rtc::AsyncUDPSocket::Create(voip_thread_->socketserver(),
+                                                rtp_local_address_));
+  if (!rtp_socket_) {
+    RTC_LOG_ERR(LERROR) << "Socket creation failed";
+    return false;
+  }
+  rtp_socket_->SignalReadPacket.connect(
+      this, &AndroidVoipClient::OnSignalReadRTPPacket);
+
+  rtcp_socket_.reset(rtc::AsyncUDPSocket::Create(voip_thread_->socketserver(),
+                                                 rtcp_local_address_));
+  if (!rtcp_socket_) {
+    RTC_LOG_ERR(LERROR) << "Socket creation failed";
+    return false;
+  }
+  rtcp_socket_->SignalReadPacket.connect(
+      this, &AndroidVoipClient::OnSignalReadRTCPPacket);
+
+  return true;
+}
+
+jboolean AndroidVoipClient::StopSession(JNIEnv* env) {
+  if (!channel_) {
+    RTC_LOG(LS_ERROR) << "Channel has not been created";
+    return false;
+  }
+  if (!StopSend(env) || !StopPlayout(env)) {
+    return false;
+  }
+
+  rtp_socket_->Close();
+  rtcp_socket_->Close();
+  // Due to consistent thread requirement on
+  // modules/utility/source/process_thread_impl.cc,
+  // code is invoked in the context of voip_thread_.
+  voip_thread_->Invoke<void>(RTC_FROM_HERE, [this] {
+    voip_engine_->Base().ReleaseChannel(*channel_);
+  });
+  channel_ = absl::nullopt;
+  return true;
+}
+
+jboolean AndroidVoipClient::StartSend(JNIEnv* env) {
+  if (!channel_) {
+    RTC_LOG(LS_ERROR) << "Channel has not been created";
+    return false;
+  }
+  // Due to consistent thread requirement on
+  // modules/audio_device/android/opensles_recorder.cc,
+  // code is invoked in the context of voip_thread_.
+  return voip_thread_->Invoke<bool>(RTC_FROM_HERE, [this] {
+    return voip_engine_->Base().StartSend(*channel_);
+  });
+}
+
+jboolean AndroidVoipClient::StopSend(JNIEnv* env) {
+  if (!channel_) {
+    RTC_LOG(LS_ERROR) << "Channel has not been created";
+    return false;
+  }
+  // Due to consistent thread requirement on
+  // modules/audio_device/android/opensles_recorder.cc,
+  // code is invoked in the context of voip_thread_.
+  return voip_thread_->Invoke<bool>(RTC_FROM_HERE, [this] {
+    return voip_engine_->Base().StopSend(*channel_);
+  });
+}
+
+jboolean AndroidVoipClient::StartPlayout(JNIEnv* env) {
+  if (!channel_) {
+    RTC_LOG(LS_ERROR) << "Channel has not been created";
+    return false;
+  }
+  // Due to consistent thread requirement on
+  // modules/audio_device/android/opensles_player.cc,
+  // code is invoked in the context of voip_thread_.
+  return voip_thread_->Invoke<bool>(RTC_FROM_HERE, [this] {
+    return voip_engine_->Base().StartPlayout(*channel_);
+  });
+}
+
+jboolean AndroidVoipClient::StopPlayout(JNIEnv* env) {
+  if (!channel_) {
+    RTC_LOG(LS_ERROR) << "Channel has not been created";
+    return false;
+  }
+  // Due to consistent thread requirement on
+  // modules/audio_device/android/opensles_player.cc,
+  // code is invoked in the context of voip_thread_.
+  return voip_thread_->Invoke<bool>(RTC_FROM_HERE, [this] {
+    return voip_engine_->Base().StopPlayout(*channel_);
+  });
+}
+
+void AndroidVoipClient::Delete(JNIEnv* env) {
+  delete this;
+}
+
+bool AndroidVoipClient::SendRtp(const uint8_t* packet,
+                                size_t length,
+                                const webrtc::PacketOptions& options) {
+  if (!rtp_socket_->SendTo(packet, length, rtp_remote_address_,
+                           rtc::PacketOptions())) {
+    RTC_LOG(LS_ERROR) << "Failed to send RTP packet";
+    return false;
+  }
+  return true;
+}
+
+bool AndroidVoipClient::SendRtcp(const uint8_t* packet, size_t length) {
+  if (!rtcp_socket_->SendTo(packet, length, rtcp_remote_address_,
+                            rtc::PacketOptions())) {
+    RTC_LOG(LS_ERROR) << "Failed to send RTCP packet";
+    return false;
+  }
+  return true;
+}
+
+void AndroidVoipClient::OnSignalReadRTPPacket(rtc::AsyncPacketSocket* socket,
+                                              const char* rtp_packet,
+                                              size_t size,
+                                              const rtc::SocketAddress& addr,
+                                              const int64_t& timestamp) {
+  if (!channel_) {
+    RTC_LOG(LS_ERROR) << "Channel has not been created";
+    return;
+  }
+  voip_engine_->Network().ReceivedRTPPacket(
+      *channel_, rtc::ArrayView<const uint8_t>(
+                     reinterpret_cast<const uint8_t*>(rtp_packet), size));
+}
+
+void AndroidVoipClient::OnSignalReadRTCPPacket(rtc::AsyncPacketSocket* socket,
+                                               const char* rtcp_packet,
+                                               size_t size,
+                                               const rtc::SocketAddress& addr,
+                                               const int64_t& timestamp) {
+  if (!channel_) {
+    RTC_LOG(LS_ERROR) << "Channel has not been created";
+    return;
+  }
+  voip_engine_->Network().ReceivedRTCPPacket(
+      *channel_, rtc::ArrayView<const uint8_t>(
+                     reinterpret_cast<const uint8_t*>(rtcp_packet), size));
+}
+
+static jlong JNI_VoipClient_CreateClient(
+    JNIEnv* env,
+    const webrtc::JavaParamRef<jobject>& application_context) {
+  return webrtc::NativeToJavaPointer(
+      AndroidVoipClient::Create(env, application_context));
+}
+
+}  // namespace webrtc_examples
diff --git a/examples/androidvoip/jni/android_voip_client.h b/examples/androidvoip/jni/android_voip_client.h
new file mode 100644
index 0000000..aed652e
--- /dev/null
+++ b/examples/androidvoip/jni/android_voip_client.h
@@ -0,0 +1,156 @@
+/*
+ *  Copyright 2020 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#ifndef EXAMPLES_ANDROIDVOIP_JNI_ANDROID_VOIP_CLIENT_H_
+#define EXAMPLES_ANDROIDVOIP_JNI_ANDROID_VOIP_CLIENT_H_
+
+#include <jni.h>
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "api/audio_codecs/audio_format.h"
+#include "api/call/transport.h"
+#include "api/voip/voip_base.h"
+#include "api/voip/voip_engine.h"
+#include "rtc_base/async_packet_socket.h"
+#include "rtc_base/async_udp_socket.h"
+#include "rtc_base/socket_address.h"
+#include "rtc_base/third_party/sigslot/sigslot.h"
+#include "rtc_base/thread.h"
+#include "sdk/android/native_api/jni/scoped_java_ref.h"
+
+namespace webrtc_examples {
+
+// AndroidVoipClient facilitates the use of the VoIP API defined in
+// api/voip/voip_engine.h. One instance of AndroidVoipClient should
+// suffice for most VoIP applications. AndroidVoipClient implements
+// webrtc::Transport to send RTP/RTCP packets to the remote endpoint.
+// It also creates methods (slots) for sockets to connect to in
+// order to receive RTP/RTCP packets. AndroidVoipClient does all
+// VoipBase related operations with rtc::Thread (voip_thread_), this
+// is to comply with consistent thread usage requirement with
+// ProcessThread used within VoipEngine. AndroidVoipClient is meant
+// to be used by Java through JNI.
+class AndroidVoipClient : public webrtc::Transport,
+                          public sigslot::has_slots<> {
+ public:
+  // Returns a pointer to an AndroidVoipClient object. Clients should
+  // use this factory method to create AndroidVoipClient objects. The
+  // method will return a nullptr in case of initialization errors.
+  // It is the client's responsibility to delete the pointer when
+  // they are done with it (this class provides a Delete() method).
+  static AndroidVoipClient* Create(
+      JNIEnv* env,
+      const webrtc::JavaParamRef<jobject>& application_context);
+
+  ~AndroidVoipClient() override;
+
+  // Returns a Java List of Strings containing names of the built-in
+  // supported codecs.
+  webrtc::ScopedJavaLocalRef<jobject> GetSupportedCodecs(JNIEnv* env);
+
+  // Returns a Java String of the default local IPv4 address. If IPv4
+  // address is not found, returns the default local IPv6 address. If
+  // IPv6 address is not found, returns an empty string.
+  webrtc::ScopedJavaLocalRef<jstring> GetLocalIPAddress(JNIEnv* env);
+
+  // Sets the encoder used by the VoIP API.
+  void SetEncoder(JNIEnv* env,
+                  const webrtc::JavaRef<jstring>& j_encoder_string);
+
+  // Sets the decoders used by the VoIP API.
+  void SetDecoders(JNIEnv* env,
+                   const webrtc::JavaParamRef<jobject>& j_decoder_strings);
+
+  // Sets two local/remote addresses, one for RTP packets, and another for
+  // RTCP packets. The RTP address will have IP address j_ip_address_string
+  // and port number j_port_number_int, the RTCP address will have IP address
+  // j_ip_address_string and port number j_port_number_int+1.
+  void SetLocalAddress(JNIEnv* env,
+                       const webrtc::JavaRef<jstring>& j_ip_address_string,
+                       jint j_port_number_int);
+  void SetRemoteAddress(JNIEnv* env,
+                        const webrtc::JavaRef<jstring>& j_ip_address_string,
+                        jint j_port_number_int);
+
+  // Starts a VoIP session. The VoIP operations below can only be
+  // used after a session has already started. Returns true if session
+  // started successfully and false otherwise.
+  jboolean StartSession(JNIEnv* env);
+
+  // Stops the current session. Returns true if session stopped
+  // successfully and false otherwise.
+  jboolean StopSession(JNIEnv* env);
+
+  // Starts sending RTP/RTCP packets to the remote endpoint. Returns
+  // the return value of StartSend in api/voip/voip_base.h.
+  jboolean StartSend(JNIEnv* env);
+
+  // Stops sending RTP/RTCP packets to the remote endpoint. Returns
+  // the return value of StopSend in api/voip/voip_base.h.
+  jboolean StopSend(JNIEnv* env);
+
+  // Starts playing out the voice data received from the remote endpoint.
+  // Returns the return value of StartPlayout in api/voip/voip_base.h.
+  jboolean StartPlayout(JNIEnv* env);
+
+  // Stops playing out the voice data received from the remote endpoint.
+  // Returns the return value of StopPlayout in api/voip/voip_base.h.
+  jboolean StopPlayout(JNIEnv* env);
+
+  // Deletes this object. Used by client when they are done.
+  void Delete(JNIEnv* env);
+
+  // Implementation for Transport.
+  bool SendRtp(const uint8_t* packet,
+               size_t length,
+               const webrtc::PacketOptions& options) override;
+  bool SendRtcp(const uint8_t* packet, size_t length) override;
+
+  // Slots for sockets to connect to.
+  void OnSignalReadRTPPacket(rtc::AsyncPacketSocket* socket,
+                             const char* rtp_packet,
+                             size_t size,
+                             const rtc::SocketAddress& addr,
+                             const int64_t& timestamp);
+  void OnSignalReadRTCPPacket(rtc::AsyncPacketSocket* socket,
+                              const char* rtcp_packet,
+                              size_t size,
+                              const rtc::SocketAddress& addr,
+                              const int64_t& timestamp);
+
+ private:
+  AndroidVoipClient(JNIEnv* env,
+                    const webrtc::JavaParamRef<jobject>& application_context);
+
+  // Used to invoke VoipBase operations and send/receive
+  // RTP/RTCP packets.
+  std::unique_ptr<rtc::Thread> voip_thread_;
+  // A list of AudioCodecSpec supported by the built-in
+  // encoder/decoder factories.
+  std::vector<webrtc::AudioCodecSpec> supported_codecs_;
+  // The entry point to all VoIP APIs.
+  std::unique_ptr<webrtc::VoipEngine> voip_engine_;
+  // Used by the VoIP API to facilitate a VoIP session.
+  absl::optional<webrtc::ChannelId> channel_;
+  // Members below are used for network related operations.
+  std::unique_ptr<rtc::AsyncUDPSocket> rtp_socket_;
+  std::unique_ptr<rtc::AsyncUDPSocket> rtcp_socket_;
+  rtc::SocketAddress rtp_local_address_;
+  rtc::SocketAddress rtcp_local_address_;
+  rtc::SocketAddress rtp_remote_address_;
+  rtc::SocketAddress rtcp_remote_address_;
+};
+
+}  // namespace webrtc_examples
+
+#endif  // EXAMPLES_ANDROIDVOIP_JNI_ANDROID_VOIP_CLIENT_H_
diff --git a/examples/androidvoip/jni/onload.cc b/examples/androidvoip/jni/onload.cc
new file mode 100644
index 0000000..b952de3
--- /dev/null
+++ b/examples/androidvoip/jni/onload.cc
@@ -0,0 +1,28 @@
+/*
+ *  Copyright 2020 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#include <jni.h>
+
+#include "rtc_base/ssl_adapter.h"
+#include "sdk/android/native_api/base/init.h"
+
+namespace webrtc_examples {
+
+extern "C" jint JNIEXPORT JNICALL JNI_OnLoad(JavaVM* jvm, void* reserved) {
+  webrtc::InitAndroid(jvm);
+  RTC_CHECK(rtc::InitializeSSL()) << "Failed to InitializeSSL()";
+  return JNI_VERSION_1_6;
+}
+
+extern "C" void JNIEXPORT JNICALL JNI_OnUnLoad(JavaVM* jvm, void* reserved) {
+  RTC_CHECK(rtc::CleanupSSL()) << "Failed to CleanupSSL()";
+}
+
+}  // namespace webrtc_examples
diff --git a/examples/androidvoip/res/layout/activity_main.xml b/examples/androidvoip/res/layout/activity_main.xml
new file mode 100644
index 0000000..c7fa5a9
--- /dev/null
+++ b/examples/androidvoip/res/layout/activity_main.xml
@@ -0,0 +1,303 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView
+  xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:app="http://schemas.android.com/apk/res-auto"
+  xmlns:tools="http://schemas.android.com/tools"
+  android:id="@+id/scroll_view"
+  android:layout_width="match_parent"
+  android:layout_height="wrap_content"
+  android:focusable="true"
+  android:focusableInTouchMode="true"
+  tools:context="org.webrtc.examples.androidvoip.MainActivity">
+
+  <LinearLayout
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:padding="8dp">
+
+    <TextView
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:layout_marginBottom="15dp"
+      android:layout_marginLeft="15dp"
+      android:layout_marginTop="15dp"
+      android:text="@string/local_endpoint_text_view"
+      android:textSize="19dp"
+      android:textStyle="bold"
+      android:textColor="@color/almost_black" />
+
+    <!--Local IP Adress-->
+    <LinearLayout
+      android:orientation="horizontal"
+      android:layout_width="match_parent"
+      android:layout_height="48dp"
+      android:layout_gravity="center_vertical" >
+
+      <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="12dp"
+        android:layout_marginLeft="15dp"
+        android:layout_marginRight="15dp"
+        android:text="@string/ip_address_text_view"
+        android:textSize="16dp" />
+
+      <TextView
+        android:id="@+id/local_ip_address_text_view"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="12dp"
+        android:layout_marginRight="15dp"
+        android:textSize="16dp" />
+
+    </LinearLayout>
+
+    <!--Local Port Number-->
+    <LinearLayout
+      android:orientation="horizontal"
+      android:layout_width="match_parent"
+      android:layout_height="48dp"
+      android:layout_gravity="center_vertical">
+
+      <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="15dp"
+        android:layout_marginLeft="15dp"
+        android:layout_marginRight="15dp"
+        android:text="@string/port_number_text_view"
+        android:textSize="16dp" />
+
+      <EditText
+        android:id="@+id/local_port_number_edit_text"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_marginRight="15dp"
+        android:layout_weight="1"
+        android:text="10000"
+        android:inputType="number"
+        android:textSize="16dp" />
+
+    </LinearLayout>
+
+    <TextView
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:layout_marginBottom="15dp"
+      android:layout_marginLeft="15dp"
+      android:layout_marginTop="30dp"
+      android:text="@string/remote_endpoint_text_view"
+      android:textSize="19dp"
+      android:textStyle="bold"
+      android:textColor="@color/almost_black" />
+
+    <!--Remote IP Adress-->
+    <LinearLayout
+      android:orientation="horizontal"
+      android:layout_width="match_parent"
+      android:layout_height="48dp"
+      android:layout_gravity="center_vertical">
+
+      <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="15dp"
+        android:layout_marginLeft="15dp"
+        android:layout_marginRight="15dp"
+        android:text="@string/ip_address_text_view"
+        android:textSize="16dp" />
+
+      <EditText
+        android:id="@+id/remote_ip_address_edit_text"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginRight="15dp"
+        android:layout_weight="1"
+        android:inputType="number"
+        android:digits="0123456789."
+        android:textSize="16dp" />
+
+    </LinearLayout>
+
+    <!--Remote Port Number-->
+    <LinearLayout
+      android:orientation="horizontal"
+      android:layout_width="match_parent"
+      android:layout_height="48dp"
+      android:layout_gravity="center_vertical">
+
+      <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="15dp"
+        android:layout_marginLeft="15dp"
+        android:layout_marginRight="15dp"
+        android:text="@string/port_number_text_view"
+        android:textSize="16dp" />
+
+      <EditText
+        android:id="@+id/remote_port_number_edit_text"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_marginRight="15dp"
+        android:layout_weight="1"
+        android:text="10000"
+        android:inputType="number"
+        android:textSize="16dp" />
+
+    </LinearLayout>
+
+    <TextView
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_marginBottom="15dp"
+      android:layout_marginLeft="15dp"
+      android:layout_marginTop="30dp"
+      android:text="@string/encoder_text_view"
+      android:textSize="19dp"
+      android:textStyle="bold"
+      android:textColor="@color/almost_black" />
+
+    <Spinner
+      android:id="@+id/encoder_spinner"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_marginBottom="15dp"
+      android:layout_marginLeft="15dp"
+      android:layout_marginTop="10dp"/>
+
+    <LinearLayout
+      android:orientation="horizontal"
+      android:layout_width="match_parent"
+      android:layout_height="48dp"
+      android:layout_marginTop="20dp"
+      android:layout_gravity="center_vertical">
+
+      <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="15dp"
+        android:layout_marginLeft="15dp"
+        android:layout_marginRight="25dp"
+        android:text="@string/decoder_text_view"
+        android:textSize="19dp"
+        android:textStyle="bold"
+        android:textColor="@color/almost_black" />
+
+      <Button
+        android:id="@+id/decoder_selection_button"
+        android:text="@string/decoder_selection_button"
+        style="?android:attr/buttonBarButtonStyle"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_marginRight="15dp"
+        android:layout_weight="1" />
+
+    </LinearLayout>
+
+
+    <TextView
+      android:id="@+id/decoders_text_view"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_marginTop="15dp"
+      android:layout_marginBottom="30dp"
+      android:layout_marginLeft="15dp"
+      android:layout_marginRight="15dp"
+      android:text="@string/decoders_text_view_default"
+      android:textSize="16dp" />
+
+
+    <RelativeLayout
+      android:id="@+id/switch_layout"
+      android:layout_width="fill_parent"
+      android:layout_height="wrap_content"
+      android:layout_marginTop="15dp"
+      android:visibility="gone" >
+
+      <View
+        android:id="@+id/divider"
+        android:layout_width="match_parent"
+        android:layout_height="1dp"
+        android:layout_marginLeft="15dp"
+        android:layout_marginRight="15dp"
+        android:layout_marginBottom="45dp"
+        android:background="@color/light_gray" />
+
+      <LinearLayout
+        android:id="@+id/start_send_switch_layout"
+        android:orientation="horizontal"
+        android:layout_width="match_parent"
+        android:layout_height="48dp"
+        android:layout_gravity="center_vertical"
+        android:layout_below="@id/divider" >
+
+        <TextView
+          android:layout_width="wrap_content"
+          android:layout_height="match_parent"
+          android:layout_marginLeft="15dp"
+          android:gravity="left"
+          android:layout_weight="1"
+          android:text="@string/start_send_text_view"
+          android:textSize="16dp" />
+
+        <Switch
+          android:id="@+id/start_send_switch"
+          android:layout_width="wrap_content"
+          android:layout_height="match_parent"
+          android:layout_marginRight="15dp"
+          android:gravity="right"
+          android:layout_weight="1" />
+
+      </LinearLayout>
+
+      <LinearLayout
+        android:orientation="horizontal"
+        android:layout_width="match_parent"
+        android:layout_height="48dp"
+        android:layout_gravity="center_vertical"
+        android:layout_below="@id/start_send_switch_layout">
+
+        <TextView
+          android:id="@+id/start_playout_text_view"
+          android:layout_width="wrap_content"
+          android:layout_height="match_parent"
+          android:layout_marginLeft="15dp"
+          android:gravity="left"
+          android:layout_weight="1"
+          android:text="@string/start_playout_text_view"
+          android:textSize="16dp" />
+
+        <Switch
+          android:id="@+id/start_playout_switch"
+          android:layout_width="wrap_content"
+          android:layout_height="match_parent"
+          android:layout_marginRight="15dp"
+          android:gravity="right"
+          android:layout_weight="1" />
+
+      </LinearLayout>
+
+    </RelativeLayout>
+
+    <LinearLayout
+      android:layout_width="match_parent"
+      android:layout_height="match_parent"
+      android:gravity="center"
+      android:orientation="vertical" >
+
+      <ToggleButton
+        android:id="@+id/session_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_margin="8dp"
+        android:textOff="@string/session_button_text_off"
+        android:textOn="@string/session_button_text_on"
+        style="?android:attr/buttonStyle" />
+
+    </LinearLayout>
+
+  </LinearLayout>
+
+</ScrollView>
diff --git a/examples/androidvoip/res/values/colors.xml b/examples/androidvoip/res/values/colors.xml
new file mode 100644
index 0000000..4dadaa9
--- /dev/null
+++ b/examples/androidvoip/res/values/colors.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+   <color name="almost_black">#484848</color>
+   <color name="light_gray">#D3D3D3</color>
+</resources>
\ No newline at end of file
diff --git a/examples/androidvoip/res/values/strings.xml b/examples/androidvoip/res/values/strings.xml
new file mode 100644
index 0000000..d519bfb
--- /dev/null
+++ b/examples/androidvoip/res/values/strings.xml
@@ -0,0 +1,19 @@
+<resources>
+  <string name="app_name">androidvoip</string>
+  <string name="local_endpoint_text_view">Local Endpoint</string>
+  <string name="remote_endpoint_text_view">Remote Endpoint</string>
+  <string name="ip_address_text_view">IP Address:</string>
+  <string name="port_number_text_view">Port Number:</string>
+  <string name="encoder_text_view">Select Encoder</string>
+  <string name="decoder_text_view">Select Decoder</string>
+  <string name="decoder_selection_button">Configure Selection</string>
+  <string name="decoders_text_view_default">No decoders selected</string>
+  <string name="dialog_title">Choose Decoders</string>
+  <string name="ok_label">Ok</string>
+  <string name="dismiss_label">Dismiss</string>
+  <string name="clear_all_label">Clear All</string>
+  <string name="start_send_text_view">Start Sending</string>
+  <string name="start_playout_text_view">Start Playout</string>
+  <string name="session_button_text_off">Start Session</string>
+  <string name="session_button_text_on">Stop Session</string>
+</resources>