@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> private lateinit var requestPermissionLauncher: ActivityResultLauncher private lateinit var requestPermissionsLauncher: ActivityResultLauncher> 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() val shouldShow = mutableListOf() 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}") } } } }