Walkie Talkie Android App
This video shows the steps and code to develop your own walkie talkie Android app. It uses Firebase Realtime database to exchange the audio data over internet.
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:
Java Code:
package com.programmerworld.walkietalkieandroidapp;
import static android.Manifest.permission.RECORD_AUDIO;
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.AcousticEchoCanceler;
import android.media.audiofx.NoiseSuppressor;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import androidx.activity.EdgeToEdge;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.ValueEventListener;
import java.util.Base64;
public class MainActivity extends AppCompatActivity {
private Button btnStartRecord, btnStopRecord, btnPlay;
private AudioRecord audioRecord;
private AudioTrack audioTrack;
private boolean isRecording = false;
private static final int SAMPLE_RATE = 16000;
private static final int BUFFER_SIZE = AudioRecord.getMinBufferSize(
SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
private DatabaseReference audioRef;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
ActivityCompat.requestPermissions(this,
new String[]{RECORD_AUDIO},
PackageManager.PERMISSION_GRANTED);
audioTrack = new AudioTrack(
AudioManager.STREAM_MUSIC,
SAMPLE_RATE,
AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT,
BUFFER_SIZE,
AudioTrack.MODE_STREAM);
btnStartRecord = findViewById(R.id.btnStartRecord);
btnStopRecord = findViewById(R.id.btnStopRecord);
btnPlay = findViewById(R.id.btnPlay);
// Firebase setup
FirebaseDatabase database = FirebaseDatabase.getInstance();
audioRef = database.getReference("audioStream");
// Start recording
btnStartRecord.setOnClickListener(v1 -> startRecording());
// Stop recording
btnStopRecord.setOnClickListener(v1 -> stopRecording());
// Play received audio
btnPlay.setOnClickListener(v1 -> playAudioStream());
// Listen to Firebase for audio data
audioRef.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot snapshot) {
String encodedAudio = snapshot.getValue(String.class);
if (encodedAudio != null) {
byte[] decodedAudio = Base64.getDecoder().decode(encodedAudio);
audioTrack.write(decodedAudio, 0, decodedAudio.length);
}
}
@Override
public void onCancelled(@NonNull DatabaseError error) {
Toast.makeText(MainActivity.this, "Failed to receive audio.", Toast.LENGTH_LONG).show();
}
});
return insets;
});
}
private void startRecording() {
if (ActivityCompat.checkSelfPermission(this, RECORD_AUDIO) != 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.
Toast.makeText(MainActivity.this, "Audio Recording permission not available!", Toast.LENGTH_LONG).show();
return;
}
audioRecord = new AudioRecord(
MediaRecorder.AudioSource.MIC,
SAMPLE_RATE,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT,
BUFFER_SIZE);
// Add noise suppressor
if (NoiseSuppressor.isAvailable()) {
NoiseSuppressor.create(audioRecord.getAudioSessionId());
}
// Add echo canceller
if (AcousticEchoCanceler.isAvailable()) {
AcousticEchoCanceler.create(audioRecord.getAudioSessionId());
}
audioRecord.startRecording();
isRecording = true;
btnStartRecord.setVisibility(View.GONE);
btnStopRecord.setVisibility(View.VISIBLE);
// Start sending audio data to Firebase
new Thread(() -> {
byte[] buffer = new byte[BUFFER_SIZE];
while (isRecording) {
int read = audioRecord.read(buffer, 0, BUFFER_SIZE);
if (read > 0) {
String encodedAudio = Base64.getEncoder().encodeToString(buffer);
audioRef.setValue(encodedAudio);
}
}
}).start();
}
private void stopRecording() {
if (audioRecord != null) {
isRecording = false;
audioRecord.stop();
audioRecord.release();
audioRecord = null;
btnStartRecord.setVisibility(View.VISIBLE);
btnStopRecord.setVisibility(View.GONE);
}
}
private void playAudioStream() {
audioTrack.play();
}
}
Manifest File:
<?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.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/walkie_talkie_icon"
android:label="@string/app_name"
android:roundIcon="@mipmap/walkie_talkie_icon_round"
android:supportsRtl="true"
android:theme="@style/Theme.WalkieTalkieAndroidApp"
tools:targetApi="31">
<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>
Gradle Files:
Root Level:
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
id("com.google.gms.google-services") version "4.4.2" apply false
}
Project Level:
plugins {
alias(libs.plugins.android.application)
id("com.google.gms.google-services")
}
android {
namespace = "com.programmerworld.walkietalkieandroidapp"
compileSdk = 34
defaultConfig {
applicationId = "com.programmerworld.walkietalkieandroidapp"
minSdk = 32
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
dependencies {
implementation(libs.appcompat)
implementation(libs.material)
implementation(libs.activity)
implementation(libs.constraintlayout)
testImplementation(libs.junit)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.espresso.core)
implementation(platform("com.google.firebase:firebase-bom:33.7.0"))
implementation("com.google.firebase:firebase-database:20.3.2")
}
Layout XML file:
<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"
android:id="@+id/main">
<Button
android:id="@+id/btnStartRecord"
android:layout_width="match_parent"
android:layout_height="100dp"
android:text="Start" />
<Button
android:id="@+id/btnStopRecord"
android:layout_width="match_parent"
android:layout_height="100dp"
android:text="Stop"
android:visibility="gone" />
<Button
android:id="@+id/btnPlay"
android:layout_width="match_parent"
android:layout_height="100dp"
android:text="Play" />
</LinearLayout>
Screenshots:


