From 4285891d8d472b93715493f2039d07674744ef4c Mon Sep 17 00:00:00 2001 From: Andrew nuark G Date: Wed, 8 Apr 2026 17:40:37 +0700 Subject: [PATCH 1/2] style: neovim broke tabs a little --- .../kotlin/xyz/nuark/liblinphone_flutter/LinphoneBridge.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/LinphoneBridge.kt b/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/LinphoneBridge.kt index 24046d8..85b40f7 100644 --- a/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/LinphoneBridge.kt +++ b/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/LinphoneBridge.kt @@ -201,9 +201,9 @@ class LinphoneBridge( core = factory.createCore(null, null, activity.baseContext) core.addListener(coreListener) - core.isIpv6Enabled = false - core.config.setInt("net", "ipv6", 0) - core.config.sync() + core.isIpv6Enabled = false + core.config.setInt("net", "ipv6", 0) + core.config.sync() // Enable video core.isVideoCaptureEnabled = true From 0940fcb908a8b39c2c6c128ea9bbdd28f168568b Mon Sep 17 00:00:00 2001 From: Andrew nuark G Date: Wed, 8 Apr 2026 17:47:37 +0700 Subject: [PATCH 2/2] feat: experimental fix for dying calls on android sdk34+ --- android/src/main/AndroidManifest.xml | 13 +++ .../LiblinphoneFlutterPlugin.kt | 25 ++++++ .../LinphoneVoipService.kt | 84 +++++++++++++++++++ 3 files changed, 122 insertions(+) create mode 100644 android/src/main/kotlin/xyz/nuark/liblinphone_flutter/LinphoneVoipService.kt 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