diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index eaa5aa7f..908d8e2b 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -60,6 +60,10 @@ add_library(lib_zrtpcppcore STATIC IMPORTED) set_target_properties(lib_zrtpcppcore PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/gzrtp/lib/${ANDROID_ABI}/libzrtpcppcore.a) +add_library(lib_sndfile STATIC IMPORTED) +set_target_properties(lib_sndfile PROPERTIES IMPORTED_LOCATION + ${distribution_DIR}/sndfile/lib/${ANDROID_ABI}/libsndfile.a) + add_library(lib_baresip STATIC IMPORTED) set_target_properties(lib_baresip PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/baresip/lib/${ANDROID_ABI}/libbaresip.a) @@ -90,5 +94,6 @@ target_link_libraries(baresip lib_amrwbenc lib_webrtc lib_zrtpcppcore + lib_sndfile z log) diff --git a/app/src/main/cpp/baresip.c b/app/src/main/cpp/baresip.c index 86e3f577..0e6477c9 100644 --- a/app/src/main/cpp/baresip.c +++ b/app/src/main/cpp/baresip.c @@ -147,7 +147,8 @@ static void ua_event_handler(struct ua *ua, enum ua_event ev, const char *tone; char event_buf[256]; enum sdp_dir ardir; - int len; + int len, err; + struct pl module, module_event, data; LOGD("ua event (%s) %s\n", uag_event_str(ev), prm); @@ -225,6 +226,15 @@ static void ua_event_handler(struct ua *ua, enum ua_event ev, case UA_EVENT_MWI_NOTIFY: len = re_snprintf(event_buf, sizeof event_buf, "mwi notify,%s", prm); break; + case UA_EVENT_MODULE: + err = re_regex(prm, strlen(prm), "[^,]*,[^,]*,[~]*", + &module, &module_event, &data); + if (err) + return; + if (!pl_strcmp(&module_event, "dump")) { + len = re_snprintf(event_buf, sizeof event_buf, "sndfile dump,%r", &data); + break; + } default: return; } @@ -1088,7 +1098,7 @@ Java_com_tutpro_baresip_Api_ua_1answer(JNIEnv *env, jobject thiz, jlong ua, jlon re_thread_leave(); } -JNIEXPORT jint JNICALL +JNIEXPORT void JNICALL Java_com_tutpro_baresip_Api_ua_1add_1custom_1header(JNIEnv *env, jobject thiz, jlong ua, jstring jname, jstring jbody) { const char *name = (*env)->GetStringUTFChars(env, jname, 0); @@ -1104,7 +1114,6 @@ Java_com_tutpro_baresip_Api_ua_1add_1custom_1header(JNIEnv *env, jobject thiz, j LOGW("adding custom header to ua %ld failed with error %d\n", (long)ua, err); (*env)->ReleaseStringUTFChars(env, jname, name); (*env)->ReleaseStringUTFChars(env, jbody, body); - return err; } JNIEXPORT void JNICALL diff --git a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt index b356eff2..9a58c510 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt @@ -43,6 +43,7 @@ import androidx.media.AudioManagerCompat import java.io.File import java.net.InetAddress import java.nio.charset.StandardCharsets +import java.text.SimpleDateFormat import java.util.* import kotlin.concurrent.schedule import kotlin.math.roundToInt @@ -384,6 +385,23 @@ class BaresipService: Service() { if (File(filesDir, "history").exists()) File(filesDir, "history").renameTo(File(filesDir, "calls")) + if (!File(filesDir, "tmp").exists()) + File(filesDir, "tmp").mkdir() + + if (!File(filesDir, "recordings").exists()) + File(filesDir, "recordings").mkdir() + else { + File(filesDir, "recordings").walk().forEach { f -> + Log.d(TAG, "***** file ${f.name}") + val recording = f.name.substringBeforeLast("-") + if (recording.startsWith("dump")) { + val time = recording.takeLast(19) + if (!recordings.containsKey(time)) + recordings[time] = filesDir.absolutePath + "/recordings/" + recording + } + } + Log.d(TAG, "Recordings $recordings") + } if (contactsMode != "android") Contact.restoreBaresipContacts() @@ -573,6 +591,11 @@ class BaresipService: Service() { return } + if (ev[0] == "sndfile dump") { + Log.d(TAG, "Got sndfile dump ${ev[1]}") + return + } + val ua = UserAgent.ofUap(uap) if (ua == null) { Log.w(TAG, "uaEvent $event did not find ua $uap") @@ -877,6 +900,23 @@ class BaresipService: Service() { NewCallHistory.add(history) NewCallHistory.save() ua.account.missedCalls = ua.account.missedCalls || missed + if (isRecOn && call.startTime != null) { + val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss") + val startTime = call.startTime!! + var time = format.format(startTime.time).toString() + var recording = filesDir.absolutePath + "/recordings/" + + "dump-$aor=>${call.peerUri}-$time" + if (File("$recording-dec.wav").exists()) { + recordings[time] = recording + } else { + startTime.add(Calendar.SECOND, 1) + time = format.format(startTime.time).toString() + recording = filesDir.absolutePath + "/recordings/" + + "dump-$aor=>${call.peerUri}-$time" + recordings[time] = recording + } + + } } if (!Utils.isVisible()) { if (missed) { @@ -1496,6 +1536,7 @@ class BaresipService: Service() { var callActionUri = "" var isMainVisible = false var isMicMuted = false + var isRecOn = false val uas = ArrayList() val calls = ArrayList() @@ -1514,6 +1555,7 @@ class BaresipService: Service() { var dnsServers = listOf() val serviceEvent = MutableLiveData>() val serviceEvents = mutableListOf() + val recordings: MutableMap = mutableMapOf() private var audioFocusRequest: AudioFocusRequestCompat? = null private var btAdapter: BluetoothAdapter? = null diff --git a/app/src/main/kotlin/com/tutpro/baresip/CallDetailsAdapter.kt b/app/src/main/kotlin/com/tutpro/baresip/CallDetailsAdapter.kt index d7516588..19e1159e 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/CallDetailsAdapter.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/CallDetailsAdapter.kt @@ -1,6 +1,8 @@ package com.tutpro.baresip +import android.annotation.SuppressLint import android.content.Context +import android.graphics.Typeface import android.text.format.DateUtils import android.view.LayoutInflater import android.view.View @@ -9,6 +11,7 @@ import android.widget.ArrayAdapter import android.widget.ImageView import android.widget.TextView import java.text.DateFormat +import java.text.SimpleDateFormat import java.util.* @@ -23,6 +26,7 @@ class CallDetailsAdapter(private val ctx: Context, private val rows: ArrayList> + private var recIcon: MenuItem? = null private var micIcon: MenuItem? = null private var speakerIcon: MenuItem? = null private lateinit var swipeRefresh: SwipeRefreshLayout @@ -1258,6 +1259,13 @@ class MainActivity : AppCompatActivity() { menuInflater.inflate(R.menu.main_menu, menu) + menuInflater.inflate(R.menu.rec_icon, menu) + recIcon = menu.findItem(R.id.recIcon) + if (BaresipService.isRecOn) + recIcon!!.setIcon(R.drawable.rec_on) + else + recIcon!!.setIcon(R.drawable.rec_off) + menuInflater.inflate(R.menu.mic_icon, menu) micIcon = menu.findItem(R.id.micIcon) if (BaresipService.isMicMuted) @@ -1279,6 +1287,19 @@ class MainActivity : AppCompatActivity() { when (item.itemId) { + R.id.recIcon -> { + if (Call.call("connected") == null) { + BaresipService.isRecOn = !BaresipService.isRecOn + if (BaresipService.isRecOn) { + item.setIcon(R.drawable.rec_on) + Api.module_load("sndfile") + } else { + item.setIcon(R.drawable.rec_off) + Api.module_unload("sndfile") + } + } + } + R.id.micIcon -> { if (Call.call("connected") != null) { BaresipService.isMicMuted = !BaresipService.isMicMuted diff --git a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt index ba44da0a..46928715 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt @@ -10,6 +10,7 @@ import android.graphics.Bitmap.createScaledBitmap import android.graphics.BitmapFactory import android.graphics.Canvas import android.graphics.Color +import android.media.AudioAttributes import android.net.Uri import android.net.wifi.WifiManager import android.provider.DocumentsContract @@ -24,10 +25,12 @@ import android.widget.ImageView import android.widget.TextView import android.media.AudioDeviceInfo import android.media.AudioManager +import android.media.MediaPlayer import android.os.* import androidx.activity.result.ActivityResultLauncher import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat +import androidx.core.net.toUri import androidx.core.text.isDigitsOnly import androidx.lifecycle.Lifecycle import androidx.lifecycle.ProcessLifecycleOwner @@ -992,4 +995,105 @@ object Utils { } } + fun playRecording(ctx: Context, recording: String) { + Log.d(TAG, "Playing recording $recording") + val decPlayer = MediaPlayer() + decPlayer.apply { + setAudioAttributes( + AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .setUsage(AudioAttributes.USAGE_MEDIA) + .build() + ) + setOnPreparedListener { + val encPlayer = MediaPlayer() + encPlayer.apply { + setAudioAttributes( + AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .setUsage(AudioAttributes.USAGE_MEDIA) + .build() + ) + setOnPreparedListener { + it.start() + decPlayer.start() + Log.d(TAG, "Started players") + } + setOnCompletionListener { + Log.d(TAG, "Stopping encPlayer") + it.stop() + it.release() + } + try { + val file = "$recording-enc.wav" + val encFile = File(file).copyTo(File(BaresipService.filesPath + "/tmp/encode.wav"), true) + val encUri = encFile.toUri() + Log.d(TAG, "Setting data source $encUri") + setDataSource(ctx, encUri) + prepareAsync() + } catch (e: IllegalArgumentException) { + Log.e(TAG, "encPlayer IllegalArgumentException: $e") + } catch (e: IOException) { + Log.e(TAG, "encPlayer IOException: $e") + } catch (e: Exception) { + Log.e(TAG, "encPlayer Exception: $e") + } + } + } + setOnCompletionListener { + Log.d(TAG, "Stopping decPlayer") + it.stop() + it.release() + } + try { + val file = "$recording-dec.wav" + val decFile = File(file).copyTo(File(BaresipService.filesPath + "/tmp/decode.wav"), true) + val decUri = decFile.toUri() + Log.d(TAG, "Setting data source $decUri") + setDataSource(ctx, decUri) + prepareAsync() + } catch (e: IllegalArgumentException) { + Log.e(TAG, "decPlayer IllegalArgumentException: $e") + } catch (e: IOException) { + Log.e(TAG, "decPlayer IOException: $e") + } catch (e: Exception) { + Log.e(TAG, "decPlayer Exception: $e") + } + } + } + + @Suppress("unused") + fun playFile(ctx: Context, path: String) { + Log.d(TAG, "Playing file $path") + MediaPlayer().apply { + setAudioAttributes( + AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .setUsage(AudioAttributes.USAGE_MEDIA) + .build() + ) + setOnPreparedListener { + Log.d(TAG, "Starting MediaPlayer") + it.start() + Log.d(TAG, "MediaPlayer started") + } + setOnCompletionListener { + Log.d(TAG, "Stopping MediaPlayer") + it.stop() + it.release() + } + try { + Log.d(TAG, "Preparing $path") + setDataSource(ctx, path.toUri()) + prepareAsync() + } catch (e: IllegalArgumentException) { + Log.e(TAG, "MediaPlayer IllegalArgumentException: ${e.printStackTrace()}") + } catch (e: IOException) { + Log.e(TAG, "MediaPlayer IOException: ${e.printStackTrace()}") + } catch (e: Exception) { + Log.e(TAG, "MediaPlayer Exception: ${e.printStackTrace()}") + } + } + } + }