More work on missed mobile calls

This commit is contained in:
Juha Heinanen
2026-07-15 15:28:34 +03:00
6 changed files with 149 additions and 54 deletions

View File

@ -94,6 +94,11 @@
<data android:scheme="tel" />
<data android:scheme="sip" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.cursor.dir/calls" />
</intent-filter>
</activity>
<activity

View File

@ -24,8 +24,8 @@ import android.media.AudioAttributes
import android.media.AudioDeviceInfo
import android.media.AudioFocusRequest
import android.media.AudioManager
import android.media.AudioManager.MODE_IN_COMMUNICATION
import android.media.AudioManager.MODE_IN_CALL
import android.media.AudioManager.MODE_IN_COMMUNICATION
import android.media.AudioManager.MODE_NORMAL
import android.media.MediaPlayer
import android.media.Ringtone
@ -80,6 +80,10 @@ import androidx.core.net.toUri
import androidx.lifecycle.MutableLiveData
import com.tutpro.baresip.Utils.e164Uri
import com.tutpro.baresip.Utils.toCircle
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.File
import java.net.InetAddress
import java.nio.charset.Charset
@ -90,10 +94,6 @@ import java.util.TimerTask
import kotlin.concurrent.schedule
import kotlin.math.roundToInt
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class BaresipService: Service() {
@ -723,6 +723,24 @@ class BaresipService: Service() {
nm.cancel(MESSAGE_NOTIFICATION_ID)
}
"Clear Unread" -> {
val uap = intent!!.getLongExtra("uap", 0L)
val ua = UserAgent.ofUap(uap)
if (ua != null) {
val peerUri = intent.getStringExtra("peer")
if (peerUri != null)
Message.updateMessagesFromPearRead(ua.account.aor, peerUri)
else {
for (m in messages)
if (m.aor == ua.account.aor)
m.new = false
Message.save()
}
ua.account.unreadMessages = Message.unreadMessages(ua.account.aor)
}
nm.cancel(MESSAGE_NOTIFICATION_ID)
}
"Message Inline Reply" -> {
val remoteInputResults = RemoteInput.getResultsFromIntent(intent!!)
if (remoteInputResults != null) {
@ -777,6 +795,14 @@ class BaresipService: Service() {
addMobileUserAgent()
}
"Clear Missed" -> {
val uap = intent!!.getLongExtra("uap", 0L)
val ua = UserAgent.ofUap(uap)
if (ua != null)
ua.account.missedCalls = false
nm.cancel(CALL_MISSED_NOTIFICATION_ID)
}
"Start Call" -> {
val uap = intent!!.getLongExtra("uap", 0L)
val uri = intent.getStringExtra("uri")!!
@ -1560,10 +1586,16 @@ class BaresipService: Service() {
.setGroupConversation(false)
.addMessage(text, timeStamp, sender)
val clearIntent = Intent(this, BaresipService::class.java)
clearIntent.action = "Clear Unread"
clearIntent.putExtra("uap", uap).putExtra("peer", peerUri)
val dpi = PendingIntent.getService(this, MESSAGE_NOTIFICATION_ID, clearIntent, piFlags)
val nb = NotificationCompat.Builder(this, HIGH_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification_message)
.setColor(ContextCompat.getColor(this, R.color.colorPrimary))
.setContentIntent(pi)
.setDeleteIntent(dpi)
.setSound(Settings.System.DEFAULT_NOTIFICATION_URI)
.setAutoCancel(true)
.setStyle(messagingStyle)
@ -1579,9 +1611,7 @@ class BaresipService: Service() {
}
nb.setNumber(unreadMessages + 1)
val remoteInput = RemoteInput.Builder(KEY_TEXT_REPLY)
.setLabel(getString(R.string.reply))
.build()
val remoteInput = RemoteInput.Builder(KEY_TEXT_REPLY).setLabel(getString(R.string.reply)).build()
val directReplyIntent = Intent(this, BaresipService::class.java)
directReplyIntent.action = "Message Inline Reply"
directReplyIntent.putExtra("uap", uap).putExtra("peer", peerUri).putExtra("time", timeStamp)
@ -1602,10 +1632,8 @@ class BaresipService: Service() {
val saveIntent = Intent(this, BaresipService::class.java)
saveIntent.action = "Message Save"
saveIntent.putExtra("uap", uap)
.putExtra("time", timeStamp.toString())
val savePendingIntent = PendingIntent
.getService(this, SAVE_REQ_CODE, saveIntent, piFlags)
saveIntent.putExtra("uap", uap).putExtra("time", timeStamp.toString())
val savePendingIntent = PendingIntent.getService(this, SAVE_REQ_CODE, saveIntent, piFlags)
val saveAction = NotificationCompat.Action.Builder(
R.drawable.ic_notification_save,
getString(R.string.save),
@ -1614,10 +1642,8 @@ class BaresipService: Service() {
val deleteIntent = Intent(this, BaresipService::class.java)
deleteIntent.action = "Message Delete"
deleteIntent.putExtra("uap", uap)
.putExtra("time", timeStamp.toString())
val deletePendingIntent = PendingIntent
.getService(this, DELETE_REQ_CODE, deleteIntent, piFlags)
deleteIntent.putExtra("uap", uap).putExtra("time", timeStamp.toString())
val deletePendingIntent = PendingIntent.getService(this, DELETE_REQ_CODE, deleteIntent, piFlags)
val deleteAction = NotificationCompat.Action.Builder(
R.drawable.ic_notification_delete,
getString(R.string.delete),
@ -2250,6 +2276,7 @@ class BaresipService: Service() {
history.add()
if (call.dir == "in" && call.startTime == null && !call.rejected) {
call.ua.account.missedCalls = true
if (!call.ua.account.isMobile)
showMissedCallNotification(uap, call.peerUri, call.ua.account.aor)
}
}
@ -3080,10 +3107,17 @@ class BaresipService: Service() {
intent,
piFlags
)
val deleteIntent = Intent(this, BaresipService::class.java)
deleteIntent.action = "Clear Missed"
deleteIntent.putExtra("uap", uap)
val dpi = PendingIntent.getService(this, CALL_MISSED_NOTIFICATION_ID, deleteIntent, piFlags)
val nb = NotificationCompat.Builder(this, HIGH_CHANNEL_ID)
nb.setSmallIcon(R.drawable.ic_notification_call_missed)
.setColor(ContextCompat.getColor(this, R.color.colorPrimary))
.setContentIntent(pi)
.setDeleteIntent(dpi)
.setCategory(NotificationCompat.CATEGORY_MISSED_CALL)
.setAutoCancel(true)

View File

@ -114,8 +114,13 @@ private fun CallsScreen(navController: NavController, viewModel: ViewModel, aor:
}
}
val ctx = LocalContext.current
BackHandler(enabled = true) {
ua.account.missedCalls = false
val serviceIntent = Intent(ctx, BaresipService::class.java)
serviceIntent.action = "Clear Missed"
serviceIntent.putExtra("uap", ua.uap)
ctx.startService(serviceIntent)
navController.navigateUp()
}
@ -182,9 +187,13 @@ private fun TopAppBar(navController: NavController, ua: UserAgent, callHistory:
),
windowInsets = WindowInsets(0, 0, 0, 0),
navigationIcon = {
val ctx = LocalContext.current
IconButton(
onClick = {
account.missedCalls = false
val serviceIntent = Intent(ctx, BaresipService::class.java)
serviceIntent.action = "Clear Missed"
serviceIntent.putExtra("uap", ua.uap)
ctx.startService(serviceIntent)
navController.navigateUp()
}
) {
@ -495,7 +504,7 @@ private fun Calls(
count++
}
if (count > 3)
Text("...", color = MaterialTheme.colorScheme.onBackground)
Text(ctx.getString(R.string.dots), color = MaterialTheme.colorScheme.onBackground)
Text(
text = Utils.friendlyUri(ctx, peerUri, ua.account),
modifier = Modifier.padding(start = 8.dp),

View File

@ -1,8 +1,8 @@
package com.tutpro.baresip
import android.content.Context
import android.content.Intent
import android.text.format.DateUtils.isToday
import android.util.Log
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.ExperimentalFoundationApi
@ -102,7 +102,6 @@ fun NavGraphBuilder.chatScreenRoute(navController: NavController, viewModel: Vie
val aor = backStackEntry.arguments?.getString("aor")!!
val peerUri = backStackEntry.arguments?.getString("peer")!!
ChatScreen(
ctx = LocalContext.current,
navController = navController,
viewModel = viewModel,
account = Account.ofAor(aor)!!,
@ -113,13 +112,13 @@ fun NavGraphBuilder.chatScreenRoute(navController: NavController, viewModel: Vie
@Composable
private fun ChatScreen(
ctx: Context,
navController: NavController,
viewModel: ViewModel,
account: Account,
peerUri: String
) {
val lifecycleOwner = LocalLifecycleOwner.current
val ctx = LocalContext.current
val aor = account.aor
@ -164,6 +163,11 @@ private fun ChatScreen(
}
BackHandler(enabled = true) {
val serviceIntent = Intent(ctx, BaresipService::class.java)
serviceIntent.action = "Clear Unread"
serviceIntent.putExtra("uap", account.accp)
serviceIntent.putExtra("peer", peerUri)
ctx.startService(serviceIntent)
backAction(navController, account, peerUri)
}
@ -173,12 +177,11 @@ private fun ChatScreen(
topBar = {
Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
Spacer(Modifier.statusBarsPadding())
TopAppBar(ctx, navController, viewModel, account, peerUri)
TopAppBar(navController, viewModel, account, peerUri)
}
},
bottomBar = {
NewMessage(
ctx = ctx,
viewModel,
account = account,
peerUri = peerUri,
@ -188,7 +191,6 @@ private fun ChatScreen(
content = { contentPadding ->
if (areMessagesLoaded)
ChatContent(
ctx,
contentPadding,
account, peerUri,
chatMessages,
@ -201,12 +203,12 @@ private fun ChatScreen(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopAppBar(
ctx: Context,
navController: NavController,
viewModel: ViewModel,
account: Account,
peerUri: String
) {
val ctx = LocalContext.current
val aor = account.aor
TopAppBar(
title = {
@ -223,7 +225,17 @@ private fun TopAppBar(
actionIconContentColor = MaterialTheme.colorScheme.onPrimary
),
navigationIcon = {
IconButton(onClick = { backAction(navController, account, peerUri) }) {
val ctx = LocalContext.current
IconButton(
onClick = {
val serviceIntent = Intent(ctx, BaresipService::class.java)
serviceIntent.action = "Clear Unread"
serviceIntent.putExtra("uap", account.accp)
serviceIntent.putExtra("peer", peerUri)
ctx.startService(serviceIntent)
backAction(navController, account, peerUri)
}
) {
Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
},
@ -282,7 +294,6 @@ private fun TopAppBar(
@Composable
private fun ChatContent(
ctx: Context,
contentPadding: PaddingValues,
account: Account,
peerUri: String,
@ -293,14 +304,15 @@ private fun ChatContent(
modifier = Modifier.fillMaxWidth().padding(contentPadding),
verticalArrangement = Arrangement.Bottom
) {
Account(ctx, account)
Account(account)
Spacer(modifier = Modifier.weight(1f))
Messages(ctx, account, peerUri, messages, onMessageDeleted)
Messages(account, peerUri, messages, onMessageDeleted)
}
}
@Composable
private fun Account(ctx: Context, account: Account) {
private fun Account(account: Account) {
val ctx = LocalContext.current
Text(
text = ctx.getString(R.string.account) + " " + account.text(),
modifier = Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 8.dp),
@ -312,12 +324,12 @@ private fun Account(ctx: Context, account: Account) {
@Composable
private fun Messages(
ctx: Context,
account: Account,
peerUri: String,
messages: List<Message>,
onMessageDeleted: () -> Unit
) {
val ctx = LocalContext.current
val peerName = Utils.friendlyUri(ctx, peerUri, account)
val showDialog = remember { mutableStateOf(false) }
@ -444,13 +456,12 @@ private fun Messages(
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun NewMessage(
ctx: Context,
viewModel: ViewModel,
account: Account,
peerUri: String,
addMessage: (message: Message) -> Unit
) {
val ctx = LocalContext.current
val aor = account.aor
val ua = UserAgent.ofAor(aor)!!

View File

@ -1,6 +1,7 @@
package com.tutpro.baresip
import android.content.Context
import android.content.Intent
import androidx.activity.compose.BackHandler
import android.text.format.DateUtils.isToday
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.BorderStroke
@ -13,11 +14,9 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
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.navigationBarsPadding
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
@ -129,8 +128,15 @@ private fun ChatsScreen(navController: NavController, aor: String) {
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}
BackHandler(enabled = true) {
val serviceIntent = Intent(ctx, BaresipService::class.java)
serviceIntent.action = "Clear Unread"
serviceIntent.putExtra("uap", account.accp)
ctx.startService(serviceIntent)
navController.navigateUp()
}
Scaffold(
modifier = Modifier.fillMaxSize().imePadding(),
containerColor = MaterialTheme.colorScheme.background,
topBar = {
Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
@ -138,10 +144,10 @@ private fun ChatsScreen(navController: NavController, aor: String) {
TopAppBar(navController, account, uaMessages)
}
},
bottomBar = { NewChatPeer(ctx, navController, account) },
bottomBar = { NewChatPeer(navController, account) },
content = { contentPadding ->
if (areMessagesLoaded)
ChatsContent(LocalContext.current, navController, contentPadding, account, uaMessages)
ChatsContent(navController, contentPadding, account, uaMessages)
},
)
}
@ -178,7 +184,16 @@ private fun TopAppBar(
actionIconContentColor = MaterialTheme.colorScheme.onPrimary
),
navigationIcon = {
IconButton(onClick = { navController.navigateUp() }) {
val ctx = LocalContext.current
IconButton(
onClick = {
val serviceIntent = Intent(ctx, BaresipService::class.java)
serviceIntent.action = "Clear Unread"
serviceIntent.putExtra("uap", account.accp)
ctx.startService(serviceIntent)
navController.navigateUp()
}
) {
Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
}
},
@ -214,7 +229,6 @@ private fun TopAppBar(
@Composable
private fun ChatsContent(
ctx: Context,
navController: NavController,
contentPadding: PaddingValues,
account: Account,
@ -225,7 +239,7 @@ private fun ChatsContent(
verticalArrangement = Arrangement.Top
) {
Account(account)
Chats(ctx, navController, account, uaMessages)
Chats(navController, account, uaMessages)
}
}
@ -242,11 +256,11 @@ private fun Account(account: Account) {
@Composable
private fun Chats(
ctx: Context,
navController: NavController,
account: Account,
uaMessages: MutableState<List<Message>>
) {
val ctx = LocalContext.current
val aor = account.aor
val showDialog = remember { mutableStateOf(false) }
@ -409,13 +423,14 @@ private fun Chats(
}
@Composable
private fun NewChatPeer(ctx: Context, navController: NavController, account: Account) {
private fun NewChatPeer(navController: NavController, account: Account) {
val ctx = LocalContext.current
val alertTitle = remember { mutableStateOf("") }
val alertMessage = remember { mutableStateOf("") }
val showAlert = remember { mutableStateOf(false) }
fun makeChat(ctx: Context, navController: NavController, account: Account, chatPeer: String) {
fun makeChat(navController: NavController, account: Account, chatPeer: String) {
val peerUri = if (Utils.isTelNumber(chatPeer))
"tel:$chatPeer"
else
@ -587,15 +602,15 @@ private fun NewChatPeer(ctx: Context, navController: NavController, account: Acc
String.format(ctx.getString(R.string.contact_no_sip_or_tel_uri), peerText)
showAlert.value = true
} else {
makeChat(ctx, navController, account, peerText)
makeChat(navController, account, peerText)
}
}
else if (uris.size == 1)
makeChat(ctx, navController, account, uris[0].uri)
makeChat(navController, account, uris[0].uri)
else {
items.value = uris.map { it.label.ifEmpty { it.uri.substringAfter(":") } }
itemAction.value = { index ->
makeChat(ctx, navController, account, uris[index].uri)
makeChat(navController, account, uris[index].uri)
}
showDialog.value = true
}

View File

@ -20,6 +20,7 @@ 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
@ -299,13 +300,16 @@ class MainActivity : ComponentActivity() {
override fun onStart() {
super.onStart()
Log.i(TAG, "Main 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 }) {
@ -361,16 +365,20 @@ class MainActivity : ComponentActivity() {
this.setShowWhenLocked(true)
this.setTurnScreenOn(true)
Log.d(TAG, "onNewIntent with action/data '${intent.action}/${intent.data}'")
Log.d(TAG, "onNewIntent action/type/data: ${intent.action}/${intent.type}/${intent.data}")
when (intent.action) {
ACTION_DIAL, ACTION_CALL, ACTION_VIEW ->
when {
isCallLogIntent(intent) -> {
handleCallLogIntent()
}
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) {
@ -400,6 +408,19 @@ class MainActivity : ComponentActivity() {
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,