3129 lines
136 KiB
Kotlin
3129 lines
136 KiB
Kotlin
@file:OptIn(ExperimentalMaterial3Api::class)
|
|
|
|
package com.tutpro.baresip
|
|
|
|
import android.Manifest.permission.BLUETOOTH_CONNECT
|
|
import android.Manifest.permission.POST_NOTIFICATIONS
|
|
import android.Manifest.permission.READ_EXTERNAL_STORAGE
|
|
import android.Manifest.permission.RECORD_AUDIO
|
|
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
|
import android.annotation.SuppressLint
|
|
import android.app.KeyguardManager
|
|
import android.app.NotificationManager
|
|
import android.content.BroadcastReceiver
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.content.Intent.ACTION_CALL
|
|
import android.content.Intent.ACTION_DIAL
|
|
import android.content.Intent.ACTION_VIEW
|
|
import android.content.IntentFilter
|
|
import android.content.pm.PackageManager
|
|
import android.content.res.Configuration
|
|
import android.content.res.Configuration.ORIENTATION_PORTRAIT
|
|
import android.media.AudioManager
|
|
import android.net.Uri
|
|
import android.os.Build
|
|
import android.os.Bundle
|
|
import android.os.Handler
|
|
import android.os.Looper
|
|
import android.os.Process
|
|
import android.os.SystemClock
|
|
import android.provider.DocumentsContract
|
|
import android.provider.MediaStore
|
|
import android.view.KeyEvent
|
|
import android.view.WindowManager
|
|
import android.view.inputmethod.InputMethodManager
|
|
import android.widget.Chronometer
|
|
import android.widget.Toast
|
|
import android.window.OnBackInvokedCallback
|
|
import android.window.OnBackInvokedDispatcher
|
|
import androidx.activity.ComponentActivity
|
|
import androidx.activity.OnBackPressedCallback
|
|
import androidx.activity.compose.setContent
|
|
import androidx.activity.enableEdgeToEdge
|
|
import androidx.activity.result.ActivityResultLauncher
|
|
import androidx.activity.result.contract.ActivityResultContracts
|
|
import androidx.activity.viewModels
|
|
import androidx.annotation.RequiresApi
|
|
import androidx.appcompat.app.AppCompatDelegate
|
|
import androidx.compose.animation.animateContentSize
|
|
import androidx.compose.foundation.BorderStroke
|
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
import androidx.compose.foundation.background
|
|
import androidx.compose.foundation.clickable
|
|
import androidx.compose.foundation.combinedClickable
|
|
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
|
import androidx.compose.foundation.gestures.detectTapGestures
|
|
import androidx.compose.foundation.layout.Arrangement
|
|
import androidx.compose.foundation.layout.Box
|
|
import androidx.compose.foundation.layout.Column
|
|
import androidx.compose.foundation.layout.PaddingValues
|
|
import androidx.compose.foundation.layout.Row
|
|
import androidx.compose.foundation.layout.Spacer
|
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
import androidx.compose.foundation.layout.height
|
|
import androidx.compose.foundation.layout.heightIn
|
|
import androidx.compose.foundation.layout.imePadding
|
|
import androidx.compose.foundation.layout.padding
|
|
import androidx.compose.foundation.layout.safeDrawingPadding
|
|
import androidx.compose.foundation.layout.size
|
|
import androidx.compose.foundation.layout.width
|
|
import androidx.compose.foundation.lazy.LazyColumn
|
|
import androidx.compose.foundation.lazy.items
|
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
import androidx.compose.foundation.rememberScrollState
|
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
import androidx.compose.foundation.text.KeyboardOptions
|
|
import androidx.compose.foundation.verticalScroll
|
|
import androidx.compose.material.icons.Icons
|
|
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
|
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
|
import androidx.compose.material.icons.filled.Menu
|
|
import androidx.compose.material.icons.outlined.Clear
|
|
import androidx.compose.material3.BasicAlertDialog
|
|
import androidx.compose.material3.ButtonColors
|
|
import androidx.compose.material3.Card
|
|
import androidx.compose.material3.CardDefaults
|
|
import androidx.compose.material3.DropdownMenuItem
|
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
import androidx.compose.material3.Icon
|
|
import androidx.compose.material3.IconButton
|
|
import androidx.compose.material3.OutlinedButton
|
|
import androidx.compose.material3.OutlinedTextField
|
|
import androidx.compose.material3.OutlinedTextFieldDefaults
|
|
import androidx.compose.material3.Scaffold
|
|
import androidx.compose.material3.Surface
|
|
import androidx.compose.material3.Switch
|
|
import androidx.compose.material3.Text
|
|
import androidx.compose.material3.TextButton
|
|
import androidx.compose.material3.TextField
|
|
import androidx.compose.material3.TopAppBar
|
|
import androidx.compose.material3.TopAppBarDefaults
|
|
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator
|
|
import androidx.compose.material3.pulltorefresh.pullToRefresh
|
|
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.runtime.LaunchedEffect
|
|
import androidx.compose.runtime.collectAsState
|
|
import androidx.compose.runtime.getValue
|
|
import androidx.compose.runtime.mutableFloatStateOf
|
|
import androidx.compose.runtime.mutableIntStateOf
|
|
import androidx.compose.runtime.mutableStateOf
|
|
import androidx.compose.runtime.remember
|
|
import androidx.compose.runtime.saveable.rememberSaveable
|
|
import androidx.compose.runtime.setValue
|
|
import androidx.compose.ui.Alignment
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.draw.shadow
|
|
import androidx.compose.ui.focus.FocusRequester
|
|
import androidx.compose.ui.focus.focusRequester
|
|
import androidx.compose.ui.focus.onFocusChanged
|
|
import androidx.compose.ui.graphics.Color
|
|
import androidx.compose.ui.graphics.toArgb
|
|
import androidx.compose.ui.graphics.vector.ImageVector
|
|
import androidx.compose.ui.input.pointer.pointerInput
|
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
|
import androidx.compose.ui.platform.SoftwareKeyboardController
|
|
import androidx.compose.ui.res.stringResource
|
|
import androidx.compose.ui.res.vectorResource
|
|
import androidx.compose.ui.text.TextStyle
|
|
import androidx.compose.ui.text.font.FontWeight
|
|
import androidx.compose.ui.text.input.KeyboardType
|
|
import androidx.compose.ui.text.style.TextOverflow
|
|
import androidx.compose.ui.unit.dp
|
|
import androidx.compose.ui.unit.sp
|
|
import androidx.compose.ui.viewinterop.AndroidView
|
|
import androidx.core.content.ContextCompat
|
|
import androidx.core.net.toUri
|
|
import androidx.lifecycle.Observer
|
|
import com.tutpro.baresip.BaresipService.Companion.contactNames
|
|
import com.tutpro.baresip.BaresipService.Companion.uas
|
|
import com.tutpro.baresip.BaresipService.Companion.uasStatus
|
|
import com.tutpro.baresip.CustomElements.AlertDialog
|
|
import com.tutpro.baresip.CustomElements.DropdownMenu
|
|
import com.tutpro.baresip.CustomElements.LabelText
|
|
import com.tutpro.baresip.CustomElements.PasswordDialog
|
|
import com.tutpro.baresip.CustomElements.SelectableAlertDialog
|
|
import com.tutpro.baresip.CustomElements.verticalScrollbar
|
|
import kotlinx.coroutines.delay
|
|
import java.io.File
|
|
import java.text.SimpleDateFormat
|
|
import java.util.Date
|
|
import java.util.Locale
|
|
import kotlin.system.exitProcess
|
|
|
|
class MainActivity : ComponentActivity() {
|
|
|
|
private lateinit var imm: InputMethodManager
|
|
private lateinit var nm: NotificationManager
|
|
private lateinit var am: AudioManager
|
|
private lateinit var kgm: KeyguardManager
|
|
private lateinit var screenEventReceiver: BroadcastReceiver
|
|
private lateinit var serviceEventObserver: Observer<Event<Long>>
|
|
private lateinit var requestPermissionLauncher: ActivityResultLauncher<String>
|
|
private lateinit var requestPermissionsLauncher: ActivityResultLauncher<Array<String>>
|
|
private lateinit var accountsRequest: ActivityResultLauncher<Intent>
|
|
private lateinit var chatRequests: ActivityResultLauncher<Intent>
|
|
private lateinit var configRequest: ActivityResultLauncher<Intent>
|
|
private lateinit var backupRequest: ActivityResultLauncher<Intent>
|
|
private lateinit var restoreRequest: ActivityResultLauncher<Intent>
|
|
private lateinit var logcatRequest: ActivityResultLauncher<Intent>
|
|
private lateinit var contactsRequest: ActivityResultLauncher<Intent>
|
|
private lateinit var callsRequest: ActivityResultLauncher<Intent>
|
|
private lateinit var accountRequest: ActivityResultLauncher<Intent>
|
|
private lateinit var comDevChangedListener: AudioManager.OnCommunicationDeviceChangedListener
|
|
private lateinit var permissions: Array<String>
|
|
|
|
private lateinit var baresipService: Intent
|
|
|
|
private var callHandler: Handler = Handler(Looper.getMainLooper())
|
|
private var callRunnable: Runnable? = null
|
|
private var downloadsInputUri: Uri? = null
|
|
private var downloadsOutputUri: Uri? = null
|
|
private var audioModeChangedListener: AudioManager.OnModeChangedListener? = null
|
|
private var keyboardController: SoftwareKeyboardController? = null
|
|
|
|
private var restart = false
|
|
private var atStartup = false
|
|
private var initialized = false
|
|
|
|
private var resumeUri = ""
|
|
private var resumeUap = 0L
|
|
private var resumeCall: Call? = null
|
|
private var resumeAction = ""
|
|
|
|
private val viewModel: ViewModel by viewModels()
|
|
|
|
private var callUri = mutableStateOf("")
|
|
private var callUriEnabled = mutableStateOf(true)
|
|
private var callUriLabel = mutableStateOf("")
|
|
private var securityIcon = mutableIntStateOf(-1)
|
|
private var callTimer: Chronometer? = null
|
|
private var showCallTimer = mutableStateOf(false)
|
|
private var showSuggestions = mutableStateOf(false)
|
|
private var showCallButton = mutableStateOf(true)
|
|
private var showCancelButton = mutableStateOf(false)
|
|
private var showAnswerRejectButtons = mutableStateOf(false)
|
|
private var showHangupButton = mutableStateOf(false)
|
|
private var showOnHoldNotice = mutableStateOf(false)
|
|
private var showPasswordDialog = mutableStateOf(false)
|
|
private var password = mutableStateOf("")
|
|
private var showPasswordsDialog = mutableStateOf(false)
|
|
private var holdIcon = mutableIntStateOf(R.drawable.call_hold)
|
|
private var transferButtonEnabled = mutableStateOf(false)
|
|
private var transferIcon = mutableIntStateOf(R.drawable.call_transfer)
|
|
private var dtmfText = mutableStateOf("")
|
|
private var dtmfEnabled = mutableStateOf(false)
|
|
private var focusDtmf = mutableStateOf(false)
|
|
private var showVmIcon by mutableStateOf(false)
|
|
private var vmIcon = mutableIntStateOf(R.drawable.voicemail)
|
|
private var messagesIcon = mutableIntStateOf(R.drawable.messages)
|
|
private var callsIcon = mutableIntStateOf(R.drawable.calls)
|
|
private var micIcon by mutableIntStateOf(R.drawable.mic_on)
|
|
private var speakerIcon by mutableIntStateOf(R.drawable.speaker_off)
|
|
private var dialpad by mutableStateOf(false)
|
|
private var dialpadButtonEnabled by mutableStateOf(true)
|
|
private var pullToRefreshEnabled by mutableStateOf(true)
|
|
private var passwordAccounts = mutableListOf<String>()
|
|
private var passwordTitle by mutableStateOf("")
|
|
|
|
private val alertTitle = mutableStateOf("")
|
|
private val alertMessage = mutableStateOf("")
|
|
private val showAlert = mutableStateOf(false)
|
|
|
|
private val dialogTitle = mutableStateOf("")
|
|
private val dialogMessage = mutableStateOf("")
|
|
private val positiveText = mutableStateOf("")
|
|
private val onPositiveClicked = mutableStateOf({})
|
|
private val negativeText = mutableStateOf("")
|
|
private val onNegativeClicked = mutableStateOf({})
|
|
private val showDialog = mutableStateOf(false)
|
|
|
|
private val showSelectItemDialog = mutableStateOf(false)
|
|
val items = mutableStateOf(listOf<String>())
|
|
private val itemAction = mutableStateOf<(Int) -> Unit>({ _ -> run {} })
|
|
|
|
private var backInvokedCallback: OnBackInvokedCallback? = null
|
|
private lateinit var onBackPressedCallback: OnBackPressedCallback
|
|
|
|
@RequiresApi(33)
|
|
private fun registerBackInvokedCallback() {
|
|
backInvokedCallback = OnBackInvokedCallback {
|
|
moveTaskToBack(true)
|
|
}
|
|
onBackInvokedDispatcher.registerOnBackInvokedCallback(
|
|
OnBackInvokedDispatcher.PRIORITY_DEFAULT,
|
|
backInvokedCallback!!
|
|
)
|
|
}
|
|
|
|
@SuppressLint("ClickableViewAccessibility")
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
|
|
super.onCreate(savedInstanceState)
|
|
|
|
enableEdgeToEdge()
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
|
registerBackInvokedCallback()
|
|
else {
|
|
onBackPressedCallback = object : OnBackPressedCallback(true) {
|
|
override fun handleOnBackPressed() {
|
|
moveTaskToBack(true)
|
|
}
|
|
}
|
|
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
|
|
}
|
|
|
|
val extraAction = intent.getStringExtra("action")
|
|
Log.d(TAG, "Main onCreate ${intent.action}/${intent.data}/$extraAction")
|
|
|
|
window.addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES)
|
|
|
|
BaresipService.darkTheme.value = Utils.isThemeDark(this)
|
|
|
|
// Must be done after view has been created
|
|
this.setShowWhenLocked(true)
|
|
this.setTurnScreenOn( true)
|
|
Utils.requestDismissKeyguard(this)
|
|
|
|
imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
|
nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
|
am = getSystemService(AUDIO_SERVICE) as AudioManager
|
|
kgm = getSystemService(KEYGUARD_SERVICE) as KeyguardManager
|
|
|
|
serviceEventObserver = Observer {
|
|
val event = it.getContentIfNotHandled()
|
|
Log.d(TAG, "Observed event $event")
|
|
if (event != null && BaresipService.serviceEvents.isNotEmpty()) {
|
|
val first = BaresipService.serviceEvents.removeAt(0)
|
|
handleServiceEvent(first.event, first.params)
|
|
}
|
|
}
|
|
|
|
BaresipService.serviceEvent.observeForever(serviceEventObserver)
|
|
|
|
screenEventReceiver = object : BroadcastReceiver() {
|
|
override fun onReceive(contxt: Context, intent: Intent) {
|
|
if (kgm.isKeyguardLocked) {
|
|
Log.d(TAG, "Screen on when locked")
|
|
this@MainActivity.setShowWhenLocked(Call.inCall())
|
|
}
|
|
}
|
|
}
|
|
|
|
this.registerReceiver(screenEventReceiver, IntentFilter().apply {
|
|
addAction(Intent.ACTION_SCREEN_ON)
|
|
})
|
|
|
|
if (Build.VERSION.SDK_INT >= 31) {
|
|
comDevChangedListener = AudioManager.OnCommunicationDeviceChangedListener { device ->
|
|
if (device != null) {
|
|
Log.d(TAG, "Com device changed to type ${device.type} in mode ${am.mode}")
|
|
speakerIcon = if (Utils.isSpeakerPhoneOn(am))
|
|
R.drawable.speaker_on
|
|
else
|
|
R.drawable.speaker_off
|
|
}
|
|
}
|
|
am.addOnCommunicationDeviceChangedListener(mainExecutor, comDevChangedListener)
|
|
}
|
|
|
|
initialized = true
|
|
|
|
accountsRequest = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
if (Account.ofAor(activityAor) != null)
|
|
spinToAor(activityAor)
|
|
else {
|
|
if (Account.ofAor(viewModel.selectedAor.value) == null) {
|
|
if (uas.value.isNotEmpty())
|
|
viewModel.updateSelectedAor(uas.value.first().account.aor)
|
|
else
|
|
viewModel.updateSelectedAor("")
|
|
}
|
|
updateIcons(Account.ofAor(viewModel.selectedAor.value))
|
|
}
|
|
if (BaresipService.isServiceRunning) {
|
|
baresipService.action = "Update Notification"
|
|
startService(baresipService)
|
|
}
|
|
}
|
|
|
|
accountRequest = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
spinToAor(activityAor)
|
|
val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
|
|
updateIcons(ua.account)
|
|
}
|
|
|
|
contactsRequest = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
}
|
|
|
|
chatRequests = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
spinToAor(activityAor)
|
|
updateIcons(Account.ofAor(activityAor)!!)
|
|
}
|
|
|
|
callsRequest = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
spinToAor(activityAor)
|
|
updateIcons(Account.ofAor(viewModel.selectedAor.value))
|
|
}
|
|
|
|
configRequest = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
if ((it.data != null) && it.data!!.hasExtra("restart")) {
|
|
dialogTitle.value = getString(R.string.restart_request)
|
|
dialogMessage.value = getString(R.string.config_restart)
|
|
positiveText.value = getString(R.string.restart)
|
|
onPositiveClicked.value = {
|
|
quitRestart(true)
|
|
}
|
|
negativeText.value = getString(R.string.cancel)
|
|
showDialog.value = true
|
|
}
|
|
val displayTheme = Preferences(applicationContext).displayTheme
|
|
if (displayTheme != AppCompatDelegate.getDefaultNightMode()) {
|
|
AppCompatDelegate.setDefaultNightMode(displayTheme)
|
|
}
|
|
}
|
|
|
|
backupRequest = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
if (it.resultCode == RESULT_OK)
|
|
it.data?.data?.also { uri ->
|
|
downloadsOutputUri = uri
|
|
passwordTitle = getString(R.string.encrypt_password)
|
|
showPasswordDialog.value = true
|
|
}
|
|
}
|
|
|
|
restoreRequest = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
if (it.resultCode == RESULT_OK)
|
|
it.data?.data?.also { uri ->
|
|
downloadsInputUri = uri
|
|
passwordTitle = getString(R.string.decrypt_password)
|
|
showPasswordDialog.value = true
|
|
}
|
|
}
|
|
|
|
logcatRequest = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
if (it.resultCode == RESULT_OK)
|
|
it.data?.data?.also { uri ->
|
|
try {
|
|
val out = contentResolver.openOutputStream(uri)
|
|
val process = Runtime.getRuntime().exec("logcat -d --pid=${Process.myPid()}")
|
|
val bufferedReader = process.inputStream.bufferedReader()
|
|
bufferedReader.forEachLine { line ->
|
|
out!!.write(line.toByteArray())
|
|
out.write('\n'.code.toByte().toInt())
|
|
}
|
|
out!!.close()
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Failed to write logcat to file: $e")
|
|
}
|
|
}
|
|
}
|
|
|
|
micIcon = if (BaresipService.isMicMuted)
|
|
R.drawable.mic_off
|
|
else
|
|
R.drawable.mic_on
|
|
|
|
setContent {
|
|
AppTheme {
|
|
keyboardController = LocalSoftwareKeyboardController.current
|
|
Surface(
|
|
modifier = Modifier.fillMaxSize(),
|
|
color = LocalCustomColors.current.background
|
|
) {
|
|
MainScreen(this)
|
|
}
|
|
}
|
|
}
|
|
|
|
baresipService = Intent(this@MainActivity, BaresipService::class.java)
|
|
|
|
atStartup = intent.hasExtra("onStartup")
|
|
|
|
when (intent?.action) {
|
|
ACTION_DIAL, ACTION_CALL, ACTION_VIEW ->
|
|
if (BaresipService.isServiceRunning)
|
|
callAction(intent.data, if (intent?.action == ACTION_CALL) "call" else "dial")
|
|
else
|
|
BaresipService.callActionUri = intent.data.toString()
|
|
.replace("tel:%2B", "tel:+")
|
|
}
|
|
|
|
permissions = if (Build.VERSION.SDK_INT >= 33)
|
|
arrayOf(POST_NOTIFICATIONS, RECORD_AUDIO, BLUETOOTH_CONNECT)
|
|
else if (Build.VERSION.SDK_INT >= 31)
|
|
arrayOf(RECORD_AUDIO, BLUETOOTH_CONNECT)
|
|
else
|
|
arrayOf(RECORD_AUDIO)
|
|
|
|
requestPermissionLauncher =
|
|
registerForActivityResult(ActivityResultContracts.RequestPermission()) {}
|
|
|
|
requestPermissionsLauncher =
|
|
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
|
|
val denied = mutableListOf<String>()
|
|
val shouldShow = mutableListOf<String>()
|
|
it.forEach { permission ->
|
|
if (!permission.value) {
|
|
denied.add(permission.key)
|
|
if (shouldShowRequestPermissionRationale(permission.key))
|
|
shouldShow.add(permission.key)
|
|
}
|
|
}
|
|
if (denied.contains(POST_NOTIFICATIONS) && !shouldShow.contains(POST_NOTIFICATIONS)) {
|
|
dialogTitle.value = getString(R.string.notice)
|
|
dialogMessage.value = getString(R.string.no_notifications)
|
|
positiveText.value = getString(R.string.ok)
|
|
onPositiveClicked.value = {
|
|
quitRestart(false)
|
|
}
|
|
negativeText.value = ""
|
|
showDialog.value = true
|
|
} else {
|
|
if (shouldShow.isNotEmpty()) {
|
|
dialogTitle.value = getString(R.string.permissions_rationale)
|
|
dialogMessage.value = getString(R.string.audio_permissions)
|
|
positiveText.value = getString(R.string.ok)
|
|
onPositiveClicked.value = {
|
|
requestPermissionsLauncher.launch(permissions)
|
|
}
|
|
negativeText.value = ""
|
|
showDialog.value = true
|
|
}
|
|
else
|
|
startBaresip()
|
|
}
|
|
}
|
|
|
|
if (!BaresipService.isServiceRunning) {
|
|
if (File(filesDir.absolutePath + "/accounts").exists()) {
|
|
passwordAccounts = String(
|
|
Utils.getFileContents(filesDir.absolutePath + "/accounts")!!,
|
|
Charsets.UTF_8
|
|
).lines().toMutableList()
|
|
showPasswordsDialog.value = true
|
|
} else {
|
|
// Baresip is started for the first time
|
|
requestPermissionsLauncher.launch(permissions)
|
|
}
|
|
}
|
|
|
|
} // OnCreate
|
|
|
|
@Composable
|
|
private fun MainScreen(ctx: Context) {
|
|
Scaffold(
|
|
modifier = Modifier.safeDrawingPadding(),
|
|
containerColor = LocalCustomColors.current.background,
|
|
topBar = { TopAppBar(ctx, getString(R.string.baresip)) },
|
|
bottomBar = { BottomBar(ctx) },
|
|
content = { contentPadding ->
|
|
MainContent(ctx, contentPadding)
|
|
}
|
|
)
|
|
}
|
|
|
|
@Composable
|
|
fun MainContent(ctx: Context, contentPadding: PaddingValues) {
|
|
|
|
var isRefreshing by remember { mutableStateOf(false) }
|
|
val refreshState = rememberPullToRefreshState()
|
|
var offset by remember { mutableFloatStateOf(0f) }
|
|
val swipeThreshold = 200
|
|
|
|
LaunchedEffect(isRefreshing) {
|
|
if (isRefreshing) {
|
|
delay(1000)
|
|
isRefreshing = false
|
|
}
|
|
}
|
|
|
|
if (showAlert.value)
|
|
AlertDialog(
|
|
showDialog = showAlert,
|
|
title = alertTitle.value,
|
|
message = alertMessage.value,
|
|
positiveButtonText = stringResource(R.string.ok),
|
|
)
|
|
|
|
if (showDialog.value)
|
|
AlertDialog(
|
|
showDialog = showDialog,
|
|
title = stringResource(R.string.confirmation),
|
|
message = dialogMessage.value,
|
|
positiveButtonText = positiveText.value,
|
|
onPositiveClicked = onPositiveClicked.value,
|
|
negativeButtonText = stringResource(R.string.cancel),
|
|
onNegativeClicked = onNegativeClicked.value
|
|
)
|
|
|
|
SelectableAlertDialog(
|
|
openDialog = showSelectItemDialog,
|
|
title = stringResource(R.string.choose_destination_uri),
|
|
items = items.value,
|
|
onItemClicked = itemAction.value,
|
|
neutralButtonText = stringResource(R.string.cancel),
|
|
onNeutralClicked = {}
|
|
)
|
|
|
|
Column(
|
|
modifier = Modifier
|
|
.imePadding()
|
|
.fillMaxWidth()
|
|
.padding(contentPadding)
|
|
.padding(top = 18.dp, bottom = 6.dp, start = 16.dp, end = 16.dp)
|
|
.fillMaxSize()
|
|
.pullToRefresh(
|
|
state = refreshState,
|
|
isRefreshing = isRefreshing,
|
|
onRefresh = {
|
|
isRefreshing = true
|
|
if (uas.value.isNotEmpty()) {
|
|
if (viewModel.selectedAor.value == "")
|
|
spinToAor(uas.value.first().account.aor)
|
|
val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
|
|
if (ua.account.regint > 0)
|
|
Api.ua_register(ua.uap)
|
|
}
|
|
},
|
|
enabled = pullToRefreshEnabled,
|
|
)
|
|
.pointerInput(Unit) {
|
|
detectHorizontalDragGestures(
|
|
onDragStart = { offset = 0f },
|
|
onDragEnd = {
|
|
if (offset < -swipeThreshold) {
|
|
if (uas.value.isNotEmpty()) {
|
|
val curPos = UserAgent.findAorIndex(viewModel.selectedAor.value)
|
|
val newPos = if (curPos == null)
|
|
0
|
|
else
|
|
(curPos + 1) % uas.value.size
|
|
if (curPos != newPos) {
|
|
val ua = uas.value[newPos]
|
|
spinToAor(ua.account.aor)
|
|
showCall(ua)
|
|
}
|
|
}
|
|
} else if (offset > swipeThreshold) {
|
|
if (uas.value.isNotEmpty()) {
|
|
val curPos = UserAgent.findAorIndex(viewModel.selectedAor.value)
|
|
val newPos = when (curPos) {
|
|
null -> 0
|
|
0 -> uas.value.size - 1
|
|
else -> curPos - 1
|
|
}
|
|
if (curPos != newPos) {
|
|
val ua = uas.value[newPos]
|
|
spinToAor(ua.account.aor)
|
|
showCall(ua)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
) { _, dragAmount ->
|
|
offset += dragAmount
|
|
}
|
|
}
|
|
.verticalScroll(rememberScrollState()),
|
|
verticalArrangement = Arrangement.Top,
|
|
horizontalAlignment = Alignment.CenterHorizontally,
|
|
) {
|
|
AccountSpinner(ctx)
|
|
CallUriRow()
|
|
CallRow(ctx)
|
|
OnHoldNotice()
|
|
AskPasswords(ctx)
|
|
AskPassword(ctx)
|
|
Indicator(
|
|
modifier = Modifier.align(Alignment.CenterHorizontally),
|
|
isRefreshing = isRefreshing,
|
|
state = refreshState,
|
|
)
|
|
}
|
|
}
|
|
|
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
@Composable
|
|
fun TopAppBar(ctx: Context, title: String) {
|
|
|
|
val recOffImage = ImageVector.vectorResource(R.drawable.rec_off)
|
|
val recOnImage = ImageVector.vectorResource(R.drawable.rec_on)
|
|
|
|
var recImage by remember { mutableStateOf(recOffImage) }
|
|
var menuExpanded by remember { mutableStateOf(false) }
|
|
|
|
val about = stringResource(R.string.about)
|
|
val settings = stringResource(R.string.configuration)
|
|
val accounts = stringResource(R.string.accounts)
|
|
val backup = stringResource(R.string.backup)
|
|
val restore = stringResource(R.string.restore)
|
|
val logcat = stringResource(R.string.logcat)
|
|
val restart = stringResource(R.string.restart)
|
|
val quit = stringResource(R.string.quit)
|
|
|
|
TopAppBar(
|
|
title = {
|
|
Text(
|
|
text = title,
|
|
color = LocalCustomColors.current.light,
|
|
fontSize = 22.sp,
|
|
fontWeight = FontWeight.Bold
|
|
)
|
|
},
|
|
colors = TopAppBarDefaults.mediumTopAppBarColors(
|
|
containerColor = LocalCustomColors.current.primary
|
|
),
|
|
actions = {
|
|
|
|
Icon(
|
|
imageVector = recImage,
|
|
modifier = Modifier.size(40.dp).combinedClickable(
|
|
onClick = {
|
|
if (Call.call("connected") == null) {
|
|
BaresipService.isRecOn = !BaresipService.isRecOn
|
|
recImage = if (BaresipService.isRecOn) {
|
|
Api.module_load("sndfile")
|
|
recOnImage
|
|
} else {
|
|
Api.module_unload("sndfile")
|
|
recOffImage
|
|
}
|
|
} else
|
|
Toast.makeText(ctx, R.string.rec_in_call, Toast.LENGTH_SHORT)
|
|
.show()
|
|
},
|
|
onLongClick = {
|
|
alertTitle.value = getString(R.string.call_recording_title)
|
|
alertMessage.value = getString(R.string.call_recording_tip)
|
|
showAlert.value = true
|
|
}
|
|
),
|
|
tint = Color.Unspecified,
|
|
contentDescription = null
|
|
)
|
|
|
|
Spacer(modifier = Modifier.width(22.dp))
|
|
|
|
Icon(
|
|
imageVector = ImageVector.vectorResource(micIcon),
|
|
modifier = Modifier
|
|
.size(40.dp)
|
|
.combinedClickable(
|
|
onClick = {
|
|
if (Call.call("connected") != null) {
|
|
BaresipService.isMicMuted = !BaresipService.isMicMuted
|
|
if (BaresipService.isMicMuted) {
|
|
micIcon = R.drawable.mic_off
|
|
Api.calls_mute(true)
|
|
} else {
|
|
micIcon = R.drawable.mic_on
|
|
Api.calls_mute(false)
|
|
}
|
|
}
|
|
},
|
|
onLongClick = {
|
|
alertTitle.value = getString(R.string.microphone_title)
|
|
alertMessage.value = getString(R.string.microphone_tip)
|
|
showAlert.value = true
|
|
},
|
|
),
|
|
tint = Color.Unspecified,
|
|
contentDescription = null
|
|
)
|
|
|
|
Spacer(modifier = Modifier.width(16.dp))
|
|
|
|
Icon(
|
|
imageVector = ImageVector.vectorResource(speakerIcon),
|
|
modifier = Modifier
|
|
.size(40.dp)
|
|
.combinedClickable(
|
|
onClick = {
|
|
if (Build.VERSION.SDK_INT >= 31)
|
|
Log.d(
|
|
TAG, "Toggling speakerphone when dev/mode is " +
|
|
"${am.communicationDevice!!.type}/${am.mode}"
|
|
)
|
|
Utils.toggleSpeakerPhone(ContextCompat.getMainExecutor(ctx), am)
|
|
speakerIcon = if (Utils.isSpeakerPhoneOn(am))
|
|
R.drawable.speaker_on
|
|
else
|
|
R.drawable.speaker_off
|
|
},
|
|
onLongClick = {
|
|
alertTitle.value = getString(R.string.speakerphone_title)
|
|
alertMessage.value = getString(R.string.speakerphone_tip)
|
|
showAlert.value = true
|
|
},
|
|
),
|
|
tint = Color.Unspecified,
|
|
contentDescription = null
|
|
)
|
|
|
|
Spacer(modifier = Modifier.width(8.dp))
|
|
|
|
IconButton(
|
|
onClick = { menuExpanded = !menuExpanded }
|
|
) {
|
|
Icon(
|
|
imageVector = Icons.Filled.Menu,
|
|
contentDescription = "Menu",
|
|
tint = LocalCustomColors.current.light
|
|
)
|
|
}
|
|
|
|
DropdownMenu(
|
|
expanded = menuExpanded,
|
|
onDismissRequest = { menuExpanded = false },
|
|
items = if (Build.VERSION.SDK_INT >= 29)
|
|
listOf(about, settings, accounts, backup, restore, logcat, restart, quit)
|
|
else
|
|
listOf(about, settings, accounts, backup, restore, restart, quit),
|
|
onItemClick = { selectedItem ->
|
|
menuExpanded = false
|
|
when (selectedItem) {
|
|
about -> {
|
|
startActivity(Intent(ctx, AboutActivity::class.java))
|
|
}
|
|
settings -> {
|
|
configRequest.launch(Intent(ctx, ConfigActivity::class.java))
|
|
}
|
|
accounts -> {
|
|
val i = Intent(ctx, AccountsActivity::class.java)
|
|
val b = Bundle()
|
|
b.putString("aor", viewModel.selectedAor.value)
|
|
i.putExtras(b)
|
|
accountsRequest.launch(i)
|
|
}
|
|
backup -> {
|
|
when {
|
|
Build.VERSION.SDK_INT >= 29 ->
|
|
pickupFileFromDownloads("backup")
|
|
ContextCompat.checkSelfPermission(ctx, WRITE_EXTERNAL_STORAGE) ==
|
|
PackageManager.PERMISSION_GRANTED -> {
|
|
Log.d(TAG, "Write External Storage permission granted")
|
|
val path = Utils.downloadsPath("baresip.bs")
|
|
downloadsOutputUri = File(path).toUri()
|
|
passwordTitle = getString(R.string.encrypt_password)
|
|
showPasswordDialog.value = true
|
|
}
|
|
shouldShowRequestPermissionRationale(WRITE_EXTERNAL_STORAGE) -> {
|
|
dialogTitle.value = getString(R.string.notice)
|
|
dialogMessage.value = getString(R.string.no_backup)
|
|
positiveText.value = getString(R.string.ok)
|
|
onPositiveClicked.value = {
|
|
requestPermissionLauncher.launch(WRITE_EXTERNAL_STORAGE)
|
|
}
|
|
negativeText.value = ""
|
|
showDialog.value = true
|
|
}
|
|
else ->
|
|
requestPermissionLauncher.launch(WRITE_EXTERNAL_STORAGE)
|
|
}
|
|
}
|
|
restore -> {
|
|
when {
|
|
Build.VERSION.SDK_INT >= 29 ->
|
|
pickupFileFromDownloads("restore")
|
|
ContextCompat.checkSelfPermission(ctx, READ_EXTERNAL_STORAGE) ==
|
|
PackageManager.PERMISSION_GRANTED -> {
|
|
Log.d(TAG, "Read External Storage permission granted")
|
|
val path = Utils.downloadsPath("baresip.bs")
|
|
downloadsInputUri = File(path).toUri()
|
|
passwordTitle = getString(R.string.decrypt_password)
|
|
showPasswordDialog.value = true
|
|
}
|
|
shouldShowRequestPermissionRationale(READ_EXTERNAL_STORAGE) -> {
|
|
dialogTitle.value = getString(R.string.notice)
|
|
dialogMessage.value = getString(R.string.no_restore)
|
|
positiveText.value = getString(R.string.ok)
|
|
onPositiveClicked.value = {
|
|
requestPermissionLauncher.launch(READ_EXTERNAL_STORAGE)
|
|
}
|
|
negativeText.value = ""
|
|
showDialog.value = true
|
|
}
|
|
else ->
|
|
requestPermissionLauncher.launch(READ_EXTERNAL_STORAGE)
|
|
}
|
|
}
|
|
logcat -> {
|
|
if (Build.VERSION.SDK_INT >= 29)
|
|
pickupFileFromDownloads("logcat")
|
|
}
|
|
restart -> {
|
|
quitRestart(true)
|
|
}
|
|
quit -> {
|
|
quitRestart(false)
|
|
}
|
|
}
|
|
}
|
|
)
|
|
}
|
|
)
|
|
}
|
|
|
|
@OptIn(ExperimentalFoundationApi::class)
|
|
@Composable
|
|
fun AccountSpinner(ctx: Context) {
|
|
|
|
var expanded by rememberSaveable { mutableStateOf(false) }
|
|
val selected: String by viewModel.selectedAor.collectAsState()
|
|
|
|
if (uas.value.isEmpty())
|
|
viewModel.updateSelectedAor("")
|
|
else
|
|
if (selected == "" || UserAgent.ofAor(selected) == null) {
|
|
viewModel.updateSelectedAor(uas.value.first().account.aor)
|
|
}
|
|
|
|
showCall(UserAgent.ofAor(selected))
|
|
updateIcons(Account.ofAor(selected))
|
|
|
|
if (selected == "") {
|
|
OutlinedButton(
|
|
onClick = {
|
|
val i = Intent(this@MainActivity, AccountsActivity::class.java)
|
|
val b = Bundle()
|
|
b.putString("aor", selected)
|
|
i.putExtras(b)
|
|
accountsRequest.launch(i)
|
|
},
|
|
modifier = Modifier
|
|
.padding(horizontal = 4.dp)
|
|
.height(50.dp)
|
|
.fillMaxWidth(),
|
|
colors = ButtonColors(
|
|
containerColor = LocalCustomColors.current.grayLight,
|
|
contentColor = LocalCustomColors.current.dark,
|
|
disabledContainerColor = LocalCustomColors.current.grayLight,
|
|
disabledContentColor = LocalCustomColors.current.dark
|
|
),
|
|
shape = RoundedCornerShape(12.dp),
|
|
contentPadding = PaddingValues(horizontal = 10.dp)
|
|
) {
|
|
Text(text = "")
|
|
}
|
|
}
|
|
else
|
|
OutlinedButton(
|
|
onClick = {
|
|
expanded = !expanded
|
|
},
|
|
enabled = true,
|
|
modifier = Modifier
|
|
.padding(horizontal = 4.dp)
|
|
.height(50.dp)
|
|
.pointerInput(Unit) {
|
|
detectTapGestures(
|
|
onPress = {
|
|
expanded = true
|
|
},
|
|
onLongPress = {
|
|
val ua = UserAgent.ofAor(selected)
|
|
if (ua != null) {
|
|
val acc = ua.account
|
|
if (Api.account_regint(acc.accp) > 0) {
|
|
Api.account_set_regint(acc.accp, 0)
|
|
Api.ua_unregister(ua.uap)
|
|
} else {
|
|
Api.account_set_regint(
|
|
acc.accp,
|
|
acc.configuredRegInt
|
|
)
|
|
Api.ua_register(ua.uap)
|
|
}
|
|
acc.regint = Api.account_regint(acc.accp)
|
|
AccountsActivity.saveAccounts()
|
|
}
|
|
}
|
|
)
|
|
},
|
|
colors = ButtonColors(
|
|
containerColor = LocalCustomColors.current.grayLight,
|
|
contentColor = LocalCustomColors.current.dark,
|
|
disabledContainerColor = LocalCustomColors.current.grayLight,
|
|
disabledContentColor = LocalCustomColors.current.dark
|
|
),
|
|
shape = RoundedCornerShape(12.dp),
|
|
contentPadding = PaddingValues(horizontal = 10.dp)
|
|
) {
|
|
Icon(
|
|
imageVector = ImageVector.vectorResource(
|
|
uasStatus.value[selected] ?:
|
|
R.drawable.locked_yellow),
|
|
contentDescription = null,
|
|
tint = Color.Unspecified,
|
|
modifier = Modifier
|
|
.padding(end = 10.dp)
|
|
.clickable(onClick = {
|
|
val i = Intent(ctx, AccountActivity::class.java)
|
|
val b = Bundle()
|
|
b.putString("aor", selected)
|
|
i.putExtras(b)
|
|
accountRequest.launch(i)
|
|
})
|
|
)
|
|
Text(
|
|
text = Account.ofAor(selected)?.text() ?: "",
|
|
fontSize = 17.sp,
|
|
fontWeight = FontWeight.Bold,
|
|
overflow = TextOverflow.Ellipsis,
|
|
maxLines = 1,
|
|
modifier = Modifier
|
|
.weight(1f)
|
|
.combinedClickable(
|
|
onClick = { expanded = true },
|
|
onLongClick = {
|
|
val ua = UserAgent.ofAor(selected)
|
|
if (ua != null) {
|
|
val acc = ua.account
|
|
if (Api.account_regint(acc.accp) > 0) {
|
|
Api.account_set_regint(acc.accp, 0)
|
|
Api.ua_unregister(ua.uap)
|
|
} else {
|
|
Api.account_set_regint(
|
|
acc.accp,
|
|
acc.configuredRegInt
|
|
)
|
|
Api.ua_register(ua.uap)
|
|
}
|
|
acc.regint = Api.account_regint(acc.accp)
|
|
AccountsActivity.saveAccounts()
|
|
}
|
|
}
|
|
)
|
|
)
|
|
Icon(
|
|
imageVector = if (expanded)
|
|
Icons.Default.KeyboardArrowUp
|
|
else
|
|
Icons.Default.KeyboardArrowDown,
|
|
contentDescription = null
|
|
)
|
|
androidx.compose.material3.DropdownMenu(
|
|
expanded = expanded,
|
|
onDismissRequest = { expanded = false },
|
|
) {
|
|
uas.value.forEachIndexed { _, ua ->
|
|
val acc = ua.account
|
|
DropdownMenuItem(
|
|
onClick = {
|
|
expanded = false
|
|
run {
|
|
viewModel.updateSelectedAor(acc.aor)
|
|
}
|
|
showCall(ua)
|
|
updateIcons(acc)
|
|
},
|
|
text = { Text(
|
|
text = acc.text(),
|
|
fontSize = 17.sp,
|
|
fontWeight = FontWeight.Bold
|
|
) },
|
|
leadingIcon = {
|
|
Icon(
|
|
imageVector = ImageVector.vectorResource(uasStatus.value[acc.aor]!!),
|
|
contentDescription = null,
|
|
tint = Color.Unspecified,
|
|
)
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun CallUriRow() {
|
|
val suggestions by remember { contactNames }
|
|
var filteredSuggestions by remember { mutableStateOf(suggestions) }
|
|
val focusRequester = remember { FocusRequester() }
|
|
val lazyListState = rememberLazyListState()
|
|
|
|
Row(modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(top = 4.dp, bottom = 8.dp),
|
|
verticalAlignment = Alignment.CenterVertically) {
|
|
Column(
|
|
modifier = Modifier.weight(1f),
|
|
horizontalAlignment = Alignment.CenterHorizontally
|
|
) {
|
|
OutlinedTextField(
|
|
value = callUri.value,
|
|
enabled = callUriEnabled.value,
|
|
singleLine = true,
|
|
colors = OutlinedTextFieldDefaults.colors(
|
|
disabledBorderColor = OutlinedTextFieldDefaults.colors().unfocusedIndicatorColor),
|
|
onValueChange = {
|
|
if (it != callUri.value) {
|
|
callUri.value = it
|
|
filteredSuggestions = suggestions.filter { suggestion ->
|
|
it.length > 2 && suggestion.startsWith(it, ignoreCase = true)
|
|
}
|
|
showSuggestions.value = it.length > 2
|
|
}
|
|
},
|
|
trailingIcon = {
|
|
if (callUriEnabled.value && callUri.value.isNotEmpty())
|
|
Icon(Icons.Outlined.Clear,
|
|
contentDescription = null,
|
|
modifier = Modifier
|
|
.clickable {
|
|
if (showSuggestions.value)
|
|
showSuggestions.value = false
|
|
else
|
|
callUri.value = ""
|
|
}
|
|
)
|
|
},
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(start = 4.dp, end = 4.dp, top = 12.dp, bottom = 2.dp)
|
|
.focusRequester(focusRequester)
|
|
.onFocusChanged {
|
|
val account = Account.ofAor(viewModel.selectedAor.value)
|
|
if (account != null) {
|
|
dialpad = account.numericKeypad
|
|
}
|
|
},
|
|
label = {
|
|
LabelText(
|
|
text = callUriLabel.value,
|
|
fontSize = 18.sp,
|
|
)
|
|
},
|
|
textStyle = TextStyle(
|
|
fontSize = 18.sp,
|
|
color = LocalCustomColors.current.itemText
|
|
),
|
|
keyboardOptions = if (dialpad)
|
|
KeyboardOptions(keyboardType = KeyboardType.Phone)
|
|
else
|
|
KeyboardOptions(keyboardType = KeyboardType.Text)
|
|
)
|
|
Spacer(modifier = Modifier.height(8.dp))
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.shadow(8.dp, RoundedCornerShape(8.dp))
|
|
.background(
|
|
LocalCustomColors.current.grayLight,
|
|
shape = RoundedCornerShape(8.dp)
|
|
)
|
|
.animateContentSize()
|
|
) {
|
|
if (showSuggestions.value && filteredSuggestions.isNotEmpty()) {
|
|
Box(modifier = Modifier
|
|
.fillMaxWidth()
|
|
.heightIn(max = 150.dp)) {
|
|
LazyColumn(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.verticalScrollbar(
|
|
state = lazyListState,
|
|
color = LocalCustomColors.current.gray
|
|
),
|
|
horizontalAlignment = Alignment.Start,
|
|
state = lazyListState
|
|
) {
|
|
items(
|
|
items = filteredSuggestions,
|
|
key = { suggestion -> suggestion }
|
|
) { suggestion ->
|
|
Box(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.clickable {
|
|
callUri.value = suggestion
|
|
showSuggestions.value = false
|
|
}
|
|
.padding(12.dp)
|
|
) {
|
|
Text(
|
|
text = suggestion,
|
|
modifier = Modifier.fillMaxWidth(),
|
|
color = LocalCustomColors.current.grayDark,
|
|
fontSize = 18.sp
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (showCallTimer.value) {
|
|
val textColor = LocalCustomColors.current.itemText.toArgb()
|
|
AndroidView(
|
|
factory = { context ->
|
|
Chronometer(context).also { callTimer = it
|
|
callTimer?.textSize = 16F
|
|
callTimer?.setTextColor(textColor)
|
|
}
|
|
},
|
|
modifier = Modifier.padding(start = 6.dp,
|
|
top = 4.dp,
|
|
end = if (securityIcon.intValue != -1) 6.dp else 0.dp),
|
|
)
|
|
}
|
|
if (securityIcon.intValue != -1) {
|
|
Icon(
|
|
imageVector = ImageVector.vectorResource(securityIcon.intValue),
|
|
contentDescription = null,
|
|
modifier = Modifier
|
|
.size(28.dp)
|
|
.padding(top = 4.dp)
|
|
.clickable {
|
|
when (securityIcon.intValue) {
|
|
R.drawable.unlocked -> {
|
|
alertTitle.value = getString(R.string.alert)
|
|
alertMessage.value = getString(R.string.call_not_secure)
|
|
showAlert.value = true
|
|
}
|
|
|
|
R.drawable.locked_yellow -> {
|
|
alertTitle.value = getString(R.string.alert)
|
|
alertMessage.value = getString(R.string.peer_not_verified)
|
|
showAlert.value = true
|
|
}
|
|
|
|
R.drawable.locked_green -> {
|
|
dialogTitle.value = getString(R.string.info)
|
|
dialogMessage.value = getString(R.string.call_is_secure)
|
|
positiveText.value = getString(R.string.unverify)
|
|
onPositiveClicked.value = {
|
|
val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
|
|
val call = ua.currentCall()
|
|
if (call != null) {
|
|
if (Api.cmd_exec("zrtp_unverify " + call.zid) != 0)
|
|
Log.e(
|
|
TAG,
|
|
"Command 'zrtp_unverify ${call.zid}' failed"
|
|
)
|
|
else
|
|
securityIcon.intValue = R.drawable.locked_yellow
|
|
}
|
|
}
|
|
negativeText.value = getString(R.string.cancel)
|
|
showDialog.value = true
|
|
}
|
|
}
|
|
},
|
|
tint = Color.Unspecified,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun CallRow(ctx: Context) {
|
|
|
|
Row( modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(start = 6.dp),
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
horizontalArrangement = Arrangement.Absolute.SpaceBetween
|
|
) {
|
|
if (showCallButton.value)
|
|
IconButton(
|
|
modifier = Modifier.size(48.dp),
|
|
onClick = {
|
|
showSuggestions.value = false
|
|
callClick(ctx)
|
|
},
|
|
) {
|
|
Icon(
|
|
imageVector = ImageVector.vectorResource(id = R.drawable.call),
|
|
modifier = Modifier.size(48.dp),
|
|
tint = Color.Unspecified,
|
|
contentDescription = null,
|
|
)
|
|
}
|
|
|
|
if (showCancelButton.value) {
|
|
Spacer(modifier = Modifier.weight(1f))
|
|
IconButton(
|
|
modifier = Modifier.size(48.dp),
|
|
onClick = {
|
|
showSuggestions.value = false
|
|
abandonAudioFocus()
|
|
var ua: UserAgent = userAgentOfSelectedAor()
|
|
val call = ua.currentCall()
|
|
if (call != null) {
|
|
val callp = call.callp
|
|
Log.d(
|
|
TAG,
|
|
"AoR ${ua.account.aor} hanging up call $callp with ${callUri.value}"
|
|
)
|
|
Api.ua_hangup(ua.uap, callp, 0, "")
|
|
}
|
|
},
|
|
) {
|
|
Icon(
|
|
imageVector = ImageVector.vectorResource(id = R.drawable.hangup),
|
|
modifier = Modifier.size(48.dp),
|
|
tint = Color.Unspecified,
|
|
contentDescription = null,
|
|
)
|
|
}
|
|
Spacer(modifier = Modifier.width(12.dp))
|
|
}
|
|
|
|
if (showHangupButton.value) {
|
|
|
|
var ua: UserAgent = userAgentOfSelectedAor()
|
|
|
|
IconButton(
|
|
modifier = Modifier.size(48.dp),
|
|
onClick = {
|
|
abandonAudioFocus()
|
|
val uaCalls = ua.calls()
|
|
if (uaCalls.isNotEmpty()) {
|
|
val call = uaCalls.last()
|
|
val callp = call.callp
|
|
Log.d(TAG, "AoR ${ua.account.aor} hanging up call $callp with ${callUri.value}")
|
|
Api.ua_hangup(ua.uap, callp, 0, "")
|
|
}
|
|
}
|
|
) {
|
|
Icon(
|
|
imageVector = ImageVector.vectorResource(id = R.drawable.hangup),
|
|
modifier = Modifier.size(48.dp),
|
|
tint = Color.Unspecified,
|
|
contentDescription = null,
|
|
)
|
|
}
|
|
|
|
IconButton(
|
|
modifier = Modifier.size(48.dp),
|
|
onClick = {
|
|
val aor = ua.account.aor
|
|
val call = ua.currentCall()
|
|
if (call != null) {
|
|
if (call.onhold) {
|
|
Log.d(
|
|
TAG,
|
|
"AoR $aor resuming call ${call.callp} with ${callUri.value}"
|
|
)
|
|
call.resume()
|
|
call.onhold = false
|
|
holdIcon.intValue = R.drawable.call_hold
|
|
} else {
|
|
Log.d(
|
|
TAG,
|
|
"AoR $aor holding call ${call.callp} with ${callUri.value}"
|
|
)
|
|
call.hold()
|
|
call.onhold = true
|
|
holdIcon.intValue = R.drawable.resume
|
|
}
|
|
}
|
|
},
|
|
) {
|
|
Icon(
|
|
imageVector = ImageVector.vectorResource(id = holdIcon.intValue),
|
|
modifier = Modifier.size(48.dp),
|
|
tint = Color.Unspecified,
|
|
contentDescription = null,
|
|
)
|
|
}
|
|
|
|
var showTransferDialog by remember { mutableStateOf(false) }
|
|
IconButton(
|
|
modifier = Modifier.size(48.dp),
|
|
enabled = transferButtonEnabled.value,
|
|
onClick = {
|
|
val call = ua.currentCall()
|
|
if (call != null) {
|
|
if (call.onHoldCall != null) {
|
|
if (!call.executeTransfer()) {
|
|
alertTitle.value = getString(R.string.notice)
|
|
alertMessage.value = getString(R.string.transfer_failed)
|
|
showAlert.value = true
|
|
}
|
|
} else
|
|
showTransferDialog = true
|
|
}
|
|
},
|
|
) {
|
|
Icon(
|
|
imageVector = ImageVector.vectorResource(transferIcon.intValue),
|
|
modifier = Modifier.size(48.dp),
|
|
tint = Color.Unspecified,
|
|
contentDescription = null,
|
|
)
|
|
}
|
|
|
|
if (showTransferDialog) {
|
|
val showDialog = remember { mutableStateOf(true) }
|
|
val blindChecked = remember { mutableStateOf(true) }
|
|
if (showDialog.value)
|
|
BasicAlertDialog(
|
|
onDismissRequest = {
|
|
keyboardController?.hide()
|
|
showDialog.value = false
|
|
showTransferDialog = false
|
|
}
|
|
) {
|
|
Card(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(top = 16.dp, start = 16.dp, end = 16.dp, bottom = 0.dp),
|
|
shape = RoundedCornerShape(16.dp),
|
|
colors = CardDefaults.cardColors(
|
|
containerColor = LocalCustomColors.current.cardBackground
|
|
)
|
|
) {
|
|
Column(modifier = Modifier.padding(16.dp)) {
|
|
Text(
|
|
text = stringResource(R.string.call_transfer),
|
|
fontSize = 20.sp,
|
|
color = LocalCustomColors.current.alert,
|
|
)
|
|
var transferUri by remember { mutableStateOf("") }
|
|
val suggestions by remember { contactNames }
|
|
var filteredSuggestions by remember { mutableStateOf(suggestions) }
|
|
val focusRequester = remember { FocusRequester() }
|
|
val lazyListState = rememberLazyListState()
|
|
OutlinedTextField(
|
|
value = transferUri,
|
|
singleLine = true,
|
|
onValueChange = {
|
|
if (it != transferUri) {
|
|
transferUri = it
|
|
filteredSuggestions =
|
|
suggestions.filter { suggestion ->
|
|
transferUri.length > 2 &&
|
|
suggestion.startsWith(
|
|
transferUri,
|
|
ignoreCase = true
|
|
)
|
|
}
|
|
showSuggestions.value = transferUri.length > 2
|
|
}
|
|
},
|
|
trailingIcon = {
|
|
if (transferUri.isNotEmpty())
|
|
Icon(
|
|
Icons.Outlined.Clear,
|
|
contentDescription = null,
|
|
modifier = Modifier.clickable {
|
|
if (showSuggestions.value)
|
|
showSuggestions.value = false
|
|
else
|
|
transferUri = ""
|
|
}
|
|
)
|
|
},
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(
|
|
start = 4.dp,
|
|
end = 4.dp,
|
|
top = 12.dp,
|
|
bottom = 2.dp
|
|
)
|
|
.focusRequester(focusRequester),
|
|
label = { LabelText(stringResource(R.string.transfer_destination)) },
|
|
textStyle = TextStyle(
|
|
fontSize = 18.sp,
|
|
color = LocalCustomColors.current.itemText
|
|
),
|
|
keyboardOptions = if (dialpad)
|
|
KeyboardOptions(keyboardType = KeyboardType.Phone)
|
|
else
|
|
KeyboardOptions(keyboardType = KeyboardType.Text)
|
|
)
|
|
Spacer(modifier = Modifier.height(8.dp))
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.shadow(8.dp, RoundedCornerShape(8.dp))
|
|
.background(
|
|
LocalCustomColors.current.grayLight,
|
|
shape = RoundedCornerShape(8.dp)
|
|
)
|
|
.animateContentSize()
|
|
) {
|
|
if (showSuggestions.value && filteredSuggestions.isNotEmpty()) {
|
|
Box(modifier = Modifier
|
|
.fillMaxWidth()
|
|
.heightIn(max = 150.dp)) {
|
|
LazyColumn(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.verticalScrollbar(
|
|
state = lazyListState,
|
|
color = LocalCustomColors.current.gray
|
|
),
|
|
horizontalAlignment = Alignment.Start,
|
|
state = lazyListState,
|
|
) {
|
|
items(
|
|
items = filteredSuggestions,
|
|
key = { suggestion -> suggestion }
|
|
) { suggestion ->
|
|
Box(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.clickable {
|
|
transferUri = suggestion
|
|
showSuggestions.value = false
|
|
}
|
|
.padding(12.dp)
|
|
) {
|
|
Text(
|
|
text = suggestion,
|
|
modifier = Modifier.fillMaxWidth(),
|
|
color = LocalCustomColors.current.grayDark,
|
|
fontSize = 18.sp
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
val call = ua.currentCall()
|
|
if (call != null && call.replaces())
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.Start,
|
|
) {
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
Text(
|
|
text = stringResource(R.string.blind),
|
|
color = LocalCustomColors.current.alert,
|
|
modifier = Modifier.padding(8.dp),
|
|
)
|
|
Switch(
|
|
checked = blindChecked.value,
|
|
onCheckedChange = {
|
|
blindChecked.value = true
|
|
}
|
|
)
|
|
}
|
|
Spacer(modifier = Modifier.width(8.dp))
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
Text(
|
|
text = stringResource(R.string.attended),
|
|
color = LocalCustomColors.current.alert,
|
|
modifier = Modifier.padding(8.dp),
|
|
)
|
|
Switch(
|
|
checked = !blindChecked.value,
|
|
onCheckedChange = {
|
|
blindChecked.value = false
|
|
}
|
|
)
|
|
}
|
|
}
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.End,
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
TextButton(
|
|
onClick = {
|
|
keyboardController?.hide()
|
|
showDialog.value = false
|
|
showTransferDialog = false
|
|
},
|
|
modifier = Modifier.padding(end = 32.dp),
|
|
) {
|
|
Text(
|
|
text = stringResource(R.string.cancel),
|
|
color = LocalCustomColors.current.gray
|
|
)
|
|
}
|
|
TextButton(
|
|
onClick = {
|
|
showSuggestions.value = false
|
|
var uriText = transferUri.trim()
|
|
if (uriText.isNotEmpty()) {
|
|
val uris = Contact.contactUris(uriText)
|
|
if (uris.size > 1) {
|
|
items.value = uris
|
|
itemAction.value = { index ->
|
|
val uri = uris[index]
|
|
transfer(
|
|
ua,
|
|
if (Utils.isTelNumber(uri)) "tel:$uri" else uri,
|
|
!blindChecked.value
|
|
)
|
|
showSelectItemDialog.value = false
|
|
}
|
|
showSelectItemDialog.value = true
|
|
}
|
|
else {
|
|
if (uris.size == 1) uriText = uris[0]
|
|
transfer(
|
|
ua,
|
|
if (Utils.isTelNumber(uriText)) "tel:$uriText" else uriText,
|
|
!blindChecked.value
|
|
)
|
|
}
|
|
keyboardController?.hide()
|
|
showDialog.value = false
|
|
showTransferDialog = false
|
|
}
|
|
},
|
|
modifier = Modifier.padding(end = 16.dp),
|
|
) {
|
|
Text(
|
|
text = stringResource(
|
|
if (blindChecked.value)
|
|
R.string.transfer
|
|
else
|
|
R.string.call
|
|
).uppercase(),
|
|
color = LocalCustomColors.current.primary
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
val focusRequester = remember { FocusRequester() }
|
|
val shouldRequestFocus by focusDtmf
|
|
TextField(
|
|
value = dtmfText.value,
|
|
onValueChange = {
|
|
if (it.length > dtmfText.value.length) {
|
|
val char = it.last()
|
|
if (char.isDigit() || char == '*' || char == '#') {
|
|
Log.d(TAG, "Got DTMF digit '$char'")
|
|
ua.currentCall()?.sendDigit(char)
|
|
}
|
|
}
|
|
dtmfText.value = it
|
|
},
|
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone),
|
|
modifier = Modifier
|
|
.width(80.dp)
|
|
.focusRequester(focusRequester),
|
|
enabled = dtmfEnabled.value,
|
|
textStyle = TextStyle(fontSize = 16.sp),
|
|
label = { LabelText(stringResource(R.string.dtmf)) },
|
|
singleLine = true
|
|
)
|
|
LaunchedEffect(shouldRequestFocus) {
|
|
if (shouldRequestFocus) {
|
|
focusRequester.requestFocus()
|
|
focusDtmf.value = false
|
|
}
|
|
}
|
|
|
|
IconButton(
|
|
modifier = Modifier.size(48.dp),
|
|
onClick = {
|
|
val call = ua.currentCall()
|
|
val stats = call?.stats("audio")
|
|
if (stats != null && call.startTime != null && stats != "") {
|
|
val parts = stats.split(",") as java.util.ArrayList
|
|
if (parts[2] == "0/0") {
|
|
parts[2] = "?/?"
|
|
parts[3] = "?/?"
|
|
parts[4] = "?/?"
|
|
}
|
|
val codecs = call.audioCodecs()
|
|
val duration = call.duration()
|
|
val txCodec = codecs.split(',')[0].split("/")
|
|
val rxCodec = codecs.split(',')[1].split("/")
|
|
alertTitle.value = getString(R.string.call_info)
|
|
alertMessage.value =
|
|
"${String.format(getString(R.string.duration), duration)}\n" +
|
|
"${getString(R.string.codecs)}: ${txCodec[0]} ch ${txCodec[2]}/" +
|
|
"${rxCodec[0]} ch ${rxCodec[2]}\n" +
|
|
"${String.format(getString(R.string.rate), parts[0])}\n" +
|
|
"${
|
|
String.format(
|
|
getString(R.string.average_rate),
|
|
parts[1]
|
|
)
|
|
}\n" +
|
|
"${getString(R.string.packets)}: ${parts[2]}\n" +
|
|
"${getString(R.string.lost)}: ${parts[3]}\n" +
|
|
String.format(getString(R.string.jitter), parts[4])
|
|
showAlert.value = true
|
|
} else {
|
|
alertTitle.value = getString(R.string.call_info)
|
|
alertMessage.value = getString(R.string.call_info_not_available)
|
|
showAlert.value = true
|
|
}
|
|
},
|
|
) {
|
|
Icon(
|
|
imageVector = ImageVector.vectorResource(id = R.drawable.info),
|
|
modifier = Modifier.size(36.dp),
|
|
tint = Color.Unspecified,
|
|
contentDescription = null,
|
|
)
|
|
}
|
|
}
|
|
|
|
if (showAnswerRejectButtons.value) {
|
|
|
|
IconButton(
|
|
modifier = Modifier.size(48.dp),
|
|
onClick = {
|
|
answer(ctx)
|
|
},
|
|
) {
|
|
Icon(
|
|
imageVector = ImageVector.vectorResource(id = R.drawable.call),
|
|
modifier = Modifier.size(48.dp),
|
|
tint = Color.Unspecified,
|
|
contentDescription = null,
|
|
)
|
|
}
|
|
|
|
Spacer(Modifier.weight(1f))
|
|
|
|
IconButton(
|
|
modifier = Modifier.size(48.dp),
|
|
onClick = {
|
|
reject()
|
|
},
|
|
) {
|
|
Icon(
|
|
imageVector = ImageVector.vectorResource(id = R.drawable.hangup),
|
|
modifier = Modifier.size(48.dp),
|
|
tint = Color.Unspecified,
|
|
contentDescription = null,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private fun callClick(ctx: Context) {
|
|
if (viewModel.selectedAor.value != "") {
|
|
if (Utils.checkPermissions(ctx, arrayOf(RECORD_AUDIO))) {
|
|
if (Call.inCall())
|
|
return
|
|
val uriText = callUri.value.trim()
|
|
if (uriText.isNotEmpty()) {
|
|
val uris = Contact.contactUris(uriText)
|
|
if (uris.isEmpty())
|
|
makeCall(uriText)
|
|
else if (uris.size == 1)
|
|
makeCall(uris[0])
|
|
else {
|
|
items.value = uris
|
|
itemAction.value = { index ->
|
|
makeCall(uris[index])
|
|
}
|
|
showSelectItemDialog.value = true
|
|
}
|
|
}
|
|
else {
|
|
val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
|
|
val latestPeerUri = CallHistoryNew.aorLatestPeerUri(ua.account.aor)
|
|
if (latestPeerUri != null)
|
|
callUri.value = Utils.friendlyUri(this, latestPeerUri, ua.account)
|
|
}
|
|
}
|
|
else
|
|
Toast.makeText(applicationContext, R.string.no_calls, Toast.LENGTH_SHORT).show()
|
|
}
|
|
}
|
|
|
|
private fun makeCall(uriText: String) {
|
|
val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
|
|
val aor = ua.account.aor
|
|
val peerUri = if (Utils.isTelNumber(uriText))
|
|
"tel:$uriText"
|
|
else
|
|
uriText
|
|
val uri = if (Utils.isTelUri(peerUri)) {
|
|
if (ua.account.telProvider == "") {
|
|
alertTitle.value = getString(R.string.notice)
|
|
alertMessage.value = String.format(getString(R.string.no_telephony_provider), aor)
|
|
showAlert.value = true
|
|
return
|
|
}
|
|
Utils.telToSip(peerUri, ua.account)
|
|
}
|
|
else
|
|
Utils.uriComplete(peerUri, aor)
|
|
if (!Utils.checkUri(uri)) {
|
|
alertTitle.value = getString(R.string.notice)
|
|
alertMessage.value = String.format(getString(R.string.invalid_sip_or_tel_uri), uri)
|
|
showAlert.value = true
|
|
}
|
|
else if (!BaresipService.requestAudioFocus(applicationContext)) {
|
|
Toast.makeText(applicationContext, R.string.audio_focus_denied,
|
|
Toast.LENGTH_SHORT).show()
|
|
} else {
|
|
if (Build.VERSION.SDK_INT < 31) {
|
|
Log.d(TAG, "Setting audio mode to MODE_IN_COMMUNICATION")
|
|
am.mode = AudioManager.MODE_IN_COMMUNICATION
|
|
runCall(ua, uri)
|
|
} else {
|
|
if (am.mode == AudioManager.MODE_IN_COMMUNICATION) {
|
|
runCall(ua, uri)
|
|
} else {
|
|
audioModeChangedListener = AudioManager.OnModeChangedListener { mode ->
|
|
if (mode == AudioManager.MODE_IN_COMMUNICATION) {
|
|
Log.d(TAG, "Audio mode changed to MODE_IN_COMMUNICATION using " +
|
|
"device ${am.communicationDevice!!.type}")
|
|
if (audioModeChangedListener != null) {
|
|
am.removeOnModeChangedListener(audioModeChangedListener!!)
|
|
audioModeChangedListener = null
|
|
}
|
|
runCall(ua, uri)
|
|
} else {
|
|
Log.d(TAG, "Audio mode changed to mode ${am.mode} using " +
|
|
"device ${am.communicationDevice!!.type}")
|
|
}
|
|
}
|
|
am.addOnModeChangedListener(mainExecutor, audioModeChangedListener!!)
|
|
Log.d(TAG, "Setting audio mode to MODE_IN_COMMUNICATION")
|
|
am.mode = AudioManager.MODE_IN_COMMUNICATION
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun runCall(ua: UserAgent, uri: String) {
|
|
callRunnable = Runnable {
|
|
callRunnable = null
|
|
if (!call(ua, uri)) {
|
|
BaresipService.abandonAudioFocus(applicationContext)
|
|
showCallButton.value = true
|
|
showCancelButton.value = false
|
|
}
|
|
else {
|
|
showCallButton.value = false
|
|
showCancelButton.value = true
|
|
}
|
|
}
|
|
callHandler.postDelayed(callRunnable!!, BaresipService.audioDelay)
|
|
}
|
|
|
|
private fun answer(ctx: Context) {
|
|
val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
|
|
val call = ua.currentCall()
|
|
if (call != null) {
|
|
Log.d(TAG, "AoR ${ua.account.aor} answering call from ${callUri.value}")
|
|
val intent = Intent(ctx, BaresipService::class.java)
|
|
intent.action = "Call Answer"
|
|
intent.putExtra("uap", ua.uap)
|
|
intent.putExtra("callp", call.callp)
|
|
intent.putExtra("video", Api.VIDMODE_OFF)
|
|
startService(intent)
|
|
}
|
|
}
|
|
|
|
private fun reject() {
|
|
val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
|
|
val call = ua.currentCall()
|
|
if (call != null) {
|
|
val callp = call.callp
|
|
Log.d(TAG, "AoR ${ua.account.aor} rejecting call $callp from ${callUri.value}")
|
|
call.rejected = true
|
|
Api.ua_hangup(ua.uap, callp, 486, "Busy Here")
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun BottomBar(ctx: Context) {
|
|
val buttonSize = 48.dp
|
|
Row( modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(bottom = 12.dp),
|
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
if (showVmIcon)
|
|
IconButton(
|
|
onClick = {
|
|
if (viewModel.selectedAor.value != "") {
|
|
val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
|
|
val acc = ua.account
|
|
if (acc.vmUri != "") {
|
|
dialogTitle.value = getString(R.string.voicemail_messages)
|
|
dialogMessage.value = acc.vmMessages(ctx)
|
|
positiveText.value = getString(R.string.listen)
|
|
onPositiveClicked.value = {
|
|
val i = Intent(ctx, MainActivity::class.java)
|
|
i.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
|
Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
|
i.putExtra("action", "call")
|
|
i.putExtra("uap", ua.uap)
|
|
i.putExtra("peer", acc.vmUri)
|
|
startActivity(i)
|
|
}
|
|
negativeText.value = getString(R.string.cancel)
|
|
onNegativeClicked.value = {}
|
|
showDialog.value = true
|
|
}
|
|
}
|
|
},
|
|
modifier = Modifier
|
|
.weight(1f)
|
|
.size(buttonSize)
|
|
) {
|
|
Icon(
|
|
imageVector = ImageVector.vectorResource(vmIcon.intValue),
|
|
contentDescription = null,
|
|
Modifier.size(buttonSize),
|
|
tint = Color.Unspecified
|
|
)
|
|
}
|
|
|
|
IconButton(
|
|
onClick = {
|
|
if (viewModel.selectedAor.value != "") {
|
|
val i = Intent(ctx, ContactsActivity::class.java)
|
|
val b = Bundle()
|
|
b.putString("aor", viewModel.selectedAor.value)
|
|
i.putExtras(b)
|
|
contactsRequest.launch(i)
|
|
}
|
|
},
|
|
modifier = Modifier
|
|
.weight(1f)
|
|
.size(buttonSize)
|
|
) {
|
|
Icon(
|
|
imageVector = ImageVector.vectorResource(R.drawable.contacts),
|
|
contentDescription = null,
|
|
Modifier.size(buttonSize),
|
|
tint = LocalCustomColors.current.secondary
|
|
)
|
|
}
|
|
|
|
IconButton(
|
|
onClick = {
|
|
if (viewModel.selectedAor.value != "") {
|
|
val i = Intent(this@MainActivity, ChatsActivity::class.java)
|
|
val b = Bundle()
|
|
b.putString("aor", viewModel.selectedAor.value)
|
|
b.putString("peer", resumeUri)
|
|
i.putExtras(b)
|
|
chatRequests.launch(i)
|
|
}
|
|
},
|
|
modifier = Modifier
|
|
.weight(1f)
|
|
.size(buttonSize)
|
|
) {
|
|
Icon(
|
|
imageVector = ImageVector.vectorResource(messagesIcon.intValue),
|
|
contentDescription = null,
|
|
Modifier.size(buttonSize),
|
|
tint = Color.Unspecified
|
|
)
|
|
}
|
|
|
|
IconButton(
|
|
onClick = {
|
|
calls(ctx)
|
|
},
|
|
modifier = Modifier
|
|
.weight(1f)
|
|
.size(buttonSize)
|
|
) {
|
|
Icon(
|
|
imageVector = ImageVector.vectorResource(R.drawable.calls),
|
|
contentDescription = null,
|
|
Modifier.size(buttonSize),
|
|
tint = Color.Unspecified
|
|
)
|
|
}
|
|
|
|
IconButton(
|
|
onClick = { dialpad = !dialpad },
|
|
modifier = Modifier
|
|
.weight(1f)
|
|
.size(buttonSize),
|
|
enabled = dialpadButtonEnabled
|
|
) {
|
|
Icon(
|
|
imageVector = if (dialpad)
|
|
ImageVector.vectorResource(R.drawable.dialpad_on)
|
|
else
|
|
ImageVector.vectorResource(R.drawable.dialpad_off),
|
|
contentDescription = null,
|
|
modifier = Modifier.size(buttonSize),
|
|
tint = Color.Unspecified
|
|
)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
@Composable
|
|
fun OnHoldNotice() {
|
|
if (showOnHoldNotice.value)
|
|
OutlinedButton(
|
|
onClick = {},
|
|
border = BorderStroke(1.dp, LocalCustomColors.current.accent),
|
|
modifier = Modifier.padding(16.dp),
|
|
shape = RoundedCornerShape(20)
|
|
) {
|
|
Text(
|
|
text = stringResource(R.string.call_is_on_hold),
|
|
fontSize = 18.sp,
|
|
color = LocalCustomColors.current.itemText,
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun AskPasswords(ctx: Context) {
|
|
if (showPasswordsDialog.value) {
|
|
if (passwordAccounts.isNotEmpty()) {
|
|
val account = passwordAccounts.removeAt(0)
|
|
val params = account.substringAfter(">")
|
|
if (Utils.paramValue(params, "auth_user") != "" && Utils.paramValue(params, "auth_pass") == "") {
|
|
val aor = account.substringAfter("<").substringBefore(">")
|
|
PasswordDialog(
|
|
ctx = ctx,
|
|
showPasswordDialog = showPasswordsDialog,
|
|
password = password,
|
|
keyboardController = keyboardController,
|
|
title = stringResource(R.string.authentication_password),
|
|
message = stringResource(R.string.account) + " " + Utils.plainAor(aor),
|
|
okAction = {
|
|
if (password.value != "")
|
|
BaresipService.aorPasswords[aor] = password.value
|
|
showPasswordsDialog.value = true
|
|
},
|
|
cancelAction = {
|
|
showPasswordsDialog.value = true
|
|
}
|
|
)
|
|
}
|
|
else {
|
|
showPasswordsDialog.value = false
|
|
showPasswordsDialog.value = true
|
|
}
|
|
}
|
|
else
|
|
requestPermissionsLauncher.launch(permissions)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun AskPassword(ctx: Context) {
|
|
if (showPasswordDialog.value)
|
|
PasswordDialog(
|
|
ctx = ctx,
|
|
showPasswordDialog = showPasswordDialog,
|
|
password = password,
|
|
keyboardController = keyboardController,
|
|
title = passwordTitle,
|
|
okAction = {
|
|
if (password.value != "") {
|
|
if (passwordTitle == getString(R.string.encrypt_password))
|
|
backup(password.value)
|
|
else
|
|
restore(password.value)
|
|
password.value = ""
|
|
}
|
|
},
|
|
cancelAction = {
|
|
if (downloadsOutputUri != null) {
|
|
Utils.deleteFile(ctx, downloadsOutputUri!!)
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
private fun calls(ctx: Context) {
|
|
if (viewModel.selectedAor.value != "") {
|
|
val i = Intent(ctx, CallsActivity::class.java)
|
|
val b = Bundle()
|
|
b.putString("aor", viewModel.selectedAor.value)
|
|
i.putExtras(b)
|
|
callsRequest.launch(i)
|
|
}
|
|
}
|
|
|
|
private fun updateIcons(acc: Account?) {
|
|
if (acc == null) {
|
|
showVmIcon = false
|
|
messagesIcon.intValue = R.drawable.messages
|
|
callsIcon.intValue = R.drawable.calls
|
|
}
|
|
else {
|
|
if (acc.vmUri != "") {
|
|
showVmIcon = true
|
|
vmIcon.intValue = if (acc.vmNew > 0)
|
|
R.drawable.voicemail_new
|
|
else
|
|
R.drawable.voicemail
|
|
} else
|
|
showVmIcon = false
|
|
messagesIcon.intValue= if (acc.unreadMessages)
|
|
R.drawable.messages_unread
|
|
else
|
|
R.drawable.messages
|
|
callsIcon.intValue = if (acc.missedCalls)
|
|
R.drawable.calls_missed
|
|
else
|
|
R.drawable.calls
|
|
}
|
|
}
|
|
|
|
override fun onStart() {
|
|
super.onStart()
|
|
Log.e(TAG, "Main onStart")
|
|
val action = intent.getStringExtra("action")
|
|
if (action != null) {
|
|
// MainActivity was not visible when call, message, or transfer request came in
|
|
intent.removeExtra("action")
|
|
handleIntent(intent, action)
|
|
}
|
|
}
|
|
|
|
override fun onResume() {
|
|
super.onResume()
|
|
Log.d(TAG, "Main onResume with action '$resumeAction'")
|
|
nm.cancelAll()
|
|
BaresipService.isMainVisible = true
|
|
when (resumeAction) {
|
|
"call show" -> {
|
|
handleServiceEvent ("call incoming",
|
|
arrayListOf(resumeCall!!.ua.uap, resumeCall!!.callp))
|
|
}
|
|
"call answer" -> {
|
|
answer(this@MainActivity)
|
|
showCall(resumeCall!!.ua)
|
|
}
|
|
"call missed" -> {
|
|
calls(this@MainActivity)
|
|
}
|
|
"call reject" ->
|
|
reject()
|
|
"call" -> {
|
|
callUri.value = Account.ofAor(viewModel.selectedAor.value)!!.resumeUri
|
|
callClick(this@MainActivity)
|
|
}
|
|
"dial" -> {
|
|
callUri.value = Account.ofAor(viewModel.selectedAor.value)!!.resumeUri
|
|
}
|
|
"call transfer", "transfer show", "transfer accept" ->
|
|
handleServiceEvent("$resumeAction,$resumeUri",
|
|
arrayListOf(resumeCall!!.ua.uap, resumeCall!!.callp))
|
|
"message", "message show", "message reply" ->
|
|
handleServiceEvent(resumeAction, arrayListOf(resumeUap, resumeUri))
|
|
else -> {
|
|
val incomingCall = Call.call("incoming")
|
|
if (incomingCall != null) {
|
|
spinToAor(incomingCall.ua.account.aor)
|
|
} else {
|
|
restoreActivities()
|
|
if (uas.value.isNotEmpty()) {
|
|
if (viewModel.selectedAor.value == "") {
|
|
if (Call.inCall())
|
|
spinToAor(Call.calls()[0].ua.account.aor)
|
|
else
|
|
spinToAor(uas.value.first().account.aor)
|
|
}
|
|
}
|
|
}
|
|
val ua = UserAgent.ofAor(viewModel.selectedAor.value)
|
|
if (ua != null) {
|
|
showCall(ua)
|
|
updateIcons(ua.account)
|
|
}
|
|
}
|
|
}
|
|
resumeAction = ""
|
|
}
|
|
|
|
override fun onPause() {
|
|
super.onPause()
|
|
Log.d(TAG, "Main onPause")
|
|
Utils.addActivity("main")
|
|
BaresipService.isMainVisible = false
|
|
callTimer?.stop()
|
|
saveCallUri()
|
|
}
|
|
|
|
override fun onStop() {
|
|
super.onStop()
|
|
Log.d(TAG, "Main onStop")
|
|
}
|
|
|
|
override fun onDestroy() {
|
|
Log.d(TAG, "Main onDestroy")
|
|
|
|
if (Build.VERSION.SDK_INT >= 33) {
|
|
if (backInvokedCallback != null)
|
|
onBackInvokedDispatcher.unregisterOnBackInvokedCallback(backInvokedCallback!!)
|
|
}
|
|
else
|
|
onBackPressedCallback.remove()
|
|
|
|
this.unregisterReceiver(screenEventReceiver)
|
|
|
|
if (Build.VERSION.SDK_INT >= 31)
|
|
am.removeOnCommunicationDeviceChangedListener(comDevChangedListener)
|
|
|
|
BaresipService.serviceEvent.removeObserver(serviceEventObserver)
|
|
BaresipService.serviceEvents.clear()
|
|
BaresipService.activities.clear()
|
|
|
|
super.onDestroy()
|
|
}
|
|
|
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
|
super.onConfigurationChanged(newConfig)
|
|
if (dtmfEnabled.value)
|
|
focusDtmf.value = true
|
|
}
|
|
|
|
override fun onNewIntent(intent: Intent) {
|
|
// Called when MainActivity already exists at the top of current task
|
|
super.onNewIntent(intent)
|
|
|
|
this.setShowWhenLocked(true)
|
|
this.setTurnScreenOn(true)
|
|
|
|
resumeAction = ""
|
|
resumeUri = ""
|
|
|
|
Log.d(TAG, "onNewIntent with action/data '${intent.action}/${intent.data}'")
|
|
|
|
when (intent.action) {
|
|
ACTION_DIAL, ACTION_CALL, ACTION_VIEW ->
|
|
callAction(intent.data, if (intent.action == ACTION_CALL) "call" else "dial")
|
|
else -> {
|
|
val action = intent.getStringExtra("action")
|
|
if (action != null) {
|
|
intent.removeExtra("action")
|
|
handleIntent(intent, action)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun callAction(uri: Uri?, action: String) {
|
|
if (Call.inCall() || uas.value.isEmpty())
|
|
return
|
|
Log.d(TAG, "Action $action to $uri")
|
|
if (uri != null) {
|
|
when (uri.scheme) {
|
|
"sip" -> {
|
|
val uriStr = Utils.uriUnescape(uri.toString())
|
|
var ua = UserAgent.ofDomain(Utils.uriHostPart(uriStr))
|
|
if (ua == null && uas.value.isNotEmpty())
|
|
ua = uas.value[0]
|
|
if (ua == null) {
|
|
Log.w(TAG, "No accounts for '$uriStr'")
|
|
return
|
|
}
|
|
spinToAor(ua.account.aor)
|
|
resumeAction = action
|
|
ua.account.resumeUri = uriStr
|
|
}
|
|
"tel" -> {
|
|
val uriStr = uri.toString().replace("%2B", "+")
|
|
.replace("%20", "")
|
|
.filterNot{setOf('-', ' ', '(', ')').contains(it)}
|
|
var account: Account? = null
|
|
for (a in Account.accounts())
|
|
if (a.telProvider != "") {
|
|
account = a
|
|
break
|
|
}
|
|
if (account == null) {
|
|
Log.w(TAG, "No telephony providers for '$uriStr'")
|
|
return
|
|
}
|
|
spinToAor(account.aor)
|
|
resumeAction = action
|
|
account.resumeUri = uriStr
|
|
}
|
|
else -> {
|
|
Log.w(TAG, "Unsupported URI scheme ${uri.scheme}")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun handleIntent(intent: Intent, action: String) {
|
|
Log.d(TAG, "Handling intent '$action'")
|
|
val ev = action.split(",")
|
|
when (ev[0]) {
|
|
"call", "dial" -> {
|
|
if (Call.inCall()) {
|
|
Toast.makeText(applicationContext, getString(R.string.call_already_active),
|
|
Toast.LENGTH_SHORT).show()
|
|
return
|
|
}
|
|
val uap = intent.getLongExtra("uap", 0L)
|
|
val ua = UserAgent.ofUap(uap)
|
|
if (ua == null) {
|
|
Log.w(TAG, "handleIntent 'call' did not find ua $uap")
|
|
return
|
|
}
|
|
spinToAor(ua.account.aor)
|
|
resumeAction = action
|
|
ua.account.resumeUri = intent.getStringExtra("peer")!!
|
|
}
|
|
"call show", "call answer" -> {
|
|
val callp = intent.getLongExtra("callp", 0L)
|
|
val call = Call.ofCallp(callp)
|
|
if (call == null) {
|
|
Log.w(TAG, "handleIntent '$action' did not find call $callp")
|
|
return
|
|
}
|
|
val ua = call.ua
|
|
spinToAor(ua.account.aor)
|
|
resumeAction = action
|
|
resumeCall = call
|
|
}
|
|
"call missed" -> {
|
|
val uap = intent.getLongExtra("uap", 0L)
|
|
val ua = UserAgent.ofUap(uap)
|
|
if (ua == null) {
|
|
Log.w(TAG, "handleIntent did not find ua $uap")
|
|
return
|
|
}
|
|
spinToAor(ua.account.aor)
|
|
resumeAction = action
|
|
}
|
|
"call transfer", "transfer show", "transfer accept" -> {
|
|
val callp = intent.getLongExtra("callp", 0L)
|
|
val call = Call.ofCallp(callp)
|
|
if (call == null) {
|
|
Log.w(TAG, "handleIntent '$action' did not find call $callp")
|
|
moveTaskToBack(true)
|
|
return
|
|
}
|
|
resumeAction = ev[0]
|
|
resumeCall = call
|
|
resumeUri = if (ev[0] == "call transfer")
|
|
ev[1]
|
|
else
|
|
intent.getStringExtra("uri")!!
|
|
}
|
|
"message", "message show", "message reply" -> {
|
|
val uap = intent.getLongExtra("uap", 0L)
|
|
val ua = UserAgent.ofUap(uap)
|
|
if (ua == null) {
|
|
Log.w(TAG, "handleIntent did not find ua $uap")
|
|
return
|
|
}
|
|
spinToAor(ua.account.aor)
|
|
resumeAction = action
|
|
resumeUap = uap
|
|
resumeUri = intent.getStringExtra("peer")!!
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
|
val stream = if (am.mode == AudioManager.MODE_RINGTONE)
|
|
AudioManager.STREAM_RING
|
|
else
|
|
AudioManager.STREAM_MUSIC
|
|
when (keyCode) {
|
|
KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP -> {
|
|
am.adjustStreamVolume(stream,
|
|
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)
|
|
AudioManager.ADJUST_LOWER else
|
|
AudioManager.ADJUST_RAISE,
|
|
AudioManager.FLAG_SHOW_UI)
|
|
Log.d(TAG, "Adjusted volume $keyCode of stream $stream to ${am.getStreamVolume(stream)}")
|
|
return true
|
|
}
|
|
}
|
|
return super.onKeyDown(keyCode, event)
|
|
}
|
|
|
|
private fun handleServiceEvent(event: String, params: ArrayList<Any>) {
|
|
|
|
fun handleNextEvent(logMessage: String? = null) {
|
|
if (logMessage != null)
|
|
Log.w(TAG, logMessage)
|
|
if (BaresipService.serviceEvents.isNotEmpty()) {
|
|
val first = BaresipService.serviceEvents.removeAt(0)
|
|
handleServiceEvent(first.event, first.params)
|
|
}
|
|
}
|
|
|
|
if (taskId == -1) {
|
|
handleNextEvent("Omit service event '$event' for task -1")
|
|
return
|
|
}
|
|
|
|
if (event == "started") {
|
|
val uriString = params[0] as String
|
|
Log.d(TAG, "Handling service event 'started' with URI '$uriString'")
|
|
if (!initialized) {
|
|
// Android has restarted baresip when permission has been denied in app settings
|
|
recreate()
|
|
return
|
|
}
|
|
if (uriString != "")
|
|
callAction(uriString.toUri(), "dial")
|
|
else {
|
|
if (viewModel.selectedAor.value == "" && uas.value.isNotEmpty())
|
|
viewModel.updateSelectedAor(uas.value.first().account.aor)
|
|
}
|
|
if (Preferences(applicationContext).displayTheme != AppCompatDelegate.getDefaultNightMode()) {
|
|
AppCompatDelegate.setDefaultNightMode(Preferences(applicationContext).displayTheme)
|
|
}
|
|
handleNextEvent()
|
|
return
|
|
}
|
|
|
|
if (event == "stopped") {
|
|
Log.d(TAG, "Handling service event 'stopped' with start error '${params[0]}'")
|
|
if (params[0] != "") {
|
|
alertTitle.value = getString(R.string.notice)
|
|
alertMessage.value = getString(R.string.start_failed)
|
|
showAlert.value = true
|
|
}
|
|
else {
|
|
finishAndRemoveTask()
|
|
if (restart)
|
|
reStart()
|
|
else
|
|
exitProcess(0)
|
|
}
|
|
return
|
|
}
|
|
|
|
val uap = params[0] as Long
|
|
val ua = UserAgent.ofUap(uap)
|
|
if (ua == null) {
|
|
handleNextEvent("handleServiceEvent '$event' did not find ua $uap")
|
|
return
|
|
}
|
|
|
|
val ev = event.split(",")
|
|
Log.d(TAG, "Handling service event '${ev[0]}' for $uap")
|
|
val acc = ua.account
|
|
val aor = ua.account.aor
|
|
|
|
when (ev[0]) {
|
|
"call rejected" -> {
|
|
if (aor == viewModel.selectedAor.value)
|
|
callsIcon.intValue = R.drawable.calls_missed
|
|
}
|
|
"call incoming", "call outgoing" -> {
|
|
val callp = params[1] as Long
|
|
if (BaresipService.isMainVisible) {
|
|
spinToAor(aor)
|
|
showCall(ua, Call.ofCallp(callp))
|
|
} else {
|
|
Log.d(TAG, "Reordering to front")
|
|
BaresipService.activities.clear()
|
|
BaresipService.serviceEvents.clear()
|
|
val i = Intent(applicationContext, MainActivity::class.java)
|
|
i.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
|
i.putExtra("action", "call show")
|
|
i.putExtra("callp", callp)
|
|
startActivity(i)
|
|
return
|
|
}
|
|
}
|
|
"call answered" -> {
|
|
showCall(ua)
|
|
}
|
|
"call redirect" -> {
|
|
val redirectUri = ev[1]
|
|
val target = Utils.friendlyUri(this, redirectUri, acc)
|
|
if (acc.autoRedirect) {
|
|
redirect(ua, target)
|
|
Toast.makeText(applicationContext,
|
|
String.format(getString(R.string.redirect_notice), target),
|
|
Toast.LENGTH_SHORT
|
|
).show()
|
|
}
|
|
else {
|
|
dialogTitle.value = getString(R.string.redirect_request)
|
|
dialogMessage.value = String.format(getString(R.string.redirect_request_query), target)
|
|
positiveText.value = getString(R.string.yes)
|
|
onPositiveClicked.value = {
|
|
redirect(ua, target)
|
|
}
|
|
negativeText.value = getString(R.string.no)
|
|
onNegativeClicked.value = {}
|
|
showDialog.value = true
|
|
}
|
|
showCall(ua)
|
|
}
|
|
"call established" -> {
|
|
if (aor == viewModel.selectedAor.value) {
|
|
dtmfText.value = ""
|
|
showCall(ua)
|
|
}
|
|
}
|
|
"call update" -> {
|
|
showCall(ua)
|
|
}
|
|
"call verify" -> {
|
|
val callp = params[1] as Long
|
|
val call = Call.ofCallp(callp)
|
|
if (call == null) {
|
|
handleNextEvent("Call $callp to be verified is not found")
|
|
return
|
|
}
|
|
dialogTitle.value = getString(R.string.verify)
|
|
dialogMessage.value = String.format(getString(R.string.verify_sas), ev[1])
|
|
positiveText.value = getString(R.string.yes)
|
|
onPositiveClicked.value = {
|
|
call.security = if (Api.cmd_exec("zrtp_verify ${ev[2]}") != 0) {
|
|
Log.e(TAG, "Command 'zrtp_verify ${ev[2]}' failed")
|
|
R.drawable.locked_yellow
|
|
} else {
|
|
R.drawable.locked_green
|
|
}
|
|
call.zid = ev[2]
|
|
if (aor == viewModel.selectedAor.value)
|
|
securityIcon.intValue = call.security
|
|
}
|
|
negativeText.value = getString(R.string.no)
|
|
onNegativeClicked.value = {
|
|
call.security = R.drawable.locked_yellow
|
|
call.zid = ev[2]
|
|
if (aor == viewModel.selectedAor.value)
|
|
securityIcon.intValue = R.drawable.locked_yellow
|
|
onNegativeClicked.value = {}
|
|
}
|
|
showDialog.value = true
|
|
}
|
|
"call verified", "call secure" -> {
|
|
val callp = params[1] as Long
|
|
val call = Call.ofCallp(callp)
|
|
if (call == null) {
|
|
handleNextEvent("Call $callp that is verified is not found")
|
|
return
|
|
}
|
|
if (aor == viewModel.selectedAor.value)
|
|
securityIcon.intValue = call.security
|
|
}
|
|
"call transfer", "transfer show" -> {
|
|
val callp = params[1] as Long
|
|
if (!BaresipService.isMainVisible) {
|
|
Log.d(TAG, "Reordering to front")
|
|
BaresipService.activities.clear()
|
|
BaresipService.serviceEvents.clear()
|
|
val i = Intent(applicationContext, MainActivity::class.java)
|
|
i.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
|
i.putExtra("action", event)
|
|
i.putExtra("callp", callp)
|
|
startActivity(i)
|
|
return
|
|
}
|
|
val call = Call.ofCallp(callp)
|
|
val target = Utils.friendlyUri(this, ev[1], acc)
|
|
dialogTitle.value = if (call != null)
|
|
getString(R.string.transfer_request)
|
|
else
|
|
getString(R.string.call_request)
|
|
dialogMessage.value = if (call != null)
|
|
String.format(getString(R.string.transfer_request_query), target)
|
|
else
|
|
String.format(getString(R.string.call_request_query), target)
|
|
positiveText.value = getString(R.string.yes)
|
|
onPositiveClicked.value = {
|
|
if (call in Call.calls())
|
|
acceptTransfer(ua, call!!, ev[1])
|
|
else
|
|
makeCall(ev[1])
|
|
}
|
|
negativeText.value = getString(R.string.no)
|
|
onNegativeClicked.value = {
|
|
if (call in Call.calls())
|
|
call!!.notifySipfrag(603, "Decline")
|
|
onNegativeClicked.value = {}
|
|
}
|
|
showDialog.value = true
|
|
}
|
|
"transfer accept" -> {
|
|
val callp = params[1] as Long
|
|
val call = Call.ofCallp(callp)
|
|
if (call in Call.calls())
|
|
Api.ua_hangup(uap, callp, 0, "")
|
|
call(ua, ev[1])
|
|
showCall(ua)
|
|
}
|
|
"transfer failed" -> {
|
|
showCall(ua)
|
|
}
|
|
"call closed" -> {
|
|
val call = ua.currentCall()
|
|
if (call != null) {
|
|
call.resume()
|
|
startCallTimer(call)
|
|
}
|
|
else
|
|
callTimer?.stop()
|
|
if (aor == viewModel.selectedAor.value) {
|
|
ua.account.resumeUri = ""
|
|
showCall(ua)
|
|
if (acc.missedCalls)
|
|
callsIcon.intValue = R.drawable.calls_missed
|
|
}
|
|
if (kgm.isDeviceLocked)
|
|
this.setShowWhenLocked(false)
|
|
}
|
|
"message", "message show", "message reply" -> {
|
|
val i = Intent(applicationContext, ChatActivity::class.java)
|
|
i.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
|
val b = Bundle()
|
|
b.putString("aor", aor)
|
|
b.putString("peer", params[1] as String)
|
|
b.putBoolean("focus", ev[0] == "message reply")
|
|
i.putExtras(b)
|
|
chatRequests.launch(i)
|
|
}
|
|
"mwi notify" -> {
|
|
val lines = ev[1].split("\n")
|
|
for (line in lines) {
|
|
if (line.startsWith("Voice-Message:")) {
|
|
val counts = (line.split(" ")[1]).split("/")
|
|
acc.vmNew = counts[0].toInt()
|
|
acc.vmOld = counts[1].toInt()
|
|
break
|
|
}
|
|
}
|
|
if (aor == viewModel.selectedAor.value) {
|
|
vmIcon.intValue = if (acc.vmNew > 0)
|
|
R.drawable.voicemail_new
|
|
else
|
|
R.drawable.voicemail
|
|
}
|
|
}
|
|
else -> Log.e(TAG, "Unknown event '${ev[0]}'")
|
|
}
|
|
|
|
handleNextEvent()
|
|
}
|
|
|
|
private fun redirect(ua: UserAgent, redirectUri: String) {
|
|
if (ua.account.aor != viewModel.selectedAor.value)
|
|
spinToAor(ua.account.aor)
|
|
callUri.value = redirectUri
|
|
callClick(this@MainActivity)
|
|
}
|
|
|
|
private fun reStart() {
|
|
Log.d(TAG, "Trigger restart")
|
|
val pm = applicationContext.packageManager
|
|
val intent = pm.getLaunchIntentForPackage(this.packageName)
|
|
this.startActivity(intent)
|
|
exitProcess(0)
|
|
}
|
|
|
|
@RequiresApi(29)
|
|
private fun pickupFileFromDownloads(action: String) {
|
|
when (action) {
|
|
"backup" -> {
|
|
backupRequest.launch(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
|
addCategory(Intent.CATEGORY_OPENABLE)
|
|
type = "application/octet-stream"
|
|
putExtra(Intent.EXTRA_TITLE, "baresip_" +
|
|
SimpleDateFormat("yyyy_MM_dd_HH_mm_ss", Locale.getDefault()).format(Date()))
|
|
putExtra(DocumentsContract.EXTRA_INITIAL_URI, MediaStore.Downloads.EXTERNAL_CONTENT_URI)
|
|
})
|
|
}
|
|
"restore" -> {
|
|
restoreRequest.launch(Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
|
addCategory(Intent.CATEGORY_OPENABLE)
|
|
type = "application/octet-stream"
|
|
putExtra(DocumentsContract.EXTRA_INITIAL_URI, MediaStore.Downloads.EXTERNAL_CONTENT_URI)
|
|
})
|
|
}
|
|
"logcat" -> {
|
|
logcatRequest.launch(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
|
addCategory(Intent.CATEGORY_OPENABLE)
|
|
type = "text/plain"
|
|
putExtra(Intent.EXTRA_TITLE, "baresip_logcat_" +
|
|
SimpleDateFormat("yyyy_MM_dd_HH_mm_ss", Locale.getDefault()).format(Date()))
|
|
putExtra(DocumentsContract.EXTRA_INITIAL_URI, MediaStore.Downloads.EXTERNAL_CONTENT_URI)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun quitRestart(reStart: Boolean) {
|
|
Log.i(TAG, "quitRestart Restart = $reStart")
|
|
window.setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
|
|
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)
|
|
if (BaresipService.isServiceRunning) {
|
|
restart = reStart
|
|
baresipService.action = "Stop"
|
|
startService(baresipService)
|
|
} else {
|
|
finishAndRemoveTask()
|
|
if (reStart)
|
|
reStart()
|
|
else
|
|
exitProcess(0)
|
|
}
|
|
}
|
|
|
|
private fun transfer(ua: UserAgent, uriText: String, attended: Boolean) {
|
|
val uri = if (Utils.isTelUri(uriText))
|
|
Utils.telToSip(uriText, ua.account)
|
|
else
|
|
Utils.uriComplete(uriText, ua.account.aor)
|
|
if (!Utils.checkUri(uri)) {
|
|
alertTitle.value = getString(R.string.notice)
|
|
alertMessage.value = String.format(getString(R.string.invalid_sip_or_tel_uri), uri)
|
|
showAlert.value = true
|
|
}
|
|
else {
|
|
val call = ua.currentCall()
|
|
if (call != null) {
|
|
if (attended) {
|
|
if (call.hold()) {
|
|
call.referTo = uri
|
|
call(ua, uri, call)
|
|
}
|
|
}
|
|
else {
|
|
if (!call.transfer(uri)) {
|
|
alertTitle.value = getString(R.string.notice)
|
|
alertMessage.value = getString(R.string.transfer_failed)
|
|
showAlert.value = true
|
|
}
|
|
}
|
|
showCall(ua)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun startBaresip() {
|
|
if (!BaresipService.isStartReceived) {
|
|
baresipService.action = "Start"
|
|
startService(baresipService)
|
|
if (atStartup)
|
|
moveTaskToBack(true)
|
|
}
|
|
}
|
|
|
|
private fun backup(password: String) {
|
|
val files = arrayListOf("accounts", "config", "contacts", "call_history",
|
|
"messages", "gzrtp.zid", "cert.pem", "ca_cert", "ca_certs.crt")
|
|
File(BaresipService.filesPath).walk().forEach {
|
|
if (it.name.endsWith(".png"))
|
|
files.add(it.name)
|
|
}
|
|
val zipFile = getString(R.string.app_name) + ".zip"
|
|
val zipFilePath = BaresipService.filesPath + "/$zipFile"
|
|
if (!Utils.zip(files, zipFile)) {
|
|
Log.w(TAG, "Failed to write zip file '$zipFile'")
|
|
alertTitle.value = getString(R.string.error)
|
|
alertMessage.value = String.format(getString(R.string.backup_failed),
|
|
Utils.fileNameOfUri(applicationContext, downloadsOutputUri!!))
|
|
showAlert.value = true
|
|
downloadsOutputUri = null
|
|
return
|
|
}
|
|
val content = Utils.getFileContents(zipFilePath)
|
|
if (content == null) {
|
|
Log.w(TAG, "Failed to read zip file '$zipFile'")
|
|
alertTitle.value = getString(R.string.error)
|
|
alertMessage.value = String.format(getString(R.string.backup_failed),
|
|
Utils.fileNameOfUri(applicationContext, downloadsOutputUri!!))
|
|
showAlert.value = true
|
|
downloadsOutputUri = null
|
|
return
|
|
}
|
|
if (!Utils.encryptToUri(applicationContext, downloadsOutputUri!!, content, password)) {
|
|
alertTitle.value = getString(R.string.error)
|
|
alertMessage.value = String.format(getString(R.string.backup_failed),
|
|
Utils.fileNameOfUri(applicationContext, downloadsOutputUri!!))
|
|
showAlert.value = true
|
|
downloadsOutputUri = null
|
|
return
|
|
}
|
|
alertTitle.value = getString(R.string.info)
|
|
alertMessage.value = String.format(getString(R.string.backed_up),
|
|
Utils.fileNameOfUri(applicationContext, downloadsOutputUri!!))
|
|
showAlert.value = true
|
|
Utils.deleteFile(File(zipFilePath))
|
|
downloadsOutputUri = null
|
|
}
|
|
|
|
private fun restore(password: String) {
|
|
val zipFile = getString(R.string.app_name) + ".zip"
|
|
val zipFilePath = BaresipService.filesPath + "/$zipFile"
|
|
val zipData = Utils.decryptFromUri(applicationContext, downloadsInputUri!!, password)
|
|
if (zipData == null) {
|
|
alertTitle.value = getString(R.string.error)
|
|
alertMessage.value = String.format(getString(R.string.restore_failed),
|
|
Utils.fileNameOfUri(applicationContext, downloadsOutputUri!!))
|
|
showAlert.value = true
|
|
downloadsOutputUri = null
|
|
return
|
|
}
|
|
if (!Utils.putFileContents(zipFilePath, zipData)) {
|
|
Log.w(TAG, "Failed to write zip file '$zipFile'")
|
|
alertTitle.value = getString(R.string.error)
|
|
alertMessage.value = String.format(getString(R.string.restore_failed),
|
|
Utils.fileNameOfUri(applicationContext, downloadsOutputUri!!))
|
|
showAlert.value = true
|
|
downloadsOutputUri = null
|
|
return
|
|
}
|
|
if (!Utils.unZip(zipFilePath)) {
|
|
Log.w(TAG, "Failed to unzip file '$zipFile'")
|
|
alertTitle.value = getString(R.string.error)
|
|
alertMessage.value = String.format(getString(R.string.restore_unzip_failed), "baresip", BuildConfig.VERSION_NAME)
|
|
showAlert.value = true
|
|
downloadsOutputUri = null
|
|
return
|
|
}
|
|
Utils.deleteFile(File(zipFilePath))
|
|
|
|
File("${BaresipService.filesPath}/recordings").walk().forEach {
|
|
if (it.name.startsWith("dump"))
|
|
Utils.deleteFile(it)
|
|
}
|
|
|
|
dialogTitle.value = getString(R.string.info)
|
|
dialogMessage.value = getString(R.string.restored)
|
|
positiveText.value = getString(R.string.restart)
|
|
onPositiveClicked.value = {
|
|
quitRestart(true)
|
|
}
|
|
negativeText.value = getString(R.string.cancel)
|
|
onNegativeClicked.value = {}
|
|
showDialog.value = true
|
|
|
|
downloadsOutputUri = null
|
|
}
|
|
|
|
private fun spinToAor(aor: String) {
|
|
if (aor != viewModel.selectedAor.value)
|
|
viewModel.updateSelectedAor(aor)
|
|
updateIcons(Account.ofAor(aor))
|
|
}
|
|
|
|
private fun userAgentOfSelectedAor(): UserAgent {
|
|
return UserAgent.ofAor(viewModel.selectedAor.value)!!
|
|
}
|
|
|
|
private fun call(ua: UserAgent, uri: String, onHoldCall: Call? = null): Boolean {
|
|
spinToAor(ua.account.aor)
|
|
val callp = ua.callAlloc(0L, Api.VIDMODE_OFF)
|
|
return if (callp != 0L) {
|
|
Log.d(TAG, "Adding outgoing call ${ua.uap}/$callp/$uri")
|
|
val call = Call(callp, ua, uri, "out", "outgoing")
|
|
call.onHoldCall = onHoldCall
|
|
call.add()
|
|
if (onHoldCall != null)
|
|
onHoldCall.newCall = call
|
|
if (call.connect(uri)) {
|
|
showCall(ua)
|
|
true
|
|
} else {
|
|
Log.w(TAG, "call_connect $callp failed")
|
|
if (onHoldCall != null)
|
|
onHoldCall.newCall = null
|
|
call.remove()
|
|
call.destroy()
|
|
showCall(ua)
|
|
false
|
|
}
|
|
} else {
|
|
Log.w(TAG, "callAlloc for ${ua.uap} to $uri failed")
|
|
false
|
|
}
|
|
}
|
|
|
|
private fun acceptTransfer(ua: UserAgent, call: Call, uri: String) {
|
|
val newCallp = ua.callAlloc(call.callp, Api.VIDMODE_OFF)
|
|
if (newCallp != 0L) {
|
|
Log.d(TAG, "Adding outgoing call ${ua.uap}/$newCallp/$uri")
|
|
val newCall = Call(newCallp, ua, uri, "out", "transferring")
|
|
newCall.add()
|
|
if (newCall.connect(uri)) {
|
|
if (ua.account.aor != viewModel.selectedAor.value)
|
|
spinToAor(ua.account.aor)
|
|
showCall(ua)
|
|
} else {
|
|
Log.w(TAG, "call_connect $newCallp failed")
|
|
call.notifySipfrag(500, "Call Error")
|
|
}
|
|
} else {
|
|
Log.w(TAG, "callAlloc for ua ${ua.uap} call ${call.callp} transfer failed")
|
|
call.notifySipfrag(500, "Call Error")
|
|
}
|
|
}
|
|
|
|
private fun showCall(ua: UserAgent?, showCall: Call? = null) {
|
|
if (ua == null)
|
|
return
|
|
val call = showCall ?: ua.currentCall()
|
|
if (call == null) {
|
|
pullToRefreshEnabled = true
|
|
if (ua.account.resumeUri != "")
|
|
callUri.value = ua.account.resumeUri
|
|
else
|
|
callUri.value = ""
|
|
callUriLabel.value = getString(R.string.outgoing_call_to_dots)
|
|
callUriEnabled.value = true
|
|
keyboardController?.hide()
|
|
showCallTimer.value = false
|
|
securityIcon.intValue = -1
|
|
showHangupButton.value = false
|
|
transferIcon.intValue = R.drawable.call_transfer
|
|
dtmfEnabled.value = false
|
|
focusDtmf.value = false
|
|
showCallButton.value = true
|
|
showCancelButton.value = false
|
|
showAnswerRejectButtons.value = false
|
|
showOnHoldNotice.value = false
|
|
dialpadButtonEnabled = true
|
|
if (BaresipService.isMicMuted) {
|
|
BaresipService.isMicMuted = false
|
|
micIcon = R.drawable.mic_on
|
|
}
|
|
} else {
|
|
pullToRefreshEnabled = false
|
|
callUriEnabled.value = false
|
|
when (call.status) {
|
|
"outgoing", "transferring", "answered" -> {
|
|
callUriLabel.value = if (call.status == "answered")
|
|
getString(R.string.incoming_call_from_dots)
|
|
else
|
|
getString(R.string.outgoing_call_to_dots)
|
|
callUri.value = Utils.friendlyUri(this, call.peerUri, ua.account)
|
|
showCallTimer.value = false
|
|
securityIcon.intValue = -1
|
|
showCallButton.value = false
|
|
showCancelButton.value = call.status == "outgoing"
|
|
showHangupButton.value = !showCancelButton.value
|
|
showAnswerRejectButtons.value = false
|
|
showOnHoldNotice.value = false
|
|
dialpadButtonEnabled = false
|
|
}
|
|
"incoming" -> {
|
|
showCallTimer.value = false
|
|
securityIcon.intValue = -1
|
|
val uri = call.diverterUri()
|
|
if (uri != "") {
|
|
callUriLabel.value = getString(R.string.diverted_by_dots)
|
|
callUri.value = Utils.friendlyUri(this, uri, ua.account)
|
|
}
|
|
else {
|
|
callUriLabel.value = getString(R.string.incoming_call_from_dots)
|
|
callUri.value = Utils.friendlyUri(this, call.peerUri, ua.account)
|
|
}
|
|
showCallButton.value = false
|
|
showCancelButton.value = false
|
|
showHangupButton.value = false
|
|
showAnswerRejectButtons.value = true
|
|
showOnHoldNotice.value = false
|
|
dialpadButtonEnabled = false
|
|
}
|
|
"connected" -> {
|
|
if (call.referTo != "") {
|
|
callUriLabel.value = getString(R.string.outgoing_call_to_dots)
|
|
callUri.value = Utils.friendlyUri(this, call.referTo, ua.account)
|
|
transferButtonEnabled.value = false
|
|
} else {
|
|
if (call.dir == "out") {
|
|
callUriLabel.value = getString(R.string.outgoing_call_to_dots)
|
|
callUri.value = Utils.friendlyUri(this, call.peerUri, ua.account)
|
|
} else {
|
|
callUriLabel.value = getString(R.string.incoming_call_from_dots)
|
|
callUri.value = Utils.friendlyUri(this, call.peerUri, ua.account)
|
|
}
|
|
transferButtonEnabled.value = true
|
|
}
|
|
transferIcon.intValue = if (call.onHoldCall == null)
|
|
R.drawable.call_transfer
|
|
else
|
|
R.drawable.call_transfer_execute
|
|
showCallTimer.value = true
|
|
startCallTimer(call)
|
|
if (ua.account.mediaEnc == "")
|
|
securityIcon.intValue = -1
|
|
else
|
|
securityIcon.intValue = call.security
|
|
showCallButton.value = false
|
|
showCancelButton.value = false
|
|
showHangupButton.value = true
|
|
showAnswerRejectButtons.value = false
|
|
if (call.onhold)
|
|
holdIcon.intValue = R.drawable.resume
|
|
else
|
|
holdIcon.intValue = R.drawable.call_hold
|
|
Handler(Looper.getMainLooper()).postDelayed({
|
|
showOnHoldNotice.value = call.held
|
|
}, 100)
|
|
if (call.held) {
|
|
keyboardController?.hide()
|
|
dtmfEnabled.value = false
|
|
focusDtmf.value = false
|
|
} else {
|
|
dtmfEnabled.value = true
|
|
focusDtmf.value = true
|
|
if (resources.configuration.orientation == ORIENTATION_PORTRAIT)
|
|
keyboardController?.show()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun restoreActivities() {
|
|
if (BaresipService.activities.isEmpty()) return
|
|
Log.d(TAG, "Activity stack ${BaresipService.activities}")
|
|
val activity = BaresipService.activities[0].split(",")
|
|
BaresipService.activities.removeAt(0)
|
|
when (activity[0]) {
|
|
"main" -> {
|
|
if (!Call.inCall() && (BaresipService.activities.size > 1))
|
|
restoreActivities()
|
|
}
|
|
"config" -> {
|
|
configRequest.launch(Intent(this, ConfigActivity::class.java))
|
|
}
|
|
"audio" -> {
|
|
startActivity(Intent(this, AudioActivity::class.java))
|
|
}
|
|
"accounts" -> {
|
|
val i = Intent(this, AccountsActivity::class.java)
|
|
val b = Bundle()
|
|
b.putString("aor", activity[1])
|
|
i.putExtras(b)
|
|
accountsRequest.launch(i)
|
|
}
|
|
"account" -> {
|
|
val i = Intent(this, AccountActivity::class.java)
|
|
val b = Bundle()
|
|
b.putString("aor", activity[1])
|
|
i.putExtras(b)
|
|
accountsRequest.launch(i)
|
|
}
|
|
"codecs" -> {
|
|
val i = Intent(this, CodecsActivity::class.java)
|
|
val b = Bundle()
|
|
b.putString("aor", activity[1])
|
|
b.putString("media", activity[2])
|
|
i.putExtras(b)
|
|
startActivity(i)
|
|
}
|
|
"about" -> {
|
|
startActivity(Intent(this, AboutActivity::class.java))
|
|
}
|
|
"contacts" -> {
|
|
val i = Intent(this, ContactsActivity::class.java)
|
|
val b = Bundle()
|
|
b.putString("aor", activity[1])
|
|
i.putExtras(b)
|
|
contactsRequest.launch(i)
|
|
}
|
|
"contact" -> {
|
|
val i = Intent(this, BaresipContactActivity::class.java)
|
|
val b = Bundle()
|
|
if (activity[1] == "true") {
|
|
b.putBoolean("new", true)
|
|
b.putString("uri", activity[2])
|
|
} else {
|
|
b.putBoolean("new", false)
|
|
b.putInt("index", activity[2].toInt())
|
|
}
|
|
i.putExtras(b)
|
|
startActivity(i)
|
|
}
|
|
"chats" -> {
|
|
val i = Intent(this, ChatsActivity::class.java)
|
|
val b = Bundle()
|
|
b.putString("aor", activity[1])
|
|
i.putExtras(b)
|
|
chatRequests.launch(i)
|
|
}
|
|
"chat" -> {
|
|
val i = Intent(this, ChatActivity::class.java)
|
|
val b = Bundle()
|
|
b.putString("aor", activity[1])
|
|
b.putString("peer", activity[2])
|
|
b.putBoolean("focus", activity[3] == "true")
|
|
i.putExtras(b)
|
|
chatRequests.launch(i)
|
|
}
|
|
"calls" -> {
|
|
val i = Intent(this, CallsActivity::class.java)
|
|
val b = Bundle()
|
|
b.putString("aor", activity[1])
|
|
i.putExtras(b)
|
|
callsRequest.launch(i)
|
|
}
|
|
"call_details" -> {
|
|
val i = Intent(this, CallDetailsActivity::class.java)
|
|
val b = Bundle()
|
|
b.putString("aor", activity[1])
|
|
b.putString("peer", activity[2])
|
|
b.putInt("position", activity[3].toInt())
|
|
i.putExtras(b)
|
|
callsRequest.launch(i)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
private fun saveCallUri() {
|
|
if (uas.value.isNotEmpty() && viewModel.selectedAor.value != "") {
|
|
val ua = UserAgent.ofAor(viewModel.selectedAor.value)!!
|
|
if (ua.calls().isEmpty())
|
|
ua.account.resumeUri = callUri.value
|
|
else
|
|
ua.account.resumeUri = ""
|
|
}
|
|
}
|
|
|
|
private fun startCallTimer(call: Call) {
|
|
Handler(Looper.getMainLooper()).postDelayed({
|
|
callTimer?.stop()
|
|
callTimer?.base = SystemClock.elapsedRealtime() - (call.duration() * 1000L)
|
|
callTimer?.start()
|
|
}, 100)
|
|
}
|
|
|
|
private fun abandonAudioFocus() {
|
|
if (Build.VERSION.SDK_INT < 31) {
|
|
if (callRunnable != null) {
|
|
callHandler.removeCallbacks(callRunnable!!)
|
|
callRunnable = null
|
|
BaresipService.abandonAudioFocus(applicationContext)
|
|
}
|
|
} else {
|
|
if (audioModeChangedListener != null) {
|
|
am.removeOnModeChangedListener(audioModeChangedListener!!)
|
|
audioModeChangedListener = null
|
|
BaresipService.abandonAudioFocus(applicationContext)
|
|
}
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
var activityAor = ""
|
|
}
|
|
|
|
init {
|
|
if (!BaresipService.libraryLoaded) {
|
|
Log.d(TAG, "Loading baresip library")
|
|
System.loadLibrary("baresip")
|
|
BaresipService.libraryLoaded = true
|
|
}
|
|
}
|
|
|
|
}
|