Work on notification improvements

This commit is contained in:
Juha Heinanen
2025-11-27 10:32:31 +02:00
parent 9099828c88
commit 97338f20d1
53 changed files with 253 additions and 39 deletions

View File

@ -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.res.Configuration
import android.database.ContentObserver
import android.graphics.ImageDecoder
import android.media.AudioAttributes
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.media.AudioManager.MODE_IN_COMMUNICATION
import android.media.AudioManager.MODE_NORMAL
import android.media.AudioManager.RINGER_MODE_SILENT
import android.media.MediaPlayer
import android.media.Ringtone
import android.media.RingtoneManager
@ -65,11 +66,13 @@ import androidx.core.app.NotificationCompat.MessagingStyle
import androidx.core.app.Person
import androidx.core.app.RemoteInput
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.core.net.toUri
import androidx.lifecycle.MutableLiveData
import androidx.media.AudioAttributesCompat
import androidx.media.AudioFocusRequestCompat
import androidx.media.AudioManagerCompat
import com.tutpro.baresip.Utils.toCircle
import java.io.File
import java.net.InetAddress
import java.nio.charset.Charset
@ -885,7 +888,7 @@ class BaresipService: Service() {
startActivity(newIntent)
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]
if (shouldStartRinging(channelId, callerNumber))
startRinging()
@ -899,15 +902,40 @@ class BaresipService: Service() {
val pi = PendingIntent.getActivity(applicationContext, CALL_REQ_CODE, intent, piFlags)
val nb = NotificationCompat.Builder(this, channelId)
val caller = Utils.friendlyUri(this, peerUri, ua.account)
val person = Person.Builder().setName(caller).build()
nb.setSmallIcon(R.drawable.ic_stat_call)
val callerContact = Contact.findContact(peerUri)
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))
.setContentIntent(pi)
.setCategory(Notification.CATEGORY_CALL)
.setAutoCancel(false)
.setOngoing(true)
.setContentTitle(getString(R.string.incoming_call_from))
.setContentText(caller)
.setContentText(getString(R.string.is_calling))
.setWhen(System.currentTimeMillis())
.setShowWhen(true)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
@ -985,7 +1013,7 @@ class BaresipService: Service() {
val pi = PendingIntent.getActivity(applicationContext, TRANSFER_REQ_CODE, intent, piFlags)
val nb = NotificationCompat.Builder(this, HIGH_CHANNEL_ID)
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))
.setContentIntent(pi)
.setDefaults(Notification.DEFAULT_SOUND)
@ -1004,8 +1032,10 @@ class BaresipService: Service() {
denyIntent.putExtra("callp", callp)
val denyPendingIntent = PendingIntent.getService(this,
DENY_REQ_CODE, denyIntent, piFlags)
nb.addAction(R.drawable.ic_stat_call, getString(R.string.accept), acceptPendingIntent)
nb.addAction(R.drawable.ic_stat_call_end, getString(R.string.deny), denyPendingIntent)
nb.addAction(R.drawable.ic_notification_call,
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())
return
}
@ -1082,7 +1112,7 @@ class BaresipService: Service() {
val pi = PendingIntent.getActivity(applicationContext, CALL_REQ_CODE,
intent, piFlags)
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))
.setContentIntent(pi)
.setCategory(Notification.CATEGORY_CALL)
@ -1167,11 +1197,34 @@ class BaresipService: Service() {
val pi = PendingIntent.getActivity(applicationContext, MESSAGE_REQ_CODE, intent, piFlags)
// message notification builder
val senderDisplayName = Utils.friendlyUri(this, peerUri, ua.account)
val senderPerson = Person.Builder()
.setName(senderDisplayName)
.setKey(peerUri)
.build()
val sender = Utils.friendlyUri(this, peerUri, ua.account)
val senderContact = Contact.findContact(peerUri)
val personBuilder = Person.Builder().setName(sender)
val contactColor = senderContact?.color() ?: "#B0B0B0"
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()
.setName(getString(R.string.you))
.setKey(ua.account.aor)
@ -1181,7 +1234,7 @@ class BaresipService: Service() {
.setGroupConversation(false)
.addMessage(text, timeStamp, senderPerson)
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))
.setContentIntent(pi)
.setSound(Settings.System.DEFAULT_NOTIFICATION_URI)
@ -1190,7 +1243,7 @@ class BaresipService: Service() {
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
.setPriority(NotificationCompat.PRIORITY_HIGH)
// messafe inline reply
// message inline reply
val remoteInput = RemoteInput.Builder(KEY_TEXT_REPLY)
.setLabel(getString(R.string.reply))
.build()
@ -1204,7 +1257,7 @@ class BaresipService: Service() {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
)
val inlineReplyAction = NotificationCompat.Action.Builder(
R.drawable.ic_stat_reply,
R.drawable.ic_notification_reply,
getString(R.string.reply),
directReplyPendingIntent
).addRemoteInput(remoteInput)
@ -1218,7 +1271,7 @@ class BaresipService: Service() {
saveIntent.putExtra("uap", uap).putExtra("time", timeStampString)
val savePendingIntent = PendingIntent.getService(this, SAVE_REQ_CODE, saveIntent, piFlags)
val saveAction = NotificationCompat.Action.Builder(
R.drawable.ic_stat_save,
R.drawable.ic_notification_save,
getString(R.string.save),
savePendingIntent
).build()
@ -1229,7 +1282,7 @@ class BaresipService: Service() {
deleteIntent.putExtra("uap", uap).putExtra("time", timeStampString)
val deletePendingIntent = PendingIntent.getService(this, DELETE_REQ_CODE, deleteIntent, piFlags)
val deleteAction = NotificationCompat.Action.Builder(
R.drawable.ic_stat_delete,
R.drawable.ic_notification_delete,
getString(R.string.delete),
deletePendingIntent
).build()
@ -1294,22 +1347,29 @@ class BaresipService: Service() {
}
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)
lowChannel.description = "Background status notifications"
lowChannel.enableVibration(false)
lowChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
nm.createNotificationChannel(lowChannel)
val highChannel = NotificationChannel(HIGH_CHANNEL_ID, "Sound and vibrate",
val ringAttributes = AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
.build()
val highChannel = NotificationChannel(HIGH_CHANNEL_ID, "Sound, vibrate, and peek",
NotificationManager.IMPORTANCE_HIGH)
highChannel.description = "Incoming calls and important alerts"
highChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
highChannel.enableVibration(true)
highChannel.setSound(Settings.System.DEFAULT_NOTIFICATION_URI, ringAttributes)
nm.createNotificationChannel(highChannel)
val mediumChannel = NotificationChannel(MEDIUM_CHANNEL_ID, "Sound",
NotificationManager.IMPORTANCE_HIGH)
val mediumChannel = NotificationChannel(MEDIUM_CHANNEL_ID, "Sound only",
NotificationManager.IMPORTANCE_DEFAULT)
mediumChannel.description = "Incoming messages"
mediumChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
mediumChannel.enableVibration(false)
mediumChannel.setSound(Settings.System.DEFAULT_NOTIFICATION_URI, ringAttributes)
nm.createNotificationChannel(mediumChannel)
}
@ -1322,7 +1382,7 @@ class BaresipService: Service() {
PendingIntent.FLAG_IMMUTABLE)
val notificationLayout = RemoteViews(packageName, R.layout.status_notification)
snb.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setSmallIcon(R.drawable.ic_stat)
.setSmallIcon(R.drawable.ic_notification_b)
.setContentIntent(pi)
.setOngoing(true)
.setStyle(NotificationCompat.DecoratedCustomViewStyle())
@ -1413,17 +1473,22 @@ class BaresipService: Service() {
}
private fun shouldVibrate(): Boolean {
return if (am.ringerMode != RINGER_MODE_SILENT)
if (am.ringerMode == AudioManager.RINGER_MODE_VIBRATE)
true
else
if (am.getStreamVolume(AudioManager.STREAM_RING) != 0)
@Suppress("DEPRECATION")
Settings.System.getInt(contentResolver, Settings.System.VIBRATE_WHEN_RINGING, 0) != 0
else
false
else
// 1. If the phone is in Silent mode, never vibrate
if (am.ringerMode == AudioManager.RINGER_MODE_SILENT) return false
// 2. If the phone is in Vibrate mode, always vibrate
if (am.ringerMode == AudioManager.RINGER_MODE_VIBRATE) return true
// 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")
Settings.System.getInt(contentResolver, Settings.System.VIBRATE_WHEN_RINGING, 0) != 0
} catch (_: Exception) {
// If the setting can't be read, default to FALSE to be non-intrusive.
false
}
}
private fun isStarredContact(callerNumber: String): Boolean {

View File

@ -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 {
val copy = when (this) {
is BaresipContact ->

View File

@ -8,6 +8,9 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.media.AudioAttributes
import android.media.AudioDeviceInfo
import android.media.AudioManager
@ -71,7 +74,7 @@ import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
import kotlin.text.replaceFirstChar
import androidx.core.graphics.toColorInt
object Utils {
@ -169,7 +172,7 @@ object Utils {
private fun e164Uri(uri: String, countryCode: String): String {
if (countryCode == "") return uri
val scheme = uri.substring(0, 4)
val scheme = uri.take(4)
val userPart = uriUserPart(uri)
return if (userPart.isDigitsOnly()) {
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> {
val directory = File(directoryPath)
if (!directory.exists()) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 408 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 595 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 422 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 629 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 362 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 531 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 513 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 342 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 492 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 248 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 331 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 533 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 680 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 592 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 961 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 599 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 528 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 785 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 780 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 495 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 587 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 924 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 547 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 592 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@ -271,6 +271,7 @@
<string name="reply">Vastaa</string>
<string name="save">Talleta</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_calls">Vastaamattomia puheluita</string>
<string name="missed_calls_count">%1$d vastaamatonta puhelua</string>

View File

@ -256,6 +256,7 @@
<string name="reply">Reply</string>
<string name="save">Save</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_calls">Missed calls</string>
<string name="missed_calls_count">%1$d missed calls</string>

View File

@ -22,6 +22,7 @@ runtimeLivedata = "1.9.4"
composeMaterial3 = "1.4.0"
navigationRuntimeAndroid = "2.9.6"
composeMaterialIcons = "1.7.8"
media3CommonKtx = "1.8.0"
[libraries]
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }