Fix crash when outgoing call picked up while in the background.

This commit is contained in:
Cody Henthorne
2024-08-13 10:51:12 -04:00
committed by GitHub
parent 6358589e19
commit 19f3219224
2 changed files with 98 additions and 36 deletions

View File

@@ -32,13 +32,14 @@ abstract class SafeForegroundService : Service() {
private val TAG = Log.tag(SafeForegroundService::class.java)
private const val ACTION_START = "start"
private const val ACTION_UPDATE = "update"
private const val ACTION_STOP = "stop"
private var states: MutableMap<Class<out SafeForegroundService>, State> = mutableMapOf()
private val stateLock = ReentrantLock()
/**
* Safety starts the target foreground service.
* Safely starts the target foreground service.
* @return False if we tried to start the service but failed, otherwise true.
*/
@CheckReturnValue
@@ -124,6 +125,44 @@ abstract class SafeForegroundService : Service() {
}
}
/**
* Safely updates the target foreground service if it is already starting.
*
* @return True if we updated a started service, otherwise false.
*/
@CheckReturnValue
fun update(context: Context, serviceClass: Class<out SafeForegroundService>, extras: Bundle = Bundle.EMPTY): Boolean {
stateLock.withLock {
val state = currentState(serviceClass)
Log.d(TAG, "[update] Current state: $state")
return when (state) {
State.STARTING -> {
Log.d(TAG, "[update] Updating service.")
try {
ForegroundServiceUtil.startWhenCapable(
context = context,
intent = Intent(context, serviceClass).apply {
action = ACTION_UPDATE
putExtras(extras)
}
)
true
} catch (e: UnableToStartException) {
Log.w(TAG, "Failed to update service class $serviceClass", e)
false
}
}
else -> {
Log.d(TAG, "[update] Service cannot be updated. Current state: $state")
false
}
}
}
}
fun isStopping(intent: Intent): Boolean {
return intent.action == ACTION_STOP
}
@@ -158,6 +197,9 @@ abstract class SafeForegroundService : Service() {
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
stopSelf()
}
ACTION_UPDATE -> {
onServiceUpdateCommandReceived(intent)
}
else -> Log.w(tag, "Unknown action: $action")
}
@@ -210,6 +252,9 @@ abstract class SafeForegroundService : Service() {
/** Event listener for when the service is stopped via an intent. */
open fun onServiceStopCommandReceived(intent: Intent) = Unit
/** Event listener for when the service is updated via an intent. */
open fun onServiceUpdateCommandReceived(intent: Intent) = Unit
private enum class State {
/** The service is not running. */
STOPPED,

View File

@@ -22,7 +22,6 @@ import androidx.core.app.NotificationManagerCompat
import androidx.core.os.bundleOf
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.core.SingleObserver
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.kotlin.subscribeBy
import io.reactivex.rxjava3.schedulers.Schedulers
@@ -59,6 +58,8 @@ class ActiveCallManager(
companion object {
private val TAG = Log.tag(ActiveCallManager::class.java)
private val requiresAsyncNotificationLoad = Build.VERSION.SDK_INT <= 29
private var activeCallManager: ActiveCallManager? = null
private val activeCallManagerLock = ReentrantLock()
@@ -141,6 +142,7 @@ class ActiveCallManager(
private val webSocketKeepAliveTask: WebSocketKeepAliveTask = WebSocketKeepAliveTask()
private var signalAudioManager: SignalAudioManager? = null
private var previousNotificationId = -1
private var previousNotificationDisposable = Disposable.disposed()
init {
registerUncaughtExceptionHandler()
@@ -152,6 +154,8 @@ class ActiveCallManager(
fun shutdown() {
Log.v(TAG, "shutdown")
previousNotificationDisposable.dispose()
uncaughtExceptionHandlerManager?.unregister()
uncaughtExceptionHandlerManager = null
@@ -170,6 +174,7 @@ class ActiveCallManager(
fun update(type: Int, recipientId: RecipientId, isVideoCall: Boolean) {
Log.i(TAG, "update $type $recipientId $isVideoCall")
previousNotificationDisposable.dispose()
val notificationId = CallNotificationBuilder.getNotificationId(type)
@@ -179,29 +184,22 @@ class ActiveCallManager(
previousNotificationId = notificationId
if (type != CallNotificationBuilder.TYPE_ESTABLISHED) {
val requiresAsyncNotificationLoad = Build.VERSION.SDK_INT <= 29
if (type == CallNotificationBuilder.TYPE_INCOMING_RINGING || type == CallNotificationBuilder.TYPE_INCOMING_CONNECTING) {
val notification = CallNotificationBuilder.getCallInProgressNotification(application, type, Recipient.resolved(recipientId), isVideoCall, requiresAsyncNotificationLoad)
NotificationManagerCompat.from(application).notify(notificationId, notification)
if (requiresAsyncNotificationLoad) {
Single.fromCallable { CallNotificationBuilder.getCallInProgressNotification(application, type, Recipient.resolved(recipientId), isVideoCall, false) }
previousNotificationDisposable = Single.fromCallable { CallNotificationBuilder.getCallInProgressNotification(application, type, Recipient.resolved(recipientId), isVideoCall, false) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : SingleObserver<Notification> {
override fun onSuccess(t: Notification) {
if (NotificationManagerCompat.from(application).activeNotifications.any { n -> n.id == notificationId }) {
NotificationManagerCompat.from(application).notify(notificationId, notification!!)
}
.subscribeBy { asyncNotification ->
if (NotificationManagerCompat.from(application).activeNotifications.any { n -> n.id == notificationId }) {
NotificationManagerCompat.from(application).notify(notificationId, asyncNotification)
}
override fun onSubscribe(d: Disposable) = Unit
override fun onError(e: Throwable) = Unit
})
}
}
} else {
ActiveCallForegroundService.start(application, recipientId, isVideoCall)
ActiveCallForegroundService.update(application, type, recipientId, isVideoCall)
}
}
@@ -268,15 +266,19 @@ class ActiveCallManager(
companion object {
private const val EXTRA_RECIPIENT_ID = "RECIPIENT_ID"
private const val EXTRA_IS_VIDEO_CALL = "IS_VIDEO_CALL"
private const val EXTRA_TYPE = "TYPE"
fun start(context: Context, recipientId: RecipientId, isVideoCall: Boolean) {
fun update(context: Context, @CallNotificationBuilder.CallNotificationType type: Int, recipientId: RecipientId, isVideoCall: Boolean) {
val extras = bundleOf(
EXTRA_TYPE to type,
EXTRA_RECIPIENT_ID to recipientId,
EXTRA_IS_VIDEO_CALL to isVideoCall
)
if (!SafeForegroundService.start(context, ActiveCallForegroundService::class.java, extras)) {
throw UnableToStartException(Exception())
if (!SafeForegroundService.update(context, ActiveCallForegroundService::class.java, extras)) {
if (!SafeForegroundService.start(context, ActiveCallForegroundService::class.java, extras)) {
throw UnableToStartException(Exception())
}
}
}
@@ -296,9 +298,17 @@ class ActiveCallManager(
get() = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
private var hangUpRtcOnDeviceCallAnswered: PhoneStateListener? = null
private var notification: Notification? = null
private var notificationDisposable: Disposable = Disposable.disposed()
@Volatile
private var asyncServiceNotification: Notification? = null
@Volatile
private var lastAsyncServiceNotificationRequestTime: Long = 0
@Volatile
private var lastAsyncServiceNotificationType: Int = -1
override fun onCreate() {
super.onCreate()
hangUpRtcOnDeviceCallAnswered = HangUpRtcOnPstnCallAnsweredListener()
@@ -312,9 +322,11 @@ class ActiveCallManager(
}
}
override fun onDestroy() {
override fun onServiceStopCommandReceived(intent: Intent) {
notificationDisposable.dispose()
}
override fun onDestroy() {
super.onDestroy()
if (!AndroidTelecomUtil.telecomSupported) {
@@ -323,42 +335,47 @@ class ActiveCallManager(
}
override fun getForegroundNotification(intent: Intent): Notification {
notificationDisposable.dispose()
if (SafeForegroundService.isStopping(intent)) {
Log.v(TAG, "Service is stopping, using generic stopping notification")
return CallNotificationBuilder.getStoppingNotification(this)
}
if (notification != null) {
return notification!!
} else if (!intent.hasExtra(EXTRA_RECIPIENT_ID)) {
if (!intent.hasExtra(EXTRA_RECIPIENT_ID) || !intent.hasExtra(EXTRA_TYPE)) {
Log.w(TAG, "Missing required data, service is stopping, using generic stopping notification")
return CallNotificationBuilder.getStoppingNotification(this)
}
val type = intent.getIntExtra(EXTRA_TYPE, 0)
val recipient: Recipient = Recipient.resolved(intent.getParcelableExtra(EXTRA_RECIPIENT_ID)!!)
val isVideoCall = intent.getBooleanExtra(EXTRA_IS_VIDEO_CALL, false)
val requiresAsyncNotificationLoad = Build.VERSION.SDK_INT <= 29
notification = createNotification(recipient, isVideoCall, skipAvatarLoad = requiresAsyncNotificationLoad)
if (requiresAsyncNotificationLoad) {
notificationDisposable = Single.fromCallable { createNotification(recipient, isVideoCall, skipAvatarLoad = false) }
if (asyncServiceNotification != null && lastAsyncServiceNotificationType == type) {
return asyncServiceNotification!!
}
val requestTime = System.currentTimeMillis()
lastAsyncServiceNotificationRequestTime = requestTime
notificationDisposable = Single.fromCallable { createNotification(type, recipient, isVideoCall, skipAvatarLoad = false) }
.subscribeOn(Schedulers.io())
.filter { requestTime == lastAsyncServiceNotificationRequestTime }
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy {
notification = it
if (NotificationManagerCompat.from(this).activeNotifications.any { n -> n.id == notificationId }) {
NotificationManagerCompat.from(application).notify(notificationId, notification!!)
}
.subscribeBy { notification ->
lastAsyncServiceNotificationType = type
asyncServiceNotification = notification
update(this, type, recipient.id, isVideoCall)
}
}
return notification!!
return createNotification(type, recipient, isVideoCall, skipAvatarLoad = requiresAsyncNotificationLoad)
}
private fun createNotification(recipient: Recipient, isVideoCall: Boolean, skipAvatarLoad: Boolean): Notification {
private fun createNotification(type: Int, recipient: Recipient, isVideoCall: Boolean, skipAvatarLoad: Boolean): Notification {
return CallNotificationBuilder.getCallInProgressNotification(
this,
CallNotificationBuilder.TYPE_ESTABLISHED,
type,
recipient,
isVideoCall,
skipAvatarLoad