Added data sync keep alive service for third party accounts without push notifications

This commit is contained in:
Sylvain Berfini 2024-04-22 14:32:55 +02:00
parent 7f1dc95cfc
commit 486f905d65
12 changed files with 279 additions and 13 deletions

View file

@ -163,6 +163,13 @@
</intent-filter>
</service>
<service
android:name=".core.CoreKeepAliveThirdPartyAccountsService"
android:exported="false"
android:foregroundServiceType="dataSync"
android:stopWithTask="false"
android:label="@string/app_name" />
<!-- Receivers -->
<receiver android:name=".core.CorePushReceiver"

View file

@ -23,6 +23,8 @@ import android.app.Activity
import android.app.Notification
import android.app.PictureInPictureParams
import android.app.Service
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.MediaStore
import org.linphone.core.tools.Log
@ -43,6 +45,10 @@ class Api28Compatibility {
}
}
fun startForegroundService(context: Context, intent: Intent) {
context.startForegroundService(intent)
}
fun enterPipMode(activity: Activity): Boolean {
val params = PictureInPictureParams.Builder()
.setAspectRatio(AppUtils.getPipRatio(activity))

View file

@ -20,9 +20,11 @@
package org.linphone.compatibility
import android.app.Activity
import android.app.ForegroundServiceStartNotAllowedException
import android.app.PictureInPictureParams
import android.app.UiModeManager
import android.content.Context
import android.content.Intent
import android.graphics.RenderEffect
import android.graphics.Shader
import android.os.Build
@ -78,5 +80,17 @@ class Api31Compatibility {
}
uiManager?.setApplicationNightMode(UiModeManager.MODE_NIGHT_AUTO)
}
fun startForegroundService(context: Context, intent: Intent) {
try {
context.startForegroundService(intent)
} catch (fssnae: ForegroundServiceStartNotAllowedException) {
Log.e("$TAG Can't start service as foreground! $fssnae")
} catch (se: SecurityException) {
Log.e("$TAG Can't start service as foreground! $se")
} catch (e: Exception) {
Log.e("$TAG Can't start service as foreground! $e")
}
}
}
}

View file

@ -37,6 +37,7 @@ class Compatibility {
companion object {
private const val TAG = "[Compatibility]"
const val FOREGROUND_SERVICE_TYPE_DATA_SYNC = 1 // Matches ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
const val FOREGROUND_SERVICE_TYPE_PHONE_CALL = 4 // Matches ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
const val FOREGROUND_SERVICE_TYPE_CAMERA = 64 // Matches ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
const val FOREGROUND_SERVICE_TYPE_MICROPHONE = 128 // ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
@ -59,6 +60,14 @@ class Compatibility {
}
}
fun startForegroundService(context: Context, intent: Intent) {
if (Version.sdkAboveOrEqual(Version.API31_ANDROID_12)) {
Api31Compatibility.startForegroundService(context, intent)
} else {
Api28Compatibility.startForegroundService(context, intent)
}
}
fun setBlurRenderEffect(view: View) {
if (Version.sdkAboveOrEqual(Version.API31_ANDROID_12)) {
Api31Compatibility.setBlurRenderEffect(view)

View file

@ -36,6 +36,7 @@ import androidx.lifecycle.MutableLiveData
import com.google.firebase.crashlytics.FirebaseCrashlytics
import org.linphone.BuildConfig
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.compatibility.Compatibility
import org.linphone.contacts.ContactsManager
import org.linphone.core.tools.Log
import org.linphone.notifications.NotificationsManager
@ -279,6 +280,10 @@ class CoreContext @UiThread constructor(val context: Context) : HandlerThread("C
corePreferences.linphoneConfigurationVersion = CorePreferences.CURRENT_VERSION
Log.i("$TAG Report Core created and started")
if (corePreferences.keepServiceAlive) {
startKeepAliveService()
}
}
@WorkerThread
@ -529,6 +534,30 @@ class CoreContext @UiThread constructor(val context: Context) : HandlerThread("C
context.startActivity(intent)
}
@WorkerThread
fun startKeepAliveService() {
val serviceIntent = Intent(Intent.ACTION_MAIN).setClass(
context,
CoreKeepAliveThirdPartyAccountsService::class.java
)
Log.i(
"$TAG Starting Keep alive for third party accounts Service (as foreground)"
)
Compatibility.startForegroundService(context, serviceIntent)
}
@WorkerThread
fun stopKeepAliveService() {
val serviceIntent = Intent(Intent.ACTION_MAIN).setClass(
context,
CoreKeepAliveThirdPartyAccountsService::class.java
)
Log.i(
"$TAG Stopping Keep alive for third party accounts Service"
)
context.stopService(serviceIntent)
}
@WorkerThread
fun updateFriendListsSubscriptionDependingOnDefaultAccount() {
val account = core.defaultAccount

View file

@ -0,0 +1,60 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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
}
}

View file

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

View file

@ -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<String, Notifiable> = HashMap()
private val chatNotificationsMap: HashMap<String, Notifiable> = 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)
}

