Started notifications manager for calls

This commit is contained in:
Sylvain Berfini 2023-08-24 11:58:34 +02:00
parent 5e150ee16a
commit 0c8bd49908
10 changed files with 882 additions and 11 deletions

View file

@ -5,6 +5,24 @@
<!-- To be able to display contacts list & match calling/called numbers -->
<uses-permission android:name="android.permission.READ_CONTACTS" />
<!-- Starting Android 13 we need to ask notification permission -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- Needed for full screen intent in incoming call notifications -->
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<!-- To vibrate when pressing DTMF keys on numpad & incoming calls -->
<uses-permission android:name="android.permission.VIBRATE" />
<!-- Needed for foreground service
(https://developer.android.com/guide/components/foreground-services) -->
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!-- Needed for Android 14
https://developer.android.com/about/versions/14/behavior-changes-14#fgs-types -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
<application
android:name=".LinphoneApplication"
android:allowBackup="true"
@ -45,6 +63,20 @@
<!-- Services -->
<service
android:name=".core.CoreForegroundService"
android:exported="false"
android:foregroundServiceType="phoneCall|camera|microphone|dataSync"
android:stopWithTask="false"
android:label="@string/app_name" />
<service
android:name="org.linphone.core.tools.service.PushService"
android:exported="false"
android:foregroundServiceType="dataSync"
android:stopWithTask="false"
android:label="@string/app_name" />
<service android:name="org.linphone.core.tools.firebase.FirebaseMessaging"
android:enabled="true"
android:exported="false">
@ -62,6 +94,11 @@
</intent-filter>
</receiver>
<receiver
android:name=".notifications.NotificationBroadcastReceiver"
android:enabled="true"
android:exported="false" />
<!-- Providers -->
<provider

View file

@ -19,10 +19,17 @@
*/
package org.linphone.contacts
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.core.app.Person
import androidx.core.graphics.drawable.IconCompat
import androidx.loader.app.LoaderManager
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.Address
import org.linphone.core.Core
import org.linphone.core.CoreListenerStub
import org.linphone.core.Friend
@ -30,12 +37,15 @@ import org.linphone.core.FriendList
import org.linphone.core.FriendListListenerStub
import org.linphone.core.tools.Log
import org.linphone.ui.main.MainActivity
import org.linphone.utils.ImageUtils
class ContactsManager {
class ContactsManager @UiThread constructor(context: Context) {
companion object {
const val TAG = "[Contacts Manager]"
}
val contactAvatar: IconCompat
private val listeners = arrayListOf<ContactsListener>()
private val friendListListener: FriendListListenerStub = object : FriendListListenerStub() {
@ -60,6 +70,13 @@ class ContactsManager {
}
}
init {
contactAvatar = IconCompat.createWithResource(
context,
R.drawable.contact
)
}
@UiThread
fun loadContacts(activity: MainActivity) {
val manager = LoaderManager.getInstance(activity)
@ -99,6 +116,14 @@ class ContactsManager {
return coreContext.core.defaultFriendList?.findFriendByRefKey(id)
}
@WorkerThread
fun findContactByAddress(address: Address): Friend? {
val friend = coreContext.core.findFriend(address)
if (friend != null) return friend
return null
}
@WorkerThread
fun onCoreStarted() {
val core = coreContext.core
@ -118,6 +143,33 @@ class ContactsManager {
}
}
@WorkerThread
fun Friend.getPerson(): Person {
val personBuilder = Person.Builder().setName(name)
val bm: Bitmap? = if (!photo.isNullOrEmpty()) {
ImageUtils.getRoundBitmapFromUri(
coreContext.context,
Uri.parse(photo ?: "")
)
} else {
null
}
personBuilder.setIcon(
if (bm == null) {
coreContext.contactsManager.contactAvatar
} else {
IconCompat.createWithAdaptiveBitmap(bm)
}
)
personBuilder.setKey(refKey)
personBuilder.setUri(nativeUri)
personBuilder.setImportant(starred)
return personBuilder.build()
}
interface ContactsListener {
fun onContactsLoaded()
}

View file

@ -35,6 +35,7 @@ import org.linphone.BuildConfig
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.contacts.ContactsManager
import org.linphone.core.tools.Log
import org.linphone.notifications.NotificationsManager
import org.linphone.ui.voip.VoipActivity
import org.linphone.utils.ActivityMonitor
import org.linphone.utils.LinphoneUtils
@ -48,7 +49,9 @@ class CoreContext @UiThread constructor(val context: Context) : HandlerThread("C
val emojiCompat: EmojiCompat
val contactsManager = ContactsManager()
val contactsManager = ContactsManager(context)
val notificationsManager = NotificationsManager(context)
private val activityMonitor = ActivityMonitor()
@ -71,14 +74,13 @@ class CoreContext @UiThread constructor(val context: Context) : HandlerThread("C
message: String
) {
Log.i("$TAG Call state changed [$state]")
if (state == Call.State.OutgoingProgress) {
postOnMainThread {
showCallActivity()
when (state) {
Call.State.OutgoingProgress, Call.State.Connected -> {
postOnMainThread {
showCallActivity()
}
}
} else if (state == Call.State.IncomingReceived) {
// TODO FIXME : remove when full screen intent notification
postOnMainThread {
showCallActivity()
else -> {
}
}
}
@ -121,6 +123,7 @@ class CoreContext @UiThread constructor(val context: Context) : HandlerThread("C
core.start()
contactsManager.onCoreStarted()
notificationsManager.onCoreStarted()
Looper.loop()
}
@ -128,7 +131,9 @@ class CoreContext @UiThread constructor(val context: Context) : HandlerThread("C
@WorkerThread
override fun destroy() {
core.stop()
contactsManager.onCoreStopped()
notificationsManager.onCoreStopped()
postOnMainThread {
(context as Application).unregisterActivityLifecycleCallbacks(activityMonitor)
@ -260,6 +265,53 @@ class CoreContext @UiThread constructor(val context: Context) : HandlerThread("C
return core.videoDevicesList.size > 2 // Count StaticImage camera
}
@WorkerThread
fun answerCall(call: Call) {
Log.i("$TAG Answering call $call")
val params = core.createCallParams(call)
if (params == null) {
Log.w("$TAG Answering call without params!")
call.accept()
return
}
// params.recordFile = LinphoneUtils.getRecordingFilePathForAddress(call.remoteAddress)
/*if (LinphoneUtils.checkIfNetworkHasLowBandwidth(context)) {
Log.w("$TAG Enabling low bandwidth mode!")
params.isLowBandwidthEnabled = true
}*/
if (call.callLog.wasConference()) {
// Prevent incoming group call to start in audio only layout
// Do the same as the conference waiting room
params.isVideoEnabled = true
params.videoDirection = if (core.videoActivationPolicy.automaticallyInitiate) MediaDirection.SendRecv else MediaDirection.RecvOnly
Log.i(
"$TAG Enabling video on call params to prevent audio-only layout when answering"
)
}
call.acceptWithParams(params)
}
@WorkerThread
fun declineCall(call: Call) {
val reason = if (core.callsNb > 1) {
Reason.Busy
} else {
Reason.Declined
}
Log.i("$TAG Declining call [$call] with reason [$reason]")
call.decline(reason)
}
@WorkerThread
fun terminateCall(call: Call) {
Log.i("$TAG Terminating call $call")
call.terminate()
}
@UiThread
private fun showCallActivity() {
Log.i("$TAG Starting VoIP activity")

View file

@ -0,0 +1,69 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.core
import android.content.Intent
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.tools.Log
import org.linphone.core.tools.service.CoreService
class CoreForegroundService : CoreService() {
companion object {
const val TAG = "[Core Foreground Service]"
}
override fun onCreate() {
super.onCreate()
Log.i("$TAG Created")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i("$TAG onStartCommand")
coreContext.notificationsManager.onServiceStarted(this)
return super.onStartCommand(intent, flags, startId)
}
override fun createServiceNotificationChannel() {
// Done elsewhere
}
override fun showForegroundServiceNotification() {
// Done elsewhere
}
override fun hideForegroundServiceNotification() {
// Done elsewhere
}
override fun onTaskRemoved(rootIntent: Intent?) {
Log.i("$TAG Task removed, doing nothing")
super.onTaskRemoved(rootIntent)
}
override fun onDestroy() {
Log.i("$TAG onDestroy")
coreContext.notificationsManager.onServiceDestroyed()
super.onDestroy()
}
}

View file

@ -0,0 +1,71 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.notifications
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.Call
import org.linphone.core.tools.Log
class NotificationBroadcastReceiver : BroadcastReceiver() {
companion object {
const val TAG = "[NotificationBroadcastReceiver]"
}
override fun onReceive(context: Context, intent: Intent) {
val notificationId = intent.getIntExtra(NotificationsManager.INTENT_NOTIF_ID, 0)
Log.i(
"[Notification Broadcast Receiver] Got notification broadcast for ID [$notificationId]"
)
if (intent.action == NotificationsManager.INTENT_ANSWER_CALL_NOTIF_ACTION || intent.action == NotificationsManager.INTENT_HANGUP_CALL_NOTIF_ACTION) {
handleCallIntent(intent)
}
}
private fun handleCallIntent(intent: Intent) {
val callId = intent.getStringExtra(NotificationsManager.INTENT_CALL_ID)
if (callId == null) {
Log.e("[Notification Broadcast Receiver] Remote SIP address is null for notification")
return
}
coreContext.postOnCoreThread { core ->
val call = core.getCallByCallid(callId)
if (call == null) {
Log.e("[Notification Broadcast Receiver] Couldn't find call from ID [$callId]")
} else {
if (intent.action == NotificationsManager.INTENT_ANSWER_CALL_NOTIF_ACTION) {
coreContext.answerCall(call)
} else {
if (call.state == Call.State.IncomingReceived ||
call.state == Call.State.IncomingEarlyMedia
) {
coreContext.declineCall(call)
} else {
coreContext.terminateCall(call)
}
}
}
}
}
}

View file

@ -0,0 +1,468 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.notifications
import android.Manifest
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service.STOP_FOREGROUND_REMOVE
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.graphics.Bitmap
import android.net.Uri
import androidx.annotation.AnyThread
import androidx.annotation.MainThread
import androidx.annotation.RequiresPermission
import androidx.annotation.WorkerThread
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.Person
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.contacts.getPerson
import org.linphone.core.Call
import org.linphone.core.Core
import org.linphone.core.CoreForegroundService
import org.linphone.core.CoreListenerStub
import org.linphone.core.Friend
import org.linphone.core.tools.Log
import org.linphone.ui.voip.VoipActivity
import org.linphone.utils.ImageUtils
import org.linphone.utils.LinphoneUtils
class NotificationsManager @MainThread constructor(private val context: Context) {
companion object {
const val TAG = "[Notifications Manager]"
const val INTENT_HANGUP_CALL_NOTIF_ACTION = "org.linphone.HANGUP_CALL_ACTION"
const val INTENT_ANSWER_CALL_NOTIF_ACTION = "org.linphone.ANSWER_CALL_ACTION"
const val INTENT_CALL_ID = "CALL_ID"
const val INTENT_NOTIF_ID = "NOTIFICATION_ID"
}
private val notificationManager: NotificationManagerCompat by lazy {
NotificationManagerCompat.from(context)
}
private val coreListener = object : CoreListenerStub() {
@WorkerThread
override fun onFirstCallStarted(core: Core) {
startCoreForegroundService()
}
@WorkerThread
override fun onCallStateChanged(
core: Core,
call: Call,
state: Call.State?,
message: String
) {
when (state) {
Call.State.IncomingReceived, Call.State.IncomingEarlyMedia -> {
showCallNotification(call, true)
}
Call.State.Connected -> {
showCallNotification(call, false)
}
Call.State.End, Call.State.Error -> {
dismissCallNotification(call)
}
else -> {
}
}
}
@WorkerThread
override fun onLastCallEnded(core: Core) {
Log.i("$TAG Last call ended, stopping foreground service")
stopCallForeground()
}
}
private var coreService: CoreForegroundService? = null
private val callNotificationsMap: HashMap<String, Notifiable> = HashMap()
init {
createServiceChannel()
createIncomingCallNotificationChannel()
for (notification in notificationManager.activeNotifications) {
if (notification.tag.isNullOrEmpty()) {
Log.w(
"$TAG Found existing (call?) notification [${notification.id}] without tag, cancelling it"
)
notificationManager.cancel(notification.id)
}
}
}
fun onServiceStarted(service: CoreForegroundService) {
Log.i("$TAG Service has been started")
coreService = service
startCallForeground()
}
fun onServiceDestroyed() {
Log.i("$TAG Service has been destroyed")
coreService = null
}
@WorkerThread
fun onCoreStarted() {
coreContext.core.addListener(coreListener)
}
@WorkerThread
fun onCoreStopped() {
Log.i("$TAG Getting destroyed, clearing foreground Service & call notifications")
coreContext.core.removeListener(coreListener)
}
@WorkerThread
private fun showCallNotification(call: Call, isIncoming: Boolean) {
val notifiable = getNotifiableForCall(call)
val callNotificationIntent = Intent(context, VoipActivity::class.java)
callNotificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val pendingIntent = PendingIntent.getActivity(
context,
0,
callNotificationIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val notification = createCallNotification(
context,
call,
notifiable,
pendingIntent,
isIncoming
)
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
) {
notify(notifiable.notificationId, notification)
}
}
@WorkerThread
private fun startCallForeground() {
val channelId = context.getString(R.string.notification_channel_service_id)
val channel = notificationManager.getNotificationChannel(channelId)
val importance = channel?.importance ?: NotificationManagerCompat.IMPORTANCE_NONE
if (importance == NotificationManagerCompat.IMPORTANCE_NONE) {
Log.e("$TAG Service channel has been disabled, can't start foreground service!")
return
}
val notifiable = getNotifiableForCall(
coreContext.core.currentCall ?: coreContext.core.calls.first()
)
val notif = notificationManager.activeNotifications.find {
it.id == notifiable.notificationId
}
notif ?: return
val service = coreService
if (service != null) {
Log.i("$TAG Service found, starting it as foreground using notification")
try {
service.startForeground(
notifiable.notificationId,
notif.notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
or ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
)
} catch (e: Exception) {
Log.e("$TAG Can't start service as foreground! $e")
}
} else {
Log.w("$TAG Core Foreground Service hasn't started yet...")
}
}
@WorkerThread
private fun stopCallForeground() {
val service = coreService
if (service != null) {
Log.i("$TAG Stopping foreground service")
service.stopForeground(STOP_FOREGROUND_REMOVE)
service.stopSelf()
} else {
Log.w("$TAG Can't stop foreground service & notif, no service was found")
}
}
@WorkerThread
private fun startCoreForegroundService() {
val service = coreService
if (service == null) {
Log.i("$TAG Starting Core Foreground Service")
val intent = Intent()
intent.setClass(coreContext.context, CoreForegroundService::class.java)
try {
context.startForegroundService(intent)
} catch (e: Exception) {
Log.e("$TAG Failed to start Service: $e")
}
}
}
@RequiresPermission(Manifest.permission.POST_NOTIFICATIONS)
@WorkerThread
private fun notify(id: Int, notification: Notification, tag: String? = null) {
Log.i("$TAG Notifying [$id] with tag [$tag]")
try {
notificationManager.notify(tag, id, notification)
} catch (iae: IllegalArgumentException) {
if (coreService == null && tag == null) {
// We can't notify using CallStyle if there isn't a foreground service running
Log.w(
"$TAG Foreground service hasn't started yet, can't display a CallStyle notification until then: $iae"
)
} else {
Log.e("$TAG Illegal Argument Exception occurred: $iae")
}
} catch (e: Exception) {
Log.e("$TAG Exception occurred: $e")
}
}
@WorkerThread
fun cancelNotification(id: Int, tag: String? = null) {
Log.i("$TAG Canceling [$id] with tag [$tag]")
notificationManager.cancel(tag, id)
}
@WorkerThread
private fun getNotificationIdForCall(call: Call): Int {
return call.callLog.startDate.toInt()
}
@WorkerThread
private fun getNotifiableForCall(call: Call): Notifiable {
val address = call.remoteAddress.asStringUriOnly()
var notifiable: Notifiable? = callNotificationsMap[address]
if (notifiable == null) {
notifiable = Notifiable(getNotificationIdForCall(call))
notifiable.callId = call.callLog.callId
callNotificationsMap[address] = notifiable
}
return notifiable
}
@WorkerThread
private fun createCallNotification(
context: Context,
call: Call,
notifiable: Notifiable,
pendingIntent: PendingIntent?,
isIncoming: Boolean
): Notification {
val declineIntent = getCallDeclinePendingIntent(notifiable)
val answerIntent = getCallAnswerPendingIntent(notifiable)
val contact =
coreContext.contactsManager.findContactByAddress(call.remoteAddress)
val contactPicture = contact?.photo
val roundPicture = if (!contactPicture.isNullOrEmpty()) {
ImageUtils.getRoundBitmapFromUri(context, Uri.parse(contactPicture))
} else {
null
}
val displayName = contact?.name ?: LinphoneUtils.getDisplayName(call.remoteAddress)
val person = getPerson(contact, displayName, roundPicture)
val caller = Person.Builder()
.setName(person.name)
.setIcon(person.icon)
.setUri(person.uri)
.setKey(person.key)
.setImportant(person.isImportant)
.build()
val isVideo = if (isIncoming) {
call.remoteParams?.isVideoEnabled ?: false
} else {
call.currentParams.isVideoEnabled
}
val smallIcon = if (isVideo) {
R.drawable.camera_enabled
} else {
R.drawable.calls
}
val style = if (isIncoming) {
NotificationCompat.CallStyle.forIncomingCall(
caller,
declineIntent,
answerIntent
)
} else {
NotificationCompat.CallStyle.forOngoingCall(
caller,
declineIntent
)
}
val channel = if (isIncoming) {
context.getString(R.string.notification_channel_incoming_call_id)
} else {
context.getString(R.string.notification_channel_service_id)
}
val builder = NotificationCompat.Builder(
context,
channel
).apply {
try {
style.setIsVideo(isVideo)
style.setAnswerButtonColorHint(
context.resources.getColor(R.color.green_online, context.theme)
)
style.setDeclineButtonColorHint(
context.resources.getColor(R.color.red_danger, context.theme)
)
setStyle(style)
} catch (iae: IllegalArgumentException) {
Log.e(
"[Api31 Compatibility] Can't use notification call style: $iae, using API 26 notification instead"
)
}
setSmallIcon(smallIcon)
setCategory(NotificationCompat.CATEGORY_CALL)
setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
setWhen(System.currentTimeMillis())
setAutoCancel(false)
setShowWhen(true)
setOngoing(true)
color = ContextCompat.getColor(context, R.color.primary_color)
setFullScreenIntent(pendingIntent, true)
}
return builder.build()
}
private fun dismissCallNotification(call: Call) {
val address = call.remoteAddress.asStringUriOnly()
val notifiable: Notifiable? = callNotificationsMap[address]
if (notifiable != null) {
cancelNotification(notifiable.notificationId)
callNotificationsMap.remove(address)
} else {
Log.w("$TAG No notification found for call with remote address [$address]")
}
}
@AnyThread
fun getCallDeclinePendingIntent(notifiable: Notifiable): PendingIntent {
val hangupIntent = Intent(context, NotificationBroadcastReceiver::class.java)
hangupIntent.action = INTENT_HANGUP_CALL_NOTIF_ACTION
hangupIntent.putExtra(INTENT_NOTIF_ID, notifiable.notificationId)
hangupIntent.putExtra(INTENT_CALL_ID, notifiable.callId)
return PendingIntent.getBroadcast(
context,
notifiable.notificationId,
hangupIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
}
@AnyThread
fun getCallAnswerPendingIntent(notifiable: Notifiable): PendingIntent {
val answerIntent = Intent(context, NotificationBroadcastReceiver::class.java)
answerIntent.action = INTENT_ANSWER_CALL_NOTIF_ACTION
answerIntent.putExtra(INTENT_NOTIF_ID, notifiable.notificationId)
answerIntent.putExtra(INTENT_CALL_ID, notifiable.callId)
return PendingIntent.getBroadcast(
context,
notifiable.notificationId,
answerIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
}
@WorkerThread
private fun getPerson(friend: Friend?, displayName: String, picture: Bitmap?): Person {
return friend?.getPerson()
?: Person.Builder()
.setName(displayName)
.setIcon(
if (picture != null) {
IconCompat.createWithAdaptiveBitmap(picture)
} else {
coreContext.contactsManager.contactAvatar
}
)
.setKey(displayName)
.build()
}
@MainThread
private fun createIncomingCallNotificationChannel() {
val id = context.getString(R.string.notification_channel_incoming_call_id)
val name = context.getString(R.string.notification_channel_incoming_call_name)
val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_HIGH)
channel.description = name
channel.lightColor = context.getColor(R.color.primary_color)
channel.lockscreenVisibility
channel.enableVibration(true)
channel.enableLights(true)
channel.setShowBadge(false)
notificationManager.createNotificationChannel(channel)
}
@MainThread
private fun createServiceChannel() {
val id = context.getString(R.string.notification_channel_service_id)
val name = context.getString(R.string.notification_channel_service_name)
val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_LOW)
channel.description = name
channel.enableVibration(false)
channel.enableLights(false)
channel.setShowBadge(false)
notificationManager.createNotificationChannel(channel)
}
class Notifiable(val notificationId: Int) {
var myself: String? = null
var callId: String? = null
}
}

View file

@ -54,6 +54,8 @@ class MainActivity : AppCompatActivity() {
private const val CONTACTS_PERMISSION_REQUEST = 0
private const val CAMERA_PERMISSION_REQUEST = 1
private const val RECORD_AUDIO_PERMISSION_REQUEST = 2
private const val POST_NOTIFICATIONS_PERMISSION_REQUEST = 3
private const val MANAGE_OWN_CALLS_PERMISSION_REQUEST = 4
}
private lateinit var binding: MainActivityBinding
@ -140,6 +142,18 @@ class MainActivity : AppCompatActivity() {
RECORD_AUDIO_PERMISSION_REQUEST
)
}
if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
POST_NOTIFICATIONS_PERMISSION_REQUEST
)
}
if (checkSelfPermission(Manifest.permission.MANAGE_OWN_CALLS) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
MANAGE_OWN_CALLS_PERMISSION_REQUEST
)
}
}
override fun onRequestPermissionsResult(

View file

@ -0,0 +1,94 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.utils
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.ImageDecoder
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.Rect
import android.net.Uri
import androidx.annotation.AnyThread
import java.io.FileNotFoundException
import org.linphone.core.tools.Log
class ImageUtils {
companion object {
const val TAG = "[Image Utils]"
@AnyThread
fun getRoundBitmapFromUri(
context: Context,
fromPictureUri: Uri?
): Bitmap? {
var bm: Bitmap? = null
if (fromPictureUri != null) {
bm = try {
// We make a copy to ensure Bitmap will be Software and not Hardware, required for shortcuts
ImageDecoder.decodeBitmap(
ImageDecoder.createSource(context.contentResolver, fromPictureUri)
).copy(
Bitmap.Config.ARGB_8888,
true
)
} catch (fnfe: FileNotFoundException) {
return null
} catch (e: Exception) {
Log.e("$TAG Failed to get bitmap from URI [$fromPictureUri]: $e")
return null
}
}
if (bm != null) {
val roundBm = getRoundBitmap(bm)
if (roundBm != null) {
bm.recycle()
return roundBm
}
}
return bm
}
@AnyThread
private fun getRoundBitmap(bitmap: Bitmap): Bitmap? {
val output =
Bitmap.createBitmap(bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(output)
val color = -0xbdbdbe
val paint = Paint()
val rect =
Rect(0, 0, bitmap.width, bitmap.height)
paint.isAntiAlias = true
canvas.drawARGB(0, 0, 0, 0)
paint.color = color
canvas.drawCircle(
bitmap.width / 2.toFloat(),
bitmap.height / 2.toFloat(),
bitmap.width / 2.toFloat(),
paint
)
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
canvas.drawBitmap(bitmap, rect, rect, paint)
return output
}
}
}

View file

@ -1,5 +1,19 @@
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE resources [
<!ENTITY appName "Linphone">
]>
<resources>
<string name="app_name">Linphone</string>
<string name="vertical_separator">|</string>
<string name="notification_channel_incoming_call_id" translatable="false">linphone_notification_call_id</string>
<string name="notification_channel_incoming_call_name">&appName; incoming calls notifications</string>
<string name="notification_channel_service_id" translatable="false">linphone_notification_service_id</string>
<string name="notification_channel_service_name">&appName; service notification</string>
<string name="incoming_call_notification_hangup_action_label">Hangup</string>
<string name="incoming_call_notification_answer_action_label">Answer</string>
</resources>

View file

@ -9,8 +9,8 @@ buildscript {
}
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '8.1.0' apply false
id 'com.android.library' version '8.1.0' apply false
id 'com.android.application' version '8.1.1' apply false
id 'com.android.library' version '8.1.1' apply false
id 'org.jetbrains.kotlin.android' version '1.9.0-RC' apply false
id 'com.google.gms.google-services' version '4.3.15' apply false
}