Work on notification improvements
@ -20,11 +20,12 @@ import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
|||||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
|
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.database.ContentObserver
|
import android.database.ContentObserver
|
||||||
|
import android.graphics.ImageDecoder
|
||||||
|
import android.media.AudioAttributes
|
||||||
import android.media.AudioDeviceInfo
|
import android.media.AudioDeviceInfo
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.media.AudioManager.MODE_IN_COMMUNICATION
|
import android.media.AudioManager.MODE_IN_COMMUNICATION
|
||||||
import android.media.AudioManager.MODE_NORMAL
|
import android.media.AudioManager.MODE_NORMAL
|
||||||
import android.media.AudioManager.RINGER_MODE_SILENT
|
|
||||||
import android.media.MediaPlayer
|
import android.media.MediaPlayer
|
||||||
import android.media.Ringtone
|
import android.media.Ringtone
|
||||||
import android.media.RingtoneManager
|
import android.media.RingtoneManager
|
||||||
@ -65,11 +66,13 @@ import androidx.core.app.NotificationCompat.MessagingStyle
|
|||||||
import androidx.core.app.Person
|
import androidx.core.app.Person
|
||||||
import androidx.core.app.RemoteInput
|
import androidx.core.app.RemoteInput
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.media.AudioAttributesCompat
|
import androidx.media.AudioAttributesCompat
|
||||||
import androidx.media.AudioFocusRequestCompat
|
import androidx.media.AudioFocusRequestCompat
|
||||||
import androidx.media.AudioManagerCompat
|
import androidx.media.AudioManagerCompat
|
||||||
|
import com.tutpro.baresip.Utils.toCircle
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
@ -885,7 +888,7 @@ class BaresipService: Service() {
|
|||||||
startActivity(newIntent)
|
startActivity(newIntent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val channelId = if (shouldVibrate()) MEDIUM_CHANNEL_ID else HIGH_CHANNEL_ID
|
val channelId = if (shouldVibrate()) HIGH_CHANNEL_ID else MEDIUM_CHANNEL_ID
|
||||||
val callerNumber = peerUri.split(":")[1].split("@")[0]
|
val callerNumber = peerUri.split(":")[1].split("@")[0]
|
||||||
if (shouldStartRinging(channelId, callerNumber))
|
if (shouldStartRinging(channelId, callerNumber))
|
||||||
startRinging()
|
startRinging()
|
||||||
@ -899,15 +902,40 @@ class BaresipService: Service() {
|
|||||||
val pi = PendingIntent.getActivity(applicationContext, CALL_REQ_CODE, intent, piFlags)
|
val pi = PendingIntent.getActivity(applicationContext, CALL_REQ_CODE, intent, piFlags)
|
||||||
val nb = NotificationCompat.Builder(this, channelId)
|
val nb = NotificationCompat.Builder(this, channelId)
|
||||||
val caller = Utils.friendlyUri(this, peerUri, ua.account)
|
val caller = Utils.friendlyUri(this, peerUri, ua.account)
|
||||||
val person = Person.Builder().setName(caller).build()
|
val callerContact = Contact.findContact(peerUri)
|
||||||
nb.setSmallIcon(R.drawable.ic_stat_call)
|
val personBuilder = Person.Builder().setName(caller)
|
||||||
|
val contactColor = callerContact?.color() ?: "#B0B0B0"
|
||||||
|
val initial = if (caller.isNotEmpty()) caller.take(1) else "?"
|
||||||
|
val textAvatarBitmap = Utils.createTextAvatar(this, initial,contactColor)
|
||||||
|
var icon = IconCompat.createWithBitmap(textAvatarBitmap)
|
||||||
|
if (callerContact is Contact.BaresipContact) {
|
||||||
|
if (callerContact.avatarImage != null)
|
||||||
|
icon = IconCompat.createWithBitmap(callerContact.avatarImage!!.toCircle())
|
||||||
|
}
|
||||||
|
else if (callerContact is Contact.AndroidContact) {
|
||||||
|
// AndroidContact has a URI, we must decode it
|
||||||
|
if (callerContact.thumbnailUri != null) {
|
||||||
|
try {
|
||||||
|
val source = ImageDecoder.createSource(contentResolver,
|
||||||
|
callerContact.thumbnailUri!!)
|
||||||
|
val bitmap = ImageDecoder.decodeBitmap(source) { decoder, _, _ ->
|
||||||
|
decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
|
||||||
|
// decoder.setTargetSize(256, 256)
|
||||||
|
}
|
||||||
|
icon = IconCompat.createWithBitmap(bitmap.toCircle())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to load Android contact avatar: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val person = personBuilder.setIcon(icon).build()
|
||||||
|
nb.setSmallIcon(R.drawable.ic_notification_call)
|
||||||
.setColor(ContextCompat.getColor(this, R.color.colorPrimary))
|
.setColor(ContextCompat.getColor(this, R.color.colorPrimary))
|
||||||
.setContentIntent(pi)
|
.setContentIntent(pi)
|
||||||
.setCategory(Notification.CATEGORY_CALL)
|
.setCategory(Notification.CATEGORY_CALL)
|
||||||
.setAutoCancel(false)
|
.setAutoCancel(false)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
.setContentTitle(getString(R.string.incoming_call_from))
|
.setContentText(getString(R.string.is_calling))
|
||||||
.setContentText(caller)
|
|
||||||
.setWhen(System.currentTimeMillis())
|
.setWhen(System.currentTimeMillis())
|
||||||
.setShowWhen(true)
|
.setShowWhen(true)
|
||||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
@ -985,7 +1013,7 @@ class BaresipService: Service() {
|
|||||||
val pi = PendingIntent.getActivity(applicationContext, TRANSFER_REQ_CODE, intent, piFlags)
|
val pi = PendingIntent.getActivity(applicationContext, TRANSFER_REQ_CODE, intent, piFlags)
|
||||||
val nb = NotificationCompat.Builder(this, HIGH_CHANNEL_ID)
|
val nb = NotificationCompat.Builder(this, HIGH_CHANNEL_ID)
|
||||||
val target = Utils.friendlyUri(this, ev[1], ua.account)
|
val target = Utils.friendlyUri(this, ev[1], ua.account)
|
||||||
nb.setSmallIcon(R.drawable.ic_stat_call)
|
nb.setSmallIcon(R.drawable.ic_notification_call)
|
||||||
.setColor(ContextCompat.getColor(this, R.color.colorPrimary))
|
.setColor(ContextCompat.getColor(this, R.color.colorPrimary))
|
||||||
.setContentIntent(pi)
|
.setContentIntent(pi)
|
||||||
.setDefaults(Notification.DEFAULT_SOUND)
|
.setDefaults(Notification.DEFAULT_SOUND)
|
||||||
@ -1004,8 +1032,10 @@ class BaresipService: Service() {
|
|||||||
denyIntent.putExtra("callp", callp)
|
denyIntent.putExtra("callp", callp)
|
||||||
val denyPendingIntent = PendingIntent.getService(this,
|
val denyPendingIntent = PendingIntent.getService(this,
|
||||||
DENY_REQ_CODE, denyIntent, piFlags)
|
DENY_REQ_CODE, denyIntent, piFlags)
|
||||||
nb.addAction(R.drawable.ic_stat_call, getString(R.string.accept), acceptPendingIntent)
|
nb.addAction(R.drawable.ic_notification_call,
|
||||||
nb.addAction(R.drawable.ic_stat_call_end, getString(R.string.deny), denyPendingIntent)
|
getString(R.string.accept), acceptPendingIntent)
|
||||||
|
nb.addAction(R.drawable.ic_notification_call_end,
|
||||||
|
getString(R.string.deny), denyPendingIntent)
|
||||||
nm.notify(TRANSFER_NOTIFICATION_ID, nb.build())
|
nm.notify(TRANSFER_NOTIFICATION_ID, nb.build())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1082,7 +1112,7 @@ class BaresipService: Service() {
|
|||||||
val pi = PendingIntent.getActivity(applicationContext, CALL_REQ_CODE,
|
val pi = PendingIntent.getActivity(applicationContext, CALL_REQ_CODE,
|
||||||
intent, piFlags)
|
intent, piFlags)
|
||||||
val nb = NotificationCompat.Builder(this, HIGH_CHANNEL_ID)
|
val nb = NotificationCompat.Builder(this, HIGH_CHANNEL_ID)
|
||||||
nb.setSmallIcon(R.drawable.ic_stat_call_missed)
|
nb.setSmallIcon(R.drawable.ic_notification_call_missed)
|
||||||
.setColor(ContextCompat.getColor(this, R.color.colorPrimary))
|
.setColor(ContextCompat.getColor(this, R.color.colorPrimary))
|
||||||
.setContentIntent(pi)
|
.setContentIntent(pi)
|
||||||
.setCategory(Notification.CATEGORY_CALL)
|
.setCategory(Notification.CATEGORY_CALL)
|
||||||
@ -1167,11 +1197,34 @@ class BaresipService: Service() {
|
|||||||
val pi = PendingIntent.getActivity(applicationContext, MESSAGE_REQ_CODE, intent, piFlags)
|
val pi = PendingIntent.getActivity(applicationContext, MESSAGE_REQ_CODE, intent, piFlags)
|
||||||
|
|
||||||
// message notification builder
|
// message notification builder
|
||||||
val senderDisplayName = Utils.friendlyUri(this, peerUri, ua.account)
|
val sender = Utils.friendlyUri(this, peerUri, ua.account)
|
||||||
val senderPerson = Person.Builder()
|
val senderContact = Contact.findContact(peerUri)
|
||||||
.setName(senderDisplayName)
|
val personBuilder = Person.Builder().setName(sender)
|
||||||
.setKey(peerUri)
|
val contactColor = senderContact?.color() ?: "#B0B0B0"
|
||||||
.build()
|
val initial = if (sender.isNotEmpty()) sender.take(1) else "?"
|
||||||
|
val textAvatarBitmap = Utils.createTextAvatar(this, initial,contactColor)
|
||||||
|
var icon = IconCompat.createWithBitmap(textAvatarBitmap)
|
||||||
|
if (senderContact is Contact.BaresipContact) {
|
||||||
|
if (senderContact.avatarImage != null)
|
||||||
|
icon = IconCompat.createWithBitmap(senderContact.avatarImage!!.toCircle())
|
||||||
|
}
|
||||||
|
else if (senderContact is Contact.AndroidContact) {
|
||||||
|
// AndroidContact has a URI, we must decode it
|
||||||
|
if (senderContact.thumbnailUri != null) {
|
||||||
|
try {
|
||||||
|
val source = ImageDecoder.createSource(contentResolver,
|
||||||
|
senderContact.thumbnailUri!!)
|
||||||
|
val bitmap = ImageDecoder.decodeBitmap(source) { decoder, _, _ ->
|
||||||
|
decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
|
||||||
|
// decoder.setTargetSize(256, 256)
|
||||||
|
}
|
||||||
|
icon = IconCompat.createWithBitmap(bitmap.toCircle())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to load Android contact avatar: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val senderPerson = personBuilder.setIcon(icon).build()
|
||||||
val localUserPerson = Person.Builder()
|
val localUserPerson = Person.Builder()
|
||||||
.setName(getString(R.string.you))
|
.setName(getString(R.string.you))
|
||||||
.setKey(ua.account.aor)
|
.setKey(ua.account.aor)
|
||||||
@ -1181,7 +1234,7 @@ class BaresipService: Service() {
|
|||||||
.setGroupConversation(false)
|
.setGroupConversation(false)
|
||||||
.addMessage(text, timeStamp, senderPerson)
|
.addMessage(text, timeStamp, senderPerson)
|
||||||
val nb = NotificationCompat.Builder(this, HIGH_CHANNEL_ID)
|
val nb = NotificationCompat.Builder(this, HIGH_CHANNEL_ID)
|
||||||
.setSmallIcon(R.drawable.ic_stat_message)
|
.setSmallIcon(R.drawable.ic_notification_message)
|
||||||
.setColor(ContextCompat.getColor(this, R.color.colorPrimary))
|
.setColor(ContextCompat.getColor(this, R.color.colorPrimary))
|
||||||
.setContentIntent(pi)
|
.setContentIntent(pi)
|
||||||
.setSound(Settings.System.DEFAULT_NOTIFICATION_URI)
|
.setSound(Settings.System.DEFAULT_NOTIFICATION_URI)
|
||||||
@ -1190,7 +1243,7 @@ class BaresipService: Service() {
|
|||||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
|
||||||
// messafe inline reply
|
// message inline reply
|
||||||
val remoteInput = RemoteInput.Builder(KEY_TEXT_REPLY)
|
val remoteInput = RemoteInput.Builder(KEY_TEXT_REPLY)
|
||||||
.setLabel(getString(R.string.reply))
|
.setLabel(getString(R.string.reply))
|
||||||
.build()
|
.build()
|
||||||
@ -1204,7 +1257,7 @@ class BaresipService: Service() {
|
|||||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
|
||||||
)
|
)
|
||||||
val inlineReplyAction = NotificationCompat.Action.Builder(
|
val inlineReplyAction = NotificationCompat.Action.Builder(
|
||||||
R.drawable.ic_stat_reply,
|
R.drawable.ic_notification_reply,
|
||||||
getString(R.string.reply),
|
getString(R.string.reply),
|
||||||
directReplyPendingIntent
|
directReplyPendingIntent
|
||||||
).addRemoteInput(remoteInput)
|
).addRemoteInput(remoteInput)
|
||||||
@ -1218,7 +1271,7 @@ class BaresipService: Service() {
|
|||||||
saveIntent.putExtra("uap", uap).putExtra("time", timeStampString)
|
saveIntent.putExtra("uap", uap).putExtra("time", timeStampString)
|
||||||
val savePendingIntent = PendingIntent.getService(this, SAVE_REQ_CODE, saveIntent, piFlags)
|
val savePendingIntent = PendingIntent.getService(this, SAVE_REQ_CODE, saveIntent, piFlags)
|
||||||
val saveAction = NotificationCompat.Action.Builder(
|
val saveAction = NotificationCompat.Action.Builder(
|
||||||
R.drawable.ic_stat_save,
|
R.drawable.ic_notification_save,
|
||||||
getString(R.string.save),
|
getString(R.string.save),
|
||||||
savePendingIntent
|
savePendingIntent
|
||||||
).build()
|
).build()
|
||||||
@ -1229,7 +1282,7 @@ class BaresipService: Service() {
|
|||||||
deleteIntent.putExtra("uap", uap).putExtra("time", timeStampString)
|
deleteIntent.putExtra("uap", uap).putExtra("time", timeStampString)
|
||||||
val deletePendingIntent = PendingIntent.getService(this, DELETE_REQ_CODE, deleteIntent, piFlags)
|
val deletePendingIntent = PendingIntent.getService(this, DELETE_REQ_CODE, deleteIntent, piFlags)
|
||||||
val deleteAction = NotificationCompat.Action.Builder(
|
val deleteAction = NotificationCompat.Action.Builder(
|
||||||
R.drawable.ic_stat_delete,
|
R.drawable.ic_notification_delete,
|
||||||
getString(R.string.delete),
|
getString(R.string.delete),
|
||||||
deletePendingIntent
|
deletePendingIntent
|
||||||
).build()
|
).build()
|
||||||
@ -1294,22 +1347,29 @@ class BaresipService: Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun createNotificationChannels() {
|
private fun createNotificationChannels() {
|
||||||
val lowChannel = NotificationChannel(LOW_CHANNEL_ID, "No sound or vibrate",
|
val lowChannel = NotificationChannel(LOW_CHANNEL_ID, "No sound, no vibrate",
|
||||||
NotificationManager.IMPORTANCE_LOW)
|
NotificationManager.IMPORTANCE_LOW)
|
||||||
|
lowChannel.description = "Background status notifications"
|
||||||
lowChannel.enableVibration(false)
|
lowChannel.enableVibration(false)
|
||||||
lowChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
lowChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
||||||
nm.createNotificationChannel(lowChannel)
|
nm.createNotificationChannel(lowChannel)
|
||||||
|
val ringAttributes = AudioAttributes.Builder()
|
||||||
val highChannel = NotificationChannel(HIGH_CHANNEL_ID, "Sound and vibrate",
|
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||||
|
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
|
||||||
|
.build()
|
||||||
|
val highChannel = NotificationChannel(HIGH_CHANNEL_ID, "Sound, vibrate, and peek",
|
||||||
NotificationManager.IMPORTANCE_HIGH)
|
NotificationManager.IMPORTANCE_HIGH)
|
||||||
|
highChannel.description = "Incoming calls and important alerts"
|
||||||
highChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
highChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
||||||
highChannel.enableVibration(true)
|
highChannel.enableVibration(true)
|
||||||
|
highChannel.setSound(Settings.System.DEFAULT_NOTIFICATION_URI, ringAttributes)
|
||||||
nm.createNotificationChannel(highChannel)
|
nm.createNotificationChannel(highChannel)
|
||||||
|
val mediumChannel = NotificationChannel(MEDIUM_CHANNEL_ID, "Sound only",
|
||||||
val mediumChannel = NotificationChannel(MEDIUM_CHANNEL_ID, "Sound",
|
NotificationManager.IMPORTANCE_DEFAULT)
|
||||||
NotificationManager.IMPORTANCE_HIGH)
|
mediumChannel.description = "Incoming messages"
|
||||||
mediumChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
mediumChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
||||||
mediumChannel.enableVibration(false)
|
mediumChannel.enableVibration(false)
|
||||||
|
mediumChannel.setSound(Settings.System.DEFAULT_NOTIFICATION_URI, ringAttributes)
|
||||||
nm.createNotificationChannel(mediumChannel)
|
nm.createNotificationChannel(mediumChannel)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1322,7 +1382,7 @@ class BaresipService: Service() {
|
|||||||
PendingIntent.FLAG_IMMUTABLE)
|
PendingIntent.FLAG_IMMUTABLE)
|
||||||
val notificationLayout = RemoteViews(packageName, R.layout.status_notification)
|
val notificationLayout = RemoteViews(packageName, R.layout.status_notification)
|
||||||
snb.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
snb.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
.setSmallIcon(R.drawable.ic_stat)
|
.setSmallIcon(R.drawable.ic_notification_b)
|
||||||
.setContentIntent(pi)
|
.setContentIntent(pi)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
.setStyle(NotificationCompat.DecoratedCustomViewStyle())
|
.setStyle(NotificationCompat.DecoratedCustomViewStyle())
|
||||||
@ -1413,18 +1473,23 @@ class BaresipService: Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun shouldVibrate(): Boolean {
|
private fun shouldVibrate(): Boolean {
|
||||||
return if (am.ringerMode != RINGER_MODE_SILENT)
|
// 1. If the phone is in Silent mode, never vibrate
|
||||||
if (am.ringerMode == AudioManager.RINGER_MODE_VIBRATE)
|
if (am.ringerMode == AudioManager.RINGER_MODE_SILENT) return false
|
||||||
true
|
// 2. If the phone is in Vibrate mode, always vibrate
|
||||||
else
|
if (am.ringerMode == AudioManager.RINGER_MODE_VIBRATE) return true
|
||||||
if (am.getStreamVolume(AudioManager.STREAM_RING) != 0)
|
// 3. If the phone is in Normal (Ringing) mode:
|
||||||
|
// First, check if the ringer volume is actually non-zero
|
||||||
|
if (am.getStreamVolume(AudioManager.STREAM_RING) == 0) return false
|
||||||
|
// Finally, check the system "Vibrate for calls" setting.
|
||||||
|
// Although deprecated, it is the standard way to check the "Also vibrate for calls" toggle.
|
||||||
|
return try {
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
Settings.System.getInt(contentResolver, Settings.System.VIBRATE_WHEN_RINGING, 0) != 0
|
Settings.System.getInt(contentResolver, Settings.System.VIBRATE_WHEN_RINGING, 0) != 0
|
||||||
else
|
} catch (_: Exception) {
|
||||||
false
|
// If the setting can't be read, default to FALSE to be non-intrusive.
|
||||||
else
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun isStarredContact(callerNumber: String): Boolean {
|
private fun isStarredContact(callerNumber: String): Boolean {
|
||||||
if (contactsMode == "baresip" || callerNumber.isBlank())
|
if (contactsMode == "baresip" || callerNumber.isBlank())
|
||||||
|
|||||||
@ -44,6 +44,16 @@ sealed class Contact {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun color(): String {
|
||||||
|
val intColor = when (this) {
|
||||||
|
is AndroidContact -> color
|
||||||
|
is BaresipContact -> color
|
||||||
|
}
|
||||||
|
// Mask with 0xFFFFFF to ensure we get the standard 6-character hex code (RRGGBB)
|
||||||
|
// ignoring the alpha channel for compatibility with simple string parsers.
|
||||||
|
return String.format("#%06X", 0xFFFFFF and intColor)
|
||||||
|
}
|
||||||
|
|
||||||
fun copy(): Contact {
|
fun copy(): Contact {
|
||||||
val copy = when (this) {
|
val copy = when (this) {
|
||||||
is BaresipContact ->
|
is BaresipContact ->
|
||||||
|
|||||||
@ -8,6 +8,9 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.PorterDuff
|
||||||
|
import android.graphics.PorterDuffXfermode
|
||||||
import android.media.AudioAttributes
|
import android.media.AudioAttributes
|
||||||
import android.media.AudioDeviceInfo
|
import android.media.AudioDeviceInfo
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
@ -71,7 +74,7 @@ import javax.net.ssl.HttpsURLConnection
|
|||||||
import javax.net.ssl.SSLContext
|
import javax.net.ssl.SSLContext
|
||||||
import javax.net.ssl.TrustManagerFactory
|
import javax.net.ssl.TrustManagerFactory
|
||||||
import javax.net.ssl.X509TrustManager
|
import javax.net.ssl.X509TrustManager
|
||||||
import kotlin.text.replaceFirstChar
|
import androidx.core.graphics.toColorInt
|
||||||
|
|
||||||
object Utils {
|
object Utils {
|
||||||
|
|
||||||
@ -169,7 +172,7 @@ object Utils {
|
|||||||
|
|
||||||
private fun e164Uri(uri: String, countryCode: String): String {
|
private fun e164Uri(uri: String, countryCode: String): String {
|
||||||
if (countryCode == "") return uri
|
if (countryCode == "") return uri
|
||||||
val scheme = uri.substring(0, 4)
|
val scheme = uri.take(4)
|
||||||
val userPart = uriUserPart(uri)
|
val userPart = uriUserPart(uri)
|
||||||
return if (userPart.isDigitsOnly()) {
|
return if (userPart.isDigitsOnly()) {
|
||||||
when {
|
when {
|
||||||
@ -1083,6 +1086,65 @@ object Utils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Bitmap.toCircle(): Bitmap {
|
||||||
|
// Use full package names to avoid conflict with Compose classes
|
||||||
|
val output = androidx.core.graphics.createBitmap(this.width, this.height, Bitmap.Config.ARGB_8888)
|
||||||
|
val canvas = android.graphics.Canvas(output)
|
||||||
|
val paint = android.graphics.Paint()
|
||||||
|
val rect = android.graphics.Rect(0, 0, this.width, this.height)
|
||||||
|
|
||||||
|
paint.isAntiAlias = true
|
||||||
|
canvas.drawARGB(0, 0, 0, 0)
|
||||||
|
|
||||||
|
canvas.drawCircle(this.width / 2f, this.height / 2f, this.width / 2f, paint)
|
||||||
|
|
||||||
|
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
|
||||||
|
canvas.drawBitmap(this, rect, rect, paint)
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createTextAvatar(context: Context, letter: String, colorHex: String): Bitmap {
|
||||||
|
// Standard notification large icon size is usually 64dp or 48dp.
|
||||||
|
// We use a decent resolution (e.g. 128x128) and let Android scale it down.
|
||||||
|
val size = 128
|
||||||
|
|
||||||
|
// Use KTX createBitmap to match toCircle style
|
||||||
|
val bitmap = androidx.core.graphics.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
||||||
|
|
||||||
|
// Use Fully Qualified Names to avoid Compose conflicts
|
||||||
|
val canvas = android.graphics.Canvas(bitmap)
|
||||||
|
|
||||||
|
// 1. Draw the colored circle background
|
||||||
|
val bgPaint = android.graphics.Paint()
|
||||||
|
bgPaint.isAntiAlias = true
|
||||||
|
try {
|
||||||
|
bgPaint.color = colorHex.toColorInt()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
bgPaint.color = android.graphics.Color.GRAY // Fallback color
|
||||||
|
}
|
||||||
|
canvas.drawCircle(size / 2f, size / 2f, size / 2f, bgPaint)
|
||||||
|
|
||||||
|
// 2. Draw the text (Initial)
|
||||||
|
val textPaint = android.graphics.Paint()
|
||||||
|
textPaint.isAntiAlias = true
|
||||||
|
textPaint.color = android.graphics.Color.WHITE
|
||||||
|
textPaint.textSize = size / 2f // Text size is half the circle size
|
||||||
|
textPaint.textAlign = android.graphics.Paint.Align.CENTER
|
||||||
|
|
||||||
|
// Use a bold font if possible to match your UI
|
||||||
|
textPaint.typeface = android.graphics.Typeface.create(android.graphics.Typeface.DEFAULT, android.graphics.Typeface.BOLD)
|
||||||
|
|
||||||
|
// Calculate vertical center to center the text properly
|
||||||
|
val bounds = android.graphics.Rect()
|
||||||
|
textPaint.getTextBounds(letter, 0, letter.length, bounds)
|
||||||
|
val yOffset = (bounds.bottom - bounds.top) / 2f
|
||||||
|
|
||||||
|
// Draw text at Center X, Center Y + half text height (to visually center)
|
||||||
|
canvas.drawText(letter.uppercase(), size / 2f, (size / 2f) + (yOffset / 2) + (bounds.height()/4), textPaint)
|
||||||
|
|
||||||
|
return bitmap
|
||||||
|
}
|
||||||
|
|
||||||
/*fun listFilesInDirectory(directoryPath: String): List<File> {
|
/*fun listFilesInDirectory(directoryPath: String): List<File> {
|
||||||
val directory = File(directoryPath)
|
val directory = File(directoryPath)
|
||||||
if (!directory.exists()) {
|
if (!directory.exists()) {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 408 B |
|
Before Width: | Height: | Size: 595 B |
|
Before Width: | Height: | Size: 422 B |
|
Before Width: | Height: | Size: 629 B |
|
Before Width: | Height: | Size: 309 B |
|
Before Width: | Height: | Size: 362 B |
|
Before Width: | Height: | Size: 531 B |
|
Before Width: | Height: | Size: 513 B |
|
Before Width: | Height: | Size: 294 B |
|
Before Width: | Height: | Size: 342 B |
|
Before Width: | Height: | Size: 351 B |
|
Before Width: | Height: | Size: 492 B |
|
Before Width: | Height: | Size: 198 B |
|
Before Width: | Height: | Size: 248 B |
|
Before Width: | Height: | Size: 331 B |
|
Before Width: | Height: | Size: 286 B |
|
Before Width: | Height: | Size: 533 B |
|
Before Width: | Height: | Size: 680 B |
|
Before Width: | Height: | Size: 592 B |
|
Before Width: | Height: | Size: 961 B |
|
Before Width: | Height: | Size: 274 B |
|
Before Width: | Height: | Size: 332 B |
|
Before Width: | Height: | Size: 599 B |
|
Before Width: | Height: | Size: 528 B |
|
Before Width: | Height: | Size: 785 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 780 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 495 B |
|
Before Width: | Height: | Size: 587 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 924 B |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 547 B |
|
Before Width: | Height: | Size: 592 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
9
app/src/main/res/drawable/ic_notification_b.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportHeight="48"
|
||||||
|
android:viewportWidth="48"
|
||||||
|
android:width="24dp">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M11,3C14.3,3 17.6,3 21,3C21,7.95 21,12.9 21,18C22.196,17.752 23.392,17.505 24.625,17.25C28.132,16.815 29.004,17.002 32.375,18.625C35.839,21.759 36.748,23.687 37.25,28.297C37.547,34.427 36.694,38.075 33,43C29.85,45.415 26.884,45.324 23,45C22.299,44.629 21.597,44.257 20.875,43.875C19.947,43.442 19.947,43.442 19,43C16.765,43.783 16.765,43.783 15,45C14,44 14,44 13.886,40.49C13.887,38.93 13.892,37.369 13.902,35.809C13.904,34.579 13.904,34.579 13.907,33.324C13.912,30.695 13.925,28.066 13.938,25.438C13.943,23.66 13.947,21.882 13.951,20.104C13.962,15.736 13.979,11.368 14,7C13.01,7 12.02,7 11,7C11,5.68 11,4.36 11,3ZM21,23C20.774,25.692 20.676,28.247 20.688,30.938C20.671,31.666 20.655,32.395 20.639,33.146C20.602,37.369 20.602,37.369 22.52,41.004C23.008,41.333 23.497,41.661 24,42C26.376,41.05 27.732,40.425 29.117,38.23C31.041,33.369 30.568,27.774 29,22.875C28.67,22.256 28.34,21.638 28,21C24.045,20.652 24.045,20.652 21,23Z"/>
|
||||||
|
</vector>
|
||||||
9
app/src/main/res/drawable/ic_notification_call.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M798,840Q673,840 551,785.5Q429,731 329,631Q229,531 174.5,409Q120,287 120,162Q120,144 132,132Q144,120 162,120L324,120Q338,120 349,129.5Q360,139 362,152L388,292Q390,308 387,319Q384,330 376,338L279,436Q299,473 326.5,507.5Q354,542 387,574Q418,605 452,631.5Q486,658 524,680L618,586Q627,577 641.5,572.5Q656,568 670,570L808,598Q822,602 831,612.5Q840,623 840,636L840,798Q840,816 828,828Q816,840 798,840ZM241,360L307,294Q307,294 307,294Q307,294 307,294L290,200Q290,200 290,200Q290,200 290,200L201,200Q201,200 201,200Q201,200 201,200Q206,241 215,281Q224,321 241,360ZM599,718Q638,735 678.5,745Q719,755 760,758Q760,758 760,758Q760,758 760,758L760,670Q760,670 760,670Q760,670 760,670L666,651Q666,651 666,651Q666,651 666,651L599,718ZM241,360Q241,360 241,360Q241,360 241,360Q241,360 241,360Q241,360 241,360L241,360Q241,360 241,360Q241,360 241,360L241,360Q241,360 241,360Q241,360 241,360ZM599,718L599,718Q599,718 599,718Q599,718 599,718L599,718Q599,718 599,718Q599,718 599,718L599,718Q599,718 599,718Q599,718 599,718Q599,718 599,718Q599,718 599,718Z"/>
|
||||||
|
</vector>
|
||||||
9
app/src/main/res/drawable/ic_notification_call_end.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M136,656L44,566Q32,554 32,538Q32,522 44,510Q132,415 247,367.5Q362,320 480,320Q598,320 712.5,367.5Q827,415 916,510Q928,522 928,538Q928,554 916,566L824,656Q813,667 798.5,668Q784,669 772,660L656,572Q648,566 644,558Q640,550 640,540L640,426Q602,414 562,407Q522,400 480,400Q438,400 398,407Q358,414 320,426L320,540Q320,550 316,558Q312,566 304,572L188,660Q176,669 161.5,668Q147,667 136,656ZM240,458Q211,473 184,492.5Q157,512 128,536Q128,536 128,536Q128,536 128,536L168,576Q168,576 168,576Q168,576 168,576L240,520Q240,520 240,520Q240,520 240,520L240,458ZM720,460L720,520Q720,520 720,520Q720,520 720,520L792,576Q792,576 792,576Q792,576 792,576L832,538Q832,538 832,538Q832,538 832,538Q803,512 776,493Q749,474 720,460ZM240,458L240,458Q240,458 240,458Q240,458 240,458L240,458Q240,458 240,458Q240,458 240,458L240,458Q240,458 240,458Q240,458 240,458Q240,458 240,458Q240,458 240,458ZM720,460Q720,460 720,460Q720,460 720,460Q720,460 720,460Q720,460 720,460L720,460Q720,460 720,460Q720,460 720,460L720,460Q720,460 720,460Q720,460 720,460Z"/>
|
||||||
|
</vector>
|
||||||
10
app/src/main/res/drawable/ic_notification_call_missed.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:autoMirrored="true"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:width="24dp">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M480,697L200,417L200,600L120,600L120,280L440,280L440,360L256,360L480,584L784,280L840,337L480,697Z"/>
|
||||||
|
</vector>
|
||||||
9
app/src/main/res/drawable/ic_notification_delete.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:width="24dp">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M280,840Q247,840 223.5,816.5Q200,793 200,760L200,240L160,240L160,160L360,160L360,120L600,120L600,160L800,160L800,240L760,240L760,760Q760,793 736.5,816.5Q713,840 680,840L280,840ZM680,240L280,240L280,760Q280,760 280,760Q280,760 280,760L680,760Q680,760 680,760Q680,760 680,760L680,240ZM360,680L440,680L440,320L360,320L360,680ZM520,680L600,680L600,320L520,320L520,680ZM280,240L280,240L280,760Q280,760 280,760Q280,760 280,760L280,760Q280,760 280,760Q280,760 280,760L280,240Z"/>
|
||||||
|
</vector>
|
||||||
10
app/src/main/res/drawable/ic_notification_message.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:autoMirrored="true"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:width="24dp">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M240,560L560,560L560,480L240,480L240,560ZM240,440L720,440L720,360L240,360L240,440ZM240,320L720,320L720,240L240,240L240,320ZM80,880L80,160Q80,127 103.5,103.5Q127,80 160,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,640Q880,673 856.5,696.5Q833,720 800,720L240,720L80,880ZM206,640L800,640Q800,640 800,640Q800,640 800,640L800,160Q800,160 800,160Q800,160 800,160L160,160Q160,160 160,160Q160,160 160,160L160,685L206,640ZM160,640L160,640L160,160Q160,160 160,160Q160,160 160,160L160,160Q160,160 160,160Q160,160 160,160L160,640Q160,640 160,640Q160,640 160,640Z"/>
|
||||||
|
</vector>
|
||||||
9
app/src/main/res/drawable/ic_notification_reply.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:autoMirrored="true"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:width="24dp">
|
||||||
|
<path android:fillColor="#FF000000"
|
||||||
|
android:pathData="M760,760L760,600Q760,550 725,515Q690,480 640,480L273,480L417,624L360,680L120,440L360,200L417,256L273,400L640,400Q723,400 781.5,458.5Q840,517 840,600L840,760L760,760Z"/>
|
||||||
|
</vector>
|
||||||
9
app/src/main/res/drawable/ic_notification_save.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:width="24dp">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M840,280L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L680,120L840,280ZM760,314L646,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L760,760Q760,760 760,760Q760,760 760,760L760,314ZM480,720Q530,720 565,685Q600,650 600,600Q600,550 565,515Q530,480 480,480Q430,480 395,515Q360,550 360,600Q360,650 395,685Q430,720 480,720ZM240,400L600,400L600,240L240,240L240,400ZM200,314L200,760Q200,760 200,760Q200,760 200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200L200,200L200,314Z"/>
|
||||||
|
</vector>
|
||||||
@ -271,6 +271,7 @@
|
|||||||
<string name="reply">Vastaa</string>
|
<string name="reply">Vastaa</string>
|
||||||
<string name="save">Talleta</string>
|
<string name="save">Talleta</string>
|
||||||
<string name="incoming_call_from">Puhelu soittajalta</string>
|
<string name="incoming_call_from">Puhelu soittajalta</string>
|
||||||
|
<string name="is_calling">soittaa</string>
|
||||||
<string name="missed_call_from">Vastaamaton puhelu soittajalta</string>
|
<string name="missed_call_from">Vastaamaton puhelu soittajalta</string>
|
||||||
<string name="missed_calls">Vastaamattomia puheluita</string>
|
<string name="missed_calls">Vastaamattomia puheluita</string>
|
||||||
<string name="missed_calls_count">%1$d vastaamatonta puhelua</string>
|
<string name="missed_calls_count">%1$d vastaamatonta puhelua</string>
|
||||||
|
|||||||
@ -256,6 +256,7 @@
|
|||||||
<string name="reply">Reply</string>
|
<string name="reply">Reply</string>
|
||||||
<string name="save">Save</string>
|
<string name="save">Save</string>
|
||||||
<string name="incoming_call_from">Incoming call from</string>
|
<string name="incoming_call_from">Incoming call from</string>
|
||||||
|
<string name="is_calling">is calling</string>
|
||||||
<string name="missed_call_from">Missed call from</string>
|
<string name="missed_call_from">Missed call from</string>
|
||||||
<string name="missed_calls">Missed calls</string>
|
<string name="missed_calls">Missed calls</string>
|
||||||
<string name="missed_calls_count">%1$d missed calls</string>
|
<string name="missed_calls_count">%1$d missed calls</string>
|
||||||
|
|||||||
@ -22,6 +22,7 @@ runtimeLivedata = "1.9.4"
|
|||||||
composeMaterial3 = "1.4.0"
|
composeMaterial3 = "1.4.0"
|
||||||
navigationRuntimeAndroid = "2.9.6"
|
navigationRuntimeAndroid = "2.9.6"
|
||||||
composeMaterialIcons = "1.7.8"
|
composeMaterialIcons = "1.7.8"
|
||||||
|
media3CommonKtx = "1.8.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
|
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
|
||||||
|
|||||||