feat: experimental fix for dying calls on android sdk34+

This commit is contained in:
Andrew 2026-04-08 17:47:37 +07:00
parent 4285891d8d
commit 0940fcb908
3 changed files with 122 additions and 0 deletions

View file

@ -8,6 +8,10 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Camera features --> <!-- Camera features -->
<uses-feature android:name="android.hardware.camera" android:required="false" /> <uses-feature android:name="android.hardware.camera" android:required="false" />
@ -15,4 +19,13 @@
<!-- Microphone feature --> <!-- Microphone feature -->
<uses-feature android:name="android.hardware.microphone" android:required="true" /> <uses-feature android:name="android.hardware.microphone" android:required="true" />
<application>
<service
android:name=".LinphoneVoipService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="phoneCall"
android:stopWithTask="false" />
</application>
</manifest> </manifest>

View file

@ -138,6 +138,7 @@ class LiblinphoneFlutterPlugin : FlutterPlugin, ActivityAware, MethodCallHandler
val callTo = call.argument<String>("callTo")!! val callTo = call.argument<String>("callTo")!!
val isVideoEnabled = call.argument<Boolean>("isVideoEnabled")!! val isVideoEnabled = call.argument<Boolean>("isVideoEnabled")!!
linphoneBridge.makeCall(callTo, isVideoEnabled) linphoneBridge.makeCall(callTo, isVideoEnabled)
startCallService()
result.success(true) result.success(true)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "makeCall: ${e.message}") Log.e(TAG, "makeCall: ${e.message}")
@ -147,11 +148,13 @@ class LiblinphoneFlutterPlugin : FlutterPlugin, ActivityAware, MethodCallHandler
"answerCall" -> { "answerCall" -> {
val res = linphoneBridge.answerCall() val res = linphoneBridge.answerCall()
if (res) startCallService()
result.success(res) result.success(res)
} }
"hangupCall" -> { "hangupCall" -> {
val res = linphoneBridge.hangupCall() val res = linphoneBridge.hangupCall()
if (res) startCallService()
result.success(res) result.success(res)
} }
@ -240,5 +243,27 @@ class LiblinphoneFlutterPlugin : FlutterPlugin, ActivityAware, MethodCallHandler
companion object { companion object {
const val TAG = "LiblinphoneFlutterPlugin" 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}")
}
}
} }
} }

View file

@ -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()
}
}