diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index 29d67b8..00eacfa 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -8,6 +8,10 @@
+
+
+
+
@@ -15,4 +19,13 @@
+
+
+
+
diff --git a/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/LiblinphoneFlutterPlugin.kt b/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/LiblinphoneFlutterPlugin.kt
index 73560e1..b62bfc7 100644
--- a/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/LiblinphoneFlutterPlugin.kt
+++ b/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/LiblinphoneFlutterPlugin.kt
@@ -138,6 +138,7 @@ class LiblinphoneFlutterPlugin : FlutterPlugin, ActivityAware, MethodCallHandler
val callTo = call.argument("callTo")!!
val isVideoEnabled = call.argument("isVideoEnabled")!!
linphoneBridge.makeCall(callTo, isVideoEnabled)
+ startCallService()
result.success(true)
} catch (e: Exception) {
Log.e(TAG, "makeCall: ${e.message}")
@@ -147,11 +148,13 @@ class LiblinphoneFlutterPlugin : FlutterPlugin, ActivityAware, MethodCallHandler
"answerCall" -> {
val res = linphoneBridge.answerCall()
+ if (res) startCallService()
result.success(res)
}
"hangupCall" -> {
val res = linphoneBridge.hangupCall()
+ if (res) startCallService()
result.success(res)
}
@@ -240,5 +243,27 @@ class LiblinphoneFlutterPlugin : FlutterPlugin, ActivityAware, MethodCallHandler
companion object {
const val TAG = "LiblinphoneFlutterPlugin"
+
+ private fun startCallService() {
+ try {
+ val intent = Intent(activity.applicationContext, LinphoneVoipService::class.java)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ activity.startForegroundService(intent)
+ } else {
+ activity.startService(intent)
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to start call service: ${e.message}")
+ }
+ }
+
+ private fun stopCallService() {
+ try {
+ val intent = Intent(activity.applicationContext, LinphoneVoipService::class.java)
+ activity.stopService(intent)
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to stop call service: ${e.message}")
+ }
+ }
}
}
diff --git a/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/LinphoneVoipService.kt b/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/LinphoneVoipService.kt
new file mode 100644
index 0000000..64530e8
--- /dev/null
+++ b/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/LinphoneVoipService.kt
@@ -0,0 +1,84 @@
+package xyz.nuark.liblinphone_flutter
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.IBinder
+import android.os.PowerManager
+import android.util.Log
+import androidx.core.app.NotificationCompat
+import android.content.pm.ServiceInfo
+
+class LinphoneVoipService : Service() {
+ private var wakeLock: PowerManager.WakeLock? = null
+ private val channelId = "linphone_voip_call_channel"
+ private val TAG = "LinphoneVoipService"
+
+ override fun onCreate() {
+ super.onCreate()
+ createNotificationChannel()
+ acquireWakeLock()
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ val notification = createNotification()
+
+ // Android 14+ requires explicit service type in startForeground
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ startForeground(100, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL)
+ } else {
+ startForeground(100, notification)
+ }
+
+ Log.i(TAG, "Foreground service started. Mic will stay active.")
+ return START_NOT_STICKY
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ releaseWakeLock()
+ Log.i(TAG, "Foreground service stopped.")
+ }
+
+ override fun onBind(intent: Intent?): IBinder? = null
+
+ private fun createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ channelId,
+ "Active VoIP Call",
+ NotificationManager.IMPORTANCE_LOW
+ ).apply {
+ setShowBadge(false)
+ setSound(null, null)
+ }
+ val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ manager.createNotificationChannel(channel)
+ }
+ }
+
+ private fun createNotification(): Notification {
+ return NotificationCompat.Builder(this, channelId)
+ .setContentTitle("Voice Call")
+ .setContentText("Call in progress")
+ .setSmallIcon(android.R.drawable.ic_menu_call) // Replace with your app's call icon
+ .setOngoing(true)
+ .setCategory(NotificationCompat.CATEGORY_CALL)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .build()
+ }
+
+ private fun acquireWakeLock() {
+ val pm = getSystemService(Context.POWER_SERVICE) as PowerManager
+ wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Linphone::CallWakeLock")
+ wakeLock?.acquire(30 * 60 * 1000L) // 30 min fallback timeout
+ }
+
+ private fun releaseWakeLock() {
+ wakeLock?.takeIf { it.isHeld }?.release()
+ }
+}
\ No newline at end of file