mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-23 11:15:44 +00:00
Fix crash when outgoing call picked up while in the background.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user