Bluetooth Voice Call Android App
In this video it shows the code to develop a Bluetooth based voice calling Android App. In this it uses AudioRecord for recording and sending the voice packets. It uses AudioTrack for receiving and playing the sound in the local.
I hope you like this video. For any questions, suggestions or appreciation please contact us at: https://programmerworld.co/contact/ or email at: programmerworld1990@gmail.com
Details:
Code:
package com.programmerworld.voicecallbluetoothapp;
import android.Manifest;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.media.AudioManager;
import android.os.Bundle;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import java.io.IOException;
import java.util.Set;
import java.util.UUID;
public class MainActivity extends AppCompatActivity {
private static final String APP_NAME = "BTVoiceCall";
private static final UUID MY_UUID = UUID.fromString("8ce255c0-200a-11e0-ac64-0800200c9a66");
private static final int REQUEST_ENABLE_BT = 1;
private static final int REQUEST_PERMISSIONS = 2;
private BluetoothAdapter bluetoothAdapter;
private BluetoothDevice remoteDevice;
private BluetoothSocket bluetoothSocket;
private BluetoothService bluetoothService;
private AcceptThread acceptThread;
private TextView statusText;
private AudioManager audioManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
bluetoothService = new BluetoothService(this);
audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
statusText = findViewById(R.id.statusText);
Button btnBecomeDiscoverable = findViewById(R.id.btnBecomeDiscoverable);
Button btnConnect = findViewById(R.id.btnConnect);
Button btnStartCall = findViewById(R.id.btnStartCall);
Button btnEndCall = findViewById(R.id.btnEndCall);
checkPermissions();
btnBecomeDiscoverable.setOnClickListener(v -> becomeDiscoverable());
btnConnect.setOnClickListener(v -> connectToDevice());
btnStartCall.setOnClickListener(v -> startCall());
btnEndCall.setOnClickListener(v -> endCall());
// Start listening for incoming connections
acceptThread = new AcceptThread();
acceptThread.start();
}
private void checkPermissions() {
String[] permissions = {
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.MODIFY_AUDIO_SETTINGS,
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.BLUETOOTH_SCAN
};
boolean needPermission = false;
for (String permission : permissions) {
if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
needPermission = true;
break;
}
}
if (needPermission) {
ActivityCompat.requestPermissions(this, permissions, REQUEST_PERMISSIONS);
}
}
private void becomeDiscoverable() {
if (bluetoothAdapter == null) {
Toast.makeText(this, "Bluetooth not supported", Toast.LENGTH_SHORT).show();
return;
}
if (!bluetoothAdapter.isEnabled()) {
Toast.makeText(this, "Please enable Bluetooth", Toast.LENGTH_SHORT).show();
return;
}
Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_ADVERTISE) != PackageManager.PERMISSION_GRANTED) {
// TODO: Consider calling
// ActivityCompat#requestPermissions
// here to request the missing permissions, and then overriding
// public void onRequestPermissionsResult(int requestCode, String[] permissions,
// int[] grantResults)
// to handle the case where the user grants the permission. See the documentation
// for ActivityCompat#requestPermissions for more details.
return;
}
startActivityForResult(discoverableIntent, REQUEST_ENABLE_BT);
}
private void connectToDevice() {
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
Toast.makeText(this, "Bluetooth not enabled", Toast.LENGTH_SHORT).show();
return;
}
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.BLUETOOTH_CONNECT}, REQUEST_ENABLE_BT);
return;
}
Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();
if (pairedDevices.size() > 0) {
for (BluetoothDevice device : pairedDevices) {
if (device.getName().contains("Redmi")) { // Modify as needed
remoteDevice = device;
Toast.makeText(this, "Connecting to " + device.getName(), Toast.LENGTH_SHORT).show();
break;
}
}
if (remoteDevice == null) {
remoteDevice = pairedDevices.iterator().next();
Toast.makeText(this, "Connecting to " + remoteDevice.getName(), Toast.LENGTH_SHORT).show();
}
new ConnectThread(remoteDevice).start();
} else {
Toast.makeText(this, "No paired devices found", Toast.LENGTH_SHORT).show();
}
}
private void startCall() {
if (bluetoothService.isConnected()) {
if (bluetoothService.startVoiceCall(audioManager)) {
statusText.setText("Status: Call Active");
} else {
Toast.makeText(this, "Audio permissions needed", Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(this, "Please connect first", Toast.LENGTH_SHORT).show();
}
}
private void endCall() {
bluetoothService.stopVoiceCall(audioManager);
statusText.setText("Status: Disconnected");
}
private class AcceptThread extends Thread {
private BluetoothServerSocket serverSocket;
public AcceptThread() {
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.BLUETOOTH_CONNECT)
== PackageManager.PERMISSION_GRANTED) {
try {
serverSocket = bluetoothAdapter.listenUsingRfcommWithServiceRecord(APP_NAME, MY_UUID);
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void run() {
BluetoothSocket socket;
while (true) {
try {
if (serverSocket != null) {
socket = serverSocket.accept();
if (socket != null) {
manageConnectedSocket(socket);
serverSocket.close();
break;
}
}
} catch (IOException e) {
e.printStackTrace();
break;
}
}
}
public void cancel() {
try {
if (serverSocket != null) serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private class ConnectThread extends Thread {
private final BluetoothSocket socket;
private final BluetoothDevice device;
public ConnectThread(BluetoothDevice device) {
this.device = device;
BluetoothSocket temp = null;
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.BLUETOOTH_CONNECT)
== PackageManager.PERMISSION_GRANTED) {
try {
temp = device.createRfcommSocketToServiceRecord(MY_UUID);
} catch (IOException e) {
e.printStackTrace();
}
}
socket = temp;
}
public void run() {
if (socket == null) return;
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.BLUETOOTH_SCAN)
== PackageManager.PERMISSION_GRANTED) {
bluetoothAdapter.cancelDiscovery();
}
try {
socket.connect();
manageConnectedSocket(socket);
} catch (IOException e) {
e.printStackTrace();
try {
socket.close();
} catch (IOException closeException) {
closeException.printStackTrace();
}
runOnUiThread(() -> Toast.makeText(MainActivity.this, "Connection failed: " + e.getMessage(), Toast.LENGTH_LONG).show());
}
}
}
private void manageConnectedSocket(BluetoothSocket socket) {
bluetoothSocket = socket;
bluetoothService.setSocket(socket);
runOnUiThread(() -> {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
// TODO: Consider calling
// ActivityCompat#requestPermissions
// here to request the missing permissions, and then overriding
// public void onRequestPermissionsResult(int requestCode, String[] permissions,
// int[] grantResults)
// to handle the case where the user grants the permission. See the documentation
// for ActivityCompat#requestPermissions for more details.
return;
}
statusText.setText("Status: Connected to " + (remoteDevice != null ? remoteDevice.getName() : "Device"));
Toast.makeText(MainActivity.this, "Connected", Toast.LENGTH_SHORT).show();
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_ENABLE_BT) {
if (resultCode == RESULT_OK) {
Toast.makeText(this, "Device is now discoverable for 300 seconds", Toast.LENGTH_SHORT).show();
statusText.setText("Status: Discoverable");
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_PERMISSIONS || requestCode == REQUEST_ENABLE_BT) {
boolean allGranted = true;
for (int result : grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
allGranted = false;
break;
}
}
if (!allGranted) {
Toast.makeText(this, "Required permissions not granted", Toast.LENGTH_LONG).show();
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (acceptThread != null) acceptThread.cancel();
bluetoothService.disconnect();
}
}
package com.programmerworld.voicecallbluetoothapp;
import android.bluetooth.BluetoothSocket;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaRecorder;
import android.media.audiofx.NoiseSuppressor;
import androidx.core.content.ContextCompat;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class BluetoothService {
private BluetoothSocket bluetoothSocket;
private OutputStream outputStream;
private InputStream inputStream;
private AudioRecord audioRecord;
private AudioTrack audioTrack;
private NoiseSuppressor noiseSuppressor;
private boolean isConnected = false;
private boolean isRecording = false;
private Context context;
// Increased sample rate for better quality
private static final int SAMPLE_RATE = 16000; //44100; // CD quality
private static final int CHANNEL_CONFIG_IN = AudioFormat.CHANNEL_IN_MONO;
private static final int CHANNEL_CONFIG_OUT = AudioFormat.CHANNEL_OUT_MONO;
private static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
private static final int BUFFER_SIZE = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG_IN, AUDIO_FORMAT) * 2; // Double buffer size
public BluetoothService(Context context) {
this.context = context;
}
public void setSocket(BluetoothSocket socket) {
this.bluetoothSocket = socket;
try {
this.outputStream = socket.getOutputStream();
this.inputStream = socket.getInputStream();
this.isConnected = true;
} catch (IOException e) {
e.printStackTrace();
isConnected = false;
}
}
public boolean startVoiceCall(AudioManager audioManager) {
if (!isConnected || bluetoothSocket == null) return false;
if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.RECORD_AUDIO)
!= PackageManager.PERMISSION_GRANTED) {
return false;
}
try {
// Optimize SCO for voice
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
audioManager.setBluetoothScoOn(true);
audioManager.startBluetoothSco();
// Initialize AudioRecord with noise suppression
audioRecord = new AudioRecord(
MediaRecorder.AudioSource.VOICE_COMMUNICATION, // Optimized for voice
SAMPLE_RATE,
CHANNEL_CONFIG_IN,
AUDIO_FORMAT,
BUFFER_SIZE
);
// Enable noise suppression if available
if (NoiseSuppressor.isAvailable()) {
noiseSuppressor = NoiseSuppressor.create(audioRecord.getAudioSessionId());
if (noiseSuppressor != null) {
noiseSuppressor.setEnabled(true);
}
}
audioTrack = new AudioTrack(
AudioManager.STREAM_VOICE_CALL, // Use voice call stream
SAMPLE_RATE,
CHANNEL_CONFIG_OUT,
AUDIO_FORMAT,
BUFFER_SIZE,
AudioTrack.MODE_STREAM
);
isRecording = true;
new Thread(this::recordAndSend).start();
new Thread(this::receiveAndPlay).start();
return true;
} catch (SecurityException e) {
e.printStackTrace();
return false;
}
}
private void recordAndSend() {
byte[] buffer = new byte[BUFFER_SIZE];
audioRecord.startRecording();
while (isRecording) {
int bytesRead = audioRecord.read(buffer, 0, BUFFER_SIZE);
if (bytesRead > 0) {
try {
if (outputStream != null) {
outputStream.write(buffer, 0, bytesRead);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private void receiveAndPlay() {
byte[] buffer = new byte[BUFFER_SIZE];
audioTrack.play();
while (isRecording) {
try {
if (inputStream != null) {
int bytesRead = inputStream.read(buffer);
if (bytesRead > 0) {
audioTrack.write(buffer, 0, bytesRead);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void stopVoiceCall(AudioManager audioManager) {
isRecording = false;
if (audioRecord != null) {
audioRecord.stop();
audioRecord.release();
audioRecord = null;
}
if (audioTrack != null) {
audioTrack.stop();
audioTrack.release();
audioTrack = null;
}
if (noiseSuppressor != null) {
noiseSuppressor.release();
noiseSuppressor = null;
}
audioManager.stopBluetoothSco();
audioManager.setBluetoothScoOn(false);
audioManager.setMode(AudioManager.MODE_NORMAL);
}
public void disconnect() {
try {
isConnected = false;
if (outputStream != null) outputStream.close();
if (inputStream != null) inputStream.close();
if (bluetoothSocket != null) bluetoothSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public boolean isConnected() {
return isConnected;
}
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.VoiceCallBluetoothApp">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<Button
android:id="@+id/btnBecomeDiscoverable"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Become Discoverable" />
<Button
android:id="@+id/btnConnect"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Connect to Device" />
<Button
android:id="@+id/btnStartCall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start Call" />
<Button
android:id="@+id/btnEndCall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="End Call" />
<TextView
android:id="@+id/statusText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Status: Disconnected" />
</LinearLayout>
Screenshots:
