diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/SafeForegroundService.kt b/app/src/main/java/org/thoughtcrime/securesms/service/SafeForegroundService.kt index 0f4c624ffc..cbbb0d3ef9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/SafeForegroundService.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/SafeForegroundService.kt @@ -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, 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, 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, diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ActiveCallManager.kt b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ActiveCallManager.kt index b9545a9bd8..5e2c243e7f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ActiveCallManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ActiveCallManager.kt @@ -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 { - 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