From 486f905d6534cfd92bb8a4d368269f410e65f0c5 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 22 Apr 2024 14:32:55 +0200 Subject: [PATCH] Added data sync keep alive service for third party accounts without push notifications --- app/src/main/AndroidManifest.xml | 7 ++ .../compatibility/Api28Compatibility.kt | 6 ++ .../compatibility/Api31Compatibility.kt | 14 +++ .../linphone/compatibility/Compatibility.kt | 9 ++ .../java/org/linphone/core/CoreContext.kt | 29 ++++++ .../CoreKeepAliveThirdPartyAccountsService.kt | 60 ++++++++++++ .../java/org/linphone/core/CorePreferences.kt | 9 ++ .../notifications/NotificationsManager.kt | 98 +++++++++++++++++-- .../settings/viewmodel/SettingsViewModel.kt | 21 +++- .../res/layout/settings_advanced_fragment.xml | 31 +++++- app/src/main/res/values-fr/strings.xml | 4 + app/src/main/res/values/strings.xml | 4 +- 12 files changed, 279 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/org/linphone/core/CoreKeepAliveThirdPartyAccountsService.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 516993228..d4385d2a7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -163,6 +163,13 @@ + + . + */ +package org.linphone.core + +import android.app.Service +import android.content.Intent +import android.os.IBinder +import androidx.annotation.MainThread +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.tools.Log + +@MainThread +class CoreKeepAliveThirdPartyAccountsService : Service() { + companion object { + private const val TAG = "[Core Keep Alive Third Party Accounts Service]" + } + + override fun onCreate() { + super.onCreate() + Log.i("$TAG Created") + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + Log.i("$TAG onStartCommand") + coreContext.notificationsManager.onKeepAliveServiceStarted(this) + return super.onStartCommand(intent, flags, startId) + } + + override fun onTaskRemoved(rootIntent: Intent?) { + Log.i("$TAG Task removed, doing nothing") + super.onTaskRemoved(rootIntent) + } + + override fun onDestroy() { + Log.i("$TAG onDestroy") + coreContext.notificationsManager.onKeepAliveServiceDestroyed() + super.onDestroy() + } + + override fun onBind(p0: Intent?): IBinder? { + return null + } +} diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index d005b8623..d6e3db597 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -71,6 +71,13 @@ class CorePreferences @UiThread constructor(private val context: Context) { config.setBool("app", "publish_presence", value) } + @get:WorkerThread @set:WorkerThread + var keepServiceAlive: Boolean + get() = config.getBool("app", "keep_service_alive", false) + set(value) { + config.setBool("app", "keep_service_alive", value) + } + // Calls settings @get:WorkerThread @set:WorkerThread @@ -104,6 +111,7 @@ class CorePreferences @UiThread constructor(private val context: Context) { // Conversation settings + @get:WorkerThread @set:WorkerThread var exportMediaToNativeGallery: Boolean // TODO: use it! // Keep old name for backward compatibility get() = config.getBool("app", "make_downloaded_images_public_in_gallery", true) @@ -113,6 +121,7 @@ class CorePreferences @UiThread constructor(private val context: Context) { /* Voice Recordings */ + @get:WorkerThread @set:WorkerThread var voiceRecordingMaxDuration: Int get() = config.getInt("app", "voice_recording_max_duration", 600000) // in ms set(value) = config.setInt("app", "voice_recording_max_duration", value) diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 596411849..c6f87966d 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -61,6 +61,7 @@ import org.linphone.core.ChatMessageReaction import org.linphone.core.ChatRoom import org.linphone.core.Core import org.linphone.core.CoreForegroundService +import org.linphone.core.CoreKeepAliveThirdPartyAccountsService import org.linphone.core.CoreListenerStub import org.linphone.core.CorePreferences import org.linphone.core.Friend @@ -93,10 +94,12 @@ class NotificationsManager @MainThread constructor(private val context: Context) const val CHAT_NOTIFICATIONS_GROUP = "CHAT_NOTIF_GROUP" private const val INCOMING_CALL_ID = 1 + private const val KEEP_ALIVE_FOR_THIRD_PARTY_ACCOUNTS_ID = 5 private const val MISSED_CALL_ID = 10 } private var currentForegroundServiceNotificationId = -1 + private var currentKeepAliveThirdPartyAccountsForegroundServiceNotificationId = -1 private var currentlyRingingCallRemoteAddress: Address? = null @@ -137,7 +140,7 @@ class NotificationsManager @MainThread constructor(private val context: Context) val notifiable = getNotifiableForCall(call) if (notifiable.notificationId == currentForegroundServiceNotificationId) { Log.i( - "$TAG Update foreground service type in case video was enabled/disabled since last time" + "$TAG Update foreground Service type in case video was enabled/disabled since last time" ) startCallForeground(call) } @@ -349,6 +352,7 @@ class NotificationsManager @MainThread constructor(private val context: Context) } private var coreService: CoreForegroundService? = null + private var keepAliveService: CoreKeepAliveThirdPartyAccountsService? = null private val callNotificationsMap: HashMap = HashMap() private val chatNotificationsMap: HashMap = HashMap() @@ -397,7 +401,7 @@ class NotificationsManager @MainThread constructor(private val context: Context) stopCallForeground() } else if (currentForegroundServiceNotificationId == -1) { Log.i( - "$TAG At least a call is still running and no foreground service notification was found" + "$TAG At least a call is still running and no foreground Service notification was found" ) val call = core.currentCall ?: core.calls.first() startCallForeground(call) @@ -411,6 +415,20 @@ class NotificationsManager @MainThread constructor(private val context: Context) coreService = null } + @MainThread + fun onKeepAliveServiceStarted(service: CoreKeepAliveThirdPartyAccountsService) { + Log.i("$TAG Keep app alive for third party accounts Service has been started") + keepAliveService = service + startKeepAliveServiceForeground() + } + + @MainThread + fun onKeepAliveServiceDestroyed() { + Log.i("$TAG Keep app alive for third party accounts Service has been destroyed") + stopKeepAliveServiceForeground() + keepAliveService = null + } + @MainThread private fun createChannels(clearPreviousChannels: Boolean) { if (clearPreviousChannels) { @@ -421,7 +439,7 @@ class NotificationsManager @MainThread constructor(private val context: Context) } } - createServiceChannel() + createThirdPartyAccountKeepAliveServiceChannel() createIncomingCallNotificationChannel() createMissedCallNotificationChannel() createActiveCallNotificationChannel() @@ -608,7 +626,7 @@ class NotificationsManager @MainThread constructor(private val context: Context) ) { mask = mask or Compatibility.FOREGROUND_SERVICE_TYPE_MICROPHONE Log.i( - "$TAG RECORD_AUDIO permission has been granted, adding FOREGROUND_SERVICE_TYPE_MICROPHONE to foreground service types mask" + "$TAG RECORD_AUDIO permission has been granted, adding FOREGROUND_SERVICE_TYPE_MICROPHONE to foreground Service types mask" ) } val isSendingVideo = when (call.currentParams.videoDirection) { @@ -623,7 +641,7 @@ class NotificationsManager @MainThread constructor(private val context: Context) ) { mask = mask or Compatibility.FOREGROUND_SERVICE_TYPE_CAMERA Log.i( - "$TAG CAMERA permission has been granted, adding FOREGROUND_SERVICE_TYPE_CAMERA to foreground service types mask" + "$TAG CAMERA permission has been granted, adding FOREGROUND_SERVICE_TYPE_CAMERA to foreground Service types mask" ) } } @@ -651,13 +669,13 @@ class NotificationsManager @MainThread constructor(private val context: Context) val service = coreService if (service != null) { Log.i( - "$TAG Stopping foreground service (was using notification ID [$currentForegroundServiceNotificationId])" + "$TAG Stopping foreground Service (was using notification ID [$currentForegroundServiceNotificationId])" ) service.stopForeground(STOP_FOREGROUND_REMOVE) service.stopSelf() currentForegroundServiceNotificationId = -1 } else { - Log.w("$TAG Can't stop foreground service & notif, no service was found") + Log.w("$TAG Can't stop foreground Service & notif, no Service was found") } } @@ -806,7 +824,7 @@ class NotificationsManager @MainThread constructor(private val context: Context) if (coreService == null && tag == null) { // We can't notify using CallStyle if there isn't a foreground service running Log.w( - "$TAG Foreground service hasn't started yet, can't display a CallStyle notification until then: $iae" + "$TAG Foreground Service hasn't started yet, can't display a CallStyle notification until then: $iae" ) } else { Log.e("$TAG Illegal Argument Exception occurred: $iae") @@ -1245,6 +1263,66 @@ class NotificationsManager @MainThread constructor(private val context: Context) .build() } + @MainThread + private fun startKeepAliveServiceForeground() { + Log.i( + "$TAG Trying to start keep alive for third party accounts foreground Service using call notification" + ) + + val channelId = context.getString(R.string.notification_channel_service_id) + val channel = notificationManager.getNotificationChannel(channelId) + val importance = channel?.importance ?: NotificationManagerCompat.IMPORTANCE_NONE + if (importance == NotificationManagerCompat.IMPORTANCE_NONE) { + Log.e( + "$TAG Keep alive for third party accounts Service channel has been disabled, can't start foreground service!" + ) + return + } + + val service = keepAliveService + if (service != null) { + val builder = NotificationCompat.Builder(context, channelId) + .setContentTitle(context.getString(R.string.app_name)) + .setSmallIcon(R.drawable.linphone_notification) + .setAutoCancel(false) + .setOngoing(true) + .setCategory(NotificationCompat.CATEGORY_SERVICE) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setShowWhen(false) + val notification = builder.build() + + Log.i( + "$TAG Keep alive for third party accounts Service found, starting it as foreground using notification ID [$KEEP_ALIVE_FOR_THIRD_PARTY_ACCOUNTS_ID] with type DATA_SYNC" + ) + Compatibility.startServiceForeground( + service, + KEEP_ALIVE_FOR_THIRD_PARTY_ACCOUNTS_ID, + notification, + Compatibility.FOREGROUND_SERVICE_TYPE_DATA_SYNC + ) + currentKeepAliveThirdPartyAccountsForegroundServiceNotificationId = KEEP_ALIVE_FOR_THIRD_PARTY_ACCOUNTS_ID + } else { + Log.w("$TAG Keep alive for third party accounts Service hasn't started yet...") + } + } + + @MainThread + private fun stopKeepAliveServiceForeground() { + val service = keepAliveService + if (service != null) { + Log.i( + "$TAG Stopping keep alive for third party accounts foreground Service (was using notification ID [$currentKeepAliveThirdPartyAccountsForegroundServiceNotificationId])" + ) + service.stopForeground(STOP_FOREGROUND_REMOVE) + service.stopSelf() + currentKeepAliveThirdPartyAccountsForegroundServiceNotificationId = -1 + } else { + Log.w( + "$TAG Can't stop keep alive for third party accounts foreground Service & notif, no Service was found" + ) + } + } + @MainThread private fun createIncomingCallNotificationChannel() { val id = context.getString(R.string.notification_channel_incoming_call_id) @@ -1302,12 +1380,12 @@ class NotificationsManager @MainThread constructor(private val context: Context) } @MainThread - private fun createServiceChannel() { + private fun createThirdPartyAccountKeepAliveServiceChannel() { val id = context.getString(R.string.notification_channel_service_id) val name = context.getString(R.string.notification_channel_service_name) val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_LOW).apply { - description = name + description = context.getString(R.string.notification_channel_service_desc) } notificationManager.createNotificationChannel(channel) } diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt index dae692d9a..130f252d5 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt @@ -115,7 +115,9 @@ class SettingsViewModel @UiThread constructor() : ViewModel() { ) val availableThemesValues = arrayListOf(-1, 0, 1) - // Advanced setttings + // Advanced settings + val keepAliveThirdPartyAccountsService = MutableLiveData() + val remoteProvisioningUrl = MutableLiveData() init { @@ -159,6 +161,8 @@ class SettingsViewModel @UiThread constructor() : ViewModel() { theme.postValue(corePreferences.darkMode) + keepAliveThirdPartyAccountsService.postValue(corePreferences.keepServiceAlive) + remoteProvisioningUrl.postValue(core.provisioningUri) } } @@ -356,6 +360,21 @@ class SettingsViewModel @UiThread constructor() : ViewModel() { } } + @UiThread + fun toggleKeepAliveThirdPartyAccountService() { + val newValue = keepAliveThirdPartyAccountsService.value == false + + coreContext.postOnCoreThread { + corePreferences.keepServiceAlive = newValue + keepAliveThirdPartyAccountsService.postValue(newValue) + if (newValue) { + coreContext.startKeepAliveService() + } else { + coreContext.stopKeepAliveService() + } + } + } + @UiThread fun updateRemoteProvisioningUrl() { coreContext.postOnCoreThread { core -> diff --git a/app/src/main/res/layout/settings_advanced_fragment.xml b/app/src/main/res/layout/settings_advanced_fragment.xml index 707de6ce8..1ac77ea8f 100644 --- a/app/src/main/res/layout/settings_advanced_fragment.xml +++ b/app/src/main/res/layout/settings_advanced_fragment.xml @@ -57,6 +57,33 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> + + + + + app:layout_constraintTop_toBottomOf="@id/keep_alive_service_switch"/> + + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 6c2b5db2a..6c53affa9 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -44,6 +44,7 @@ &appName; notifications d\'appels entrants &appName; notifications d\'appels manqués &appName; notification de service + Ce service sera actif en permanence pour garder l\'app en vie et vous permettre de recevoir appels et messages sans recourir aux notifications push. &appName; notifications des conversations A réagi par %s à : %s Marquer comme lu @@ -256,7 +257,10 @@ Sombre Clair Auto + Paramètres avancés + Garder l\'app en vie via un Service + URL de configuration distante Votre compte diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d8702fbb0..0992705e6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -79,6 +79,7 @@ &appName; incoming calls notifications &appName; missed calls notifications &appName; service notification + This service will run all the time to keep app alive and allow you to receive calls and messages without push notifications. &appName; instant messages notifications Reacted by %s to: %s Mark as read @@ -291,8 +292,9 @@ Dark theme Light theme Auto - Advanced settings + Advanced settings + Keep app alive using Service Remote provisioning URL