View file

@ -115,7 +115,9 @@ class SettingsViewModel @UiThread constructor() : ViewModel() {
)
val availableThemesValues = arrayListOf(-1, 0, 1)
// Advanced setttings
// Advanced settings
val keepAliveThirdPartyAccountsService = MutableLiveData<Boolean>()
val remoteProvisioningUrl = MutableLiveData<String>()
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 ->

View file

@ -57,6 +57,33 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.materialswitch.MaterialSwitch
style="@style/material_switch_style"
android:id="@+id/keep_alive_service_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:checked="@{viewModel.keepAliveThirdPartyAccountsService}"
android:onClick="@{() -> viewModel.toggleKeepAliveThirdPartyAccountService()}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/header_style"
android:id="@+id/push_notifications_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="10dp"
android:text="@string/settings_advanced_keep_alive_service_title"
android:maxLines="2"
android:ellipsize="end"
app:layout_constraintTop_toTopOf="@id/keep_alive_service_switch"
app:layout_constraintBottom_toBottomOf="@id/keep_alive_service_switch"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/keep_alive_service_switch"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/header_style"
android:id="@+id/remote_provisioning_label"
@ -68,7 +95,7 @@
android:paddingBottom="8dp"
android:text="@string/settings_advanced_remote_provisioning_url"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
app:layout_constraintTop_toBottomOf="@id/keep_alive_service_switch"/>
<androidx.appcompat.widget.AppCompatEditText
style="@style/default_text_style"
@ -91,6 +118,8 @@
app:layout_constraintStart_toStartOf="@id/remote_provisioning_label"
app:layout_constraintEnd_toEndOf="parent"/>
<!-- TODO: apply button ? -->
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View file

@ -44,6 +44,7 @@
<string name="notification_channel_incoming_call_name">&appName; notifications d\'appels entrants</string>
<string name="notification_channel_missed_call_name">&appName; notifications d\'appels manqués</string>
<string name="notification_channel_service_name">&appName; notification de service</string>
<string name="notification_channel_service_desc">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.</string>
<string name="notification_channel_chat_name">&appName; notifications des conversations</string>
<string name="notification_chat_message_reaction_received">A réagi par %s à : %s</string>
<string name="notification_mark_message_as_read">Marquer comme lu</string>
@ -256,7 +257,10 @@
<string name="settings_user_interface_dark_theme_label">Sombre</string>
<string name="settings_user_interface_light_theme_label">Clair</string>
<string name="settings_user_interface_auto_theme_label">Auto</string>
<string name="settings_advanced_title">Paramètres avancés</string>
<string name="settings_advanced_keep_alive_service_title">Garder l\'app en vie via un Service</string>
<string name="settings_advanced_remote_provisioning_url">URL de configuration distante</string>
<!-- Account profile & settings -->
<string name="manage_account_title">Votre compte</string>

View file

@ -79,6 +79,7 @@
<string name="notification_channel_incoming_call_name">&appName; incoming calls notifications</string>
<string name="notification_channel_missed_call_name">&appName; missed calls notifications</string>
<string name="notification_channel_service_name">&appName; service notification</string>
<string name="notification_channel_service_desc">This service will run all the time to keep app alive and allow you to receive calls and messages without push notifications.</string>
<string name="notification_channel_chat_name">&appName; instant messages notifications</string>
<string name="notification_chat_message_reaction_received">Reacted by %s to: %s</string>
<string name="notification_mark_message_as_read">Mark as read</string>
@ -291,8 +292,9 @@
<string name="settings_user_interface_dark_theme_label">Dark theme</string>
<string name="settings_user_interface_light_theme_label">Light theme</string>
<string name="settings_user_interface_auto_theme_label">Auto</string>
<string name="settings_advanced_title">Advanced settings</string>
<string name="settings_advanced_title">Advanced settings</string>
<string name="settings_advanced_keep_alive_service_title">Keep app alive using Service</string>
<string name="settings_advanced_remote_provisioning_url">Remote provisioning URL</string>
<!-- Account profile & settings -->