494 lines
20 KiB
Kotlin
494 lines
20 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.media.AudioManager
|
|
import android.os.Build
|
|
import android.os.Bundle
|
|
import android.provider.CallLog
|
|
import android.view.KeyEvent
|
|
import android.view.WindowManager
|
|
import androidx.activity.ComponentActivity
|
|
import androidx.activity.compose.setContent
|
|
import androidx.activity.result.ActivityResultLauncher
|
|
import androidx.activity.result.contract.ActivityResultContracts
|
|
import androidx.activity.viewModels
|
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
import androidx.compose.runtime.LaunchedEffect
|
|
import androidx.lifecycle.Observer
|
|
import androidx.lifecycle.lifecycleScope
|
|
import kotlinx.coroutines.launch
|
|
import androidx.navigation.NavHostController
|
|
import androidx.navigation.compose.NavHost
|
|
import androidx.navigation.compose.rememberNavController
|
|
import androidx.core.content.ContextCompat
|
|
import kotlin.system.exitProcess
|
|
|
|
class MainActivity : ComponentActivity() {
|
|
|
|
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 comDevChangedListener: AudioManager.OnCommunicationDeviceChangedListener
|
|
|
|
private lateinit var baresipService: Intent
|
|
|
|
private var restart = false
|
|
private var atStartup = false
|
|
private var initialized = false
|
|
|
|
private val viewModel: ViewModel by viewModels()
|
|
private lateinit var navController: NavHostController
|
|
|
|
@SuppressLint("ClickableViewAccessibility")
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
|
|
super.onCreate(savedInstanceState)
|
|
|
|
val extraAction = intent.getStringExtra("action")
|
|
Log.i(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)
|
|
|
|
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)
|
|
if (taskId != -1) {
|
|
if (first.event == "started" && !initialized)
|
|
// Android has restarted baresip when permission has been denied in app settings
|
|
recreate()
|
|
else {
|
|
if (first.event == "stopped") {
|
|
Log.d(TAG, "Handling service event 'stopped' with start error '${first.params[0]}'")
|
|
if (first.params[0] != "")
|
|
handleDialog(
|
|
ctx = applicationContext,
|
|
title = getString(R.string.notice),
|
|
message =getString(R.string.start_failed)
|
|
)
|
|
else {
|
|
finishAndRemoveTask()
|
|
if (restart)
|
|
reStart()
|
|
else
|
|
exitProcess(0)
|
|
}
|
|
}
|
|
else
|
|
handleServiceEvent(this, viewModel, first.event, first.params)
|
|
}
|
|
}
|
|
else
|
|
Log.d(TAG, "Omit service event '$event' for task -1")
|
|
}
|
|
}
|
|
|
|
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}")
|
|
}
|
|
am.addOnCommunicationDeviceChangedListener(mainExecutor, comDevChangedListener)
|
|
}
|
|
|
|
initialized = true
|
|
|
|
val restartApp = {
|
|
Log.i(TAG, "Restarting baresip")
|
|
window.setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
|
|
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)
|
|
if (BaresipService.isServiceRunning) {
|
|
restart = true
|
|
baresipService.action = "Stop"
|
|
ContextCompat.startForegroundService(this, baresipService)
|
|
}
|
|
else {
|
|
finishAndRemoveTask()
|
|
val pm = applicationContext.packageManager
|
|
val intent = pm.getLaunchIntentForPackage(applicationContext.packageName)
|
|
if (intent != null) {
|
|
applicationContext.startActivity(intent)
|
|
exitProcess(0)
|
|
}
|
|
else
|
|
Log.e(TAG, "Failed to restart: Launch intent is null")
|
|
}
|
|
}
|
|
|
|
val quitApp = {
|
|
Log.i(TAG, "Quiting baresip")
|
|
window.setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)
|
|
if (BaresipService.isServiceRunning) {
|
|
restart = false
|
|
baresipService.action = "Stop"
|
|
ContextCompat.startForegroundService(this, baresipService)
|
|
}
|
|
else {
|
|
finishAndRemoveTask()
|
|
exitProcess(0)
|
|
}
|
|
}
|
|
|
|
baresipService = Intent(this@MainActivity, BaresipService::class.java)
|
|
|
|
atStartup = intent.hasExtra("onStartup")
|
|
|
|
if (intent?.action == Intent.ACTION_VIEW &&
|
|
"baresip".equals(intent.data?.scheme, ignoreCase = true)
|
|
) {
|
|
handleProvisioningIntent(this, viewModel, intent)
|
|
} else {
|
|
when (intent?.action) {
|
|
ACTION_DIAL, ACTION_CALL, ACTION_VIEW ->
|
|
if (BaresipService.isServiceRunning)
|
|
callAction(
|
|
applicationContext,
|
|
viewModel,
|
|
intent.data,
|
|
if (intent?.action == ACTION_CALL) "call" else "dial"
|
|
)
|
|
else
|
|
BaresipService.callActionUri = intent.data
|
|
.toString().replace("%2B", "+")
|
|
.replace("%20", "")
|
|
.filterNot{setOf('-', ' ', '(', ')').contains(it)}
|
|
}
|
|
}
|
|
|
|
val 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 if (Build.VERSION.SDK_INT < 29)
|
|
arrayOf(RECORD_AUDIO, READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE)
|
|
else
|
|
arrayOf(RECORD_AUDIO)
|
|
|
|
requestPermissionLauncher =
|
|
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
|
|
Log.i(TAG, "Permission granted: $isGranted")
|
|
}
|
|
|
|
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))
|
|
handleDialog(
|
|
ctx = applicationContext,
|
|
title = getString(R.string.notice),
|
|
message = getString(R.string.no_notifications),
|
|
action = { quitRestart(false) }
|
|
)
|
|
else if (shouldShow.isNotEmpty())
|
|
handleDialog(
|
|
ctx = applicationContext,
|
|
title = getString(R.string.permissions_rationale),
|
|
message = getString(R.string.audio_permissions),
|
|
action = { requestPermissionsLauncher.launch(permissions) }
|
|
)
|
|
else if (!BaresipService.isStartReceived) {
|
|
baresipService.action = "Start"
|
|
ContextCompat.startForegroundService(this, baresipService)
|
|
if (atStartup) moveTaskToBack(true)
|
|
}
|
|
}
|
|
|
|
setContent {
|
|
|
|
AppTheme {
|
|
|
|
navController = rememberNavController()
|
|
|
|
LaunchedEffect(key1 = viewModel) {
|
|
viewModel.navigationCommand.collect { command ->
|
|
Log.d(TAG, "MainActivity: Received NavigationCommand: $command")
|
|
when (command) {
|
|
is NavigationCommand.NavigateToChat -> {
|
|
val route = "chat/${command.aor}/${command.peerUri}"
|
|
navController.navigate(route) { launchSingleTop = true }
|
|
}
|
|
is NavigationCommand.NavigateToCalls -> {
|
|
val route = "calls/${command.aor}"
|
|
navController.navigate(route) { launchSingleTop = true }
|
|
}
|
|
is NavigationCommand.NavigateToChats ->
|
|
navController.navigate("chats") { launchSingleTop = true }
|
|
is NavigationCommand.NavigateToHome ->
|
|
navController.navigate("main") {
|
|
popUpTo("main") { inclusive = true }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
NavHost(navController, startDestination = "main") {
|
|
mainScreenRoute(
|
|
navController = navController,
|
|
viewModel = viewModel,
|
|
onRequestPermissions = { requestPermissionsLauncher.launch(permissions) },
|
|
onRestartApp = { restartApp() },
|
|
onQuitApp = { quitApp() }
|
|
)
|
|
aboutScreenRoute(navController)
|
|
settingsScreenRoute(
|
|
navController = navController,
|
|
onRestartApp = { restartApp() }
|
|
)
|
|
accountsScreenRoute(navController)
|
|
audioScreenRoute(navController)
|
|
accountScreenRoute(navController)
|
|
codecsScreenRoute(navController)
|
|
contactsScreenRoute(navController)
|
|
contactScreenRoute(navController, viewModel)
|
|
callsScreenRoute(navController, viewModel)
|
|
callDetailsScreenRoute(navController, viewModel)
|
|
blockedScreenRoute(navController)
|
|
blockingScreenRoute(navController)
|
|
chatsScreenRoute(navController)
|
|
chatScreenRoute(navController, viewModel)
|
|
}
|
|
}
|
|
}
|
|
|
|
} // OnCreate
|
|
|
|
override fun onStart() {
|
|
super.onStart()
|
|
Log.i(TAG, "Main onStart action/type/data: ${intent.action}/${intent.type}/${intent.data}")
|
|
val action = intent.getStringExtra("action")
|
|
if (action != null) {
|
|
// MainActivity was not visible when call, message, or transfer request came in
|
|
intent.removeExtra("action")
|
|
handleIntent(applicationContext, viewModel, intent, action)
|
|
}
|
|
else if (isCallLogIntent(intent)) {
|
|
handleCallLogIntent()
|
|
}
|
|
else if (intent.action == Intent.ACTION_MAIN && !atStartup) {
|
|
val activeNotifications = nm.activeNotifications
|
|
if (activeNotifications.any { it.id == CALL_MISSED_NOTIFICATION_ID }) {
|
|
val ua = BaresipService.uas.value.find { it.account.missedCalls }
|
|
if (ua != null)
|
|
viewModel.navigateToCalls(ua.account.aor)
|
|
}
|
|
else if (activeNotifications.any { it.id == MESSAGE_NOTIFICATION_ID }) {
|
|
val lastUnread = BaresipService.messages.lastOrNull { it.new }
|
|
if (lastUnread != null)
|
|
viewModel.onNewMessageReceived(lastUnread.aor, lastUnread.peerUri)
|
|
else
|
|
viewModel.navigateToChats()
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onResume() {
|
|
super.onResume()
|
|
Log.d(TAG, "Main onResume")
|
|
nm.cancelAll()
|
|
if (Build.VERSION.SDK_INT >= 29) {
|
|
val baresipService = Intent(this, BaresipService::class.java)
|
|
baresipService.action = "Check Roles"
|
|
startService(baresipService)
|
|
}
|
|
}
|
|
|
|
override fun onPause() {
|
|
super.onPause()
|
|
Log.d(TAG, "Main onPause")
|
|
}
|
|
|
|
override fun onDestroy() {
|
|
Log.d(TAG, "Main onDestroy")
|
|
|
|
this.unregisterReceiver(screenEventReceiver)
|
|
|
|
if (Build.VERSION.SDK_INT >= 31)
|
|
am.removeOnCommunicationDeviceChangedListener(comDevChangedListener)
|
|
|
|
BaresipService.serviceEvent.removeObserver(serviceEventObserver)
|
|
BaresipService.serviceEvents.clear()
|
|
|
|
super.onDestroy()
|
|
}
|
|
|
|
override fun onNewIntent(intent: Intent) {
|
|
// Called when MainActivity already exists at the top of current task
|
|
super.onNewIntent(intent)
|
|
setIntent(intent)
|
|
|
|
this.setShowWhenLocked(true)
|
|
this.setTurnScreenOn(true)
|
|
|
|
Log.d(TAG, "onNewIntent action/type/data: ${intent.action}/${intent.type}/${intent.data}")
|
|
|
|
when {
|
|
isCallLogIntent(intent) -> {
|
|
handleCallLogIntent()
|
|
}
|
|
intent.action == Intent.ACTION_VIEW &&
|
|
intent.data?.scheme.equals("baresip", ignoreCase = true) -> {
|
|
handleProvisioningIntent(applicationContext, viewModel, intent)
|
|
}
|
|
intent.action in listOf(ACTION_DIAL, ACTION_CALL, ACTION_VIEW) -> {
|
|
callAction(
|
|
applicationContext,
|
|
viewModel,
|
|
intent.data,
|
|
if (intent.action == ACTION_CALL) "call" else "dial"
|
|
)
|
|
}
|
|
else -> {
|
|
val action = intent.getStringExtra("action")
|
|
if (action != null) {
|
|
intent.removeExtra("action")
|
|
handleIntent(applicationContext, viewModel, intent, action)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
|
val stream = if (am.mode == AudioManager.MODE_RINGTONE)
|
|
AudioManager.STREAM_RING
|
|
else
|
|
AudioManager.STREAM_VOICE_CALL
|
|
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 isCallLogIntent(intent: Intent?): Boolean {
|
|
return intent?.action == ACTION_VIEW && (intent.type == CallLog.Calls.CONTENT_TYPE ||
|
|
intent.data?.toString()?.contains("calls") == true)
|
|
}
|
|
|
|
private fun handleCallLogIntent() {
|
|
Log.d(TAG, "Handling Call Log intent")
|
|
val ua = BaresipService.uas.value.find { it.account.isMobile }
|
|
?: BaresipService.uas.value.firstOrNull()
|
|
if (ua != null)
|
|
viewModel.navigateToCalls(ua.account.aor)
|
|
}
|
|
|
|
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"
|
|
ContextCompat.startForegroundService(this, baresipService)
|
|
}
|
|
else {
|
|
finishAndRemoveTask()
|
|
if (reStart)
|
|
quitRestart(true)
|
|
else
|
|
exitProcess(0)
|
|
}
|
|
}
|
|
|
|
private fun reStart() {
|
|
Log.d(TAG, "Trigger restart")
|
|
val pm = applicationContext.packageManager
|
|
val intent = pm.getLaunchIntentForPackage(this.packageName)
|
|
this.startActivity(intent)
|
|
exitProcess(0)
|
|
}
|
|
|
|
private fun handleProvisioningIntent(ctx: Context, viewModel: ViewModel, intent: Intent) {
|
|
val data = intent.data ?: run {
|
|
Log.w(TAG, "Provisioning intent has no data")
|
|
return
|
|
}
|
|
val endpoint = data.getQueryParameter("endpoint") ?: return
|
|
val extension = data.getQueryParameter("extension") ?: return
|
|
Log.i(TAG, "Provisioning from $endpoint ext=$extension")
|
|
|
|
lifecycleScope.launch {
|
|
try {
|
|
val payload = Provisioning.fetchAndDecryptBundle(ctx, endpoint, extension)
|
|
val acc = Provisioning.applyBundle(ctx, payload)
|
|
if (acc == null) throw Provisioning.ProvisioningException("Failed to apply bundle")
|
|
Log.i(TAG, "Provisioning applied: ${acc.aor}")
|
|
viewModel.navigateToHome()
|
|
if (BaresipService.isServiceRunning) {
|
|
val restart = Intent(ctx, BaresipService::class.java)
|
|
restart.action = "Stop"
|
|
ContextCompat.startForegroundService(ctx, restart)
|
|
} else {
|
|
val restartApp = Intent(ctx, MainActivity::class.java)
|
|
restartApp.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
|
ctx.startActivity(restartApp)
|
|
exitProcess(0)
|
|
}
|
|
} catch (e: Provisioning.ProvisioningException) {
|
|
Log.e(TAG, "Provisioning failed: ${e.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|