mirror of
https://gitlab.linphone.org/BC/public/linphone-android.git
synced 2026-01-22 14:18:15 +00:00
Started notifications manager for calls
This commit is contained in:
parent
5e150ee16a
commit
0c8bd49908
10 changed files with 882 additions and 11 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
69
app/src/main/java/org/linphone/core/CoreForegroundService.kt
Normal file
69
app/src/main/java/org/linphone/core/CoreForegroundService.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
94
app/src/main/java/org/linphone/utils/ImageUtils.kt
Normal file
94
app/src/main/java/org/linphone/utils/ImageUtils.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue