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> </intent-filter>
</service> </service>
<service
android:name=".core.CoreKeepAliveThirdPartyAccountsService"
android:exported="false"
android:foregroundServiceType="dataSync"
android:stopWithTask="false"
android:label="@string/app_name" />
<!-- Receivers --> <!-- Receivers -->
<receiver android:name=".core.CorePushReceiver" <receiver android:name=".core.CorePushReceiver"

View file

@ -23,6 +23,8 @@ import android.app.Activity
import android.app.Notification import android.app.Notification
import android.app.PictureInPictureParams import android.app.PictureInPictureParams
import android.app.Service import android.app.Service
import android.content.Context
import android.content.Intent
import android.net.Uri import android.net.Uri
import android.provider.MediaStore import android.provider.MediaStore
import org.linphone.core.tools.Log 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 { fun enterPipMode(activity: Activity): Boolean {
val params = PictureInPictureParams.Builder() val params = PictureInPictureParams.Builder()
.setAspectRatio(AppUtils.getPipRatio(activity)) .setAspectRatio(AppUtils.getPipRatio(activity))

View file

@ -20,9 +20,11 @@
package org.linphone.compatibility package org.linphone.compatibility
import android.app.Activity import android.app.Activity
import android.app.ForegroundServiceStartNotAllowedException
import android.app.PictureInPictureParams import android.app.PictureInPictureParams
import android.app.UiModeManager import android.app.UiModeManager
import android.content.Context import android.content.Context
import android.content.Intent
import android.graphics.RenderEffect import android.graphics.RenderEffect
import android.graphics.Shader import android.graphics.Shader
import android.os.Build import android.os.Build
@ -78,5 +80,17 @@ class Api31Compatibility {
} }
uiManager?.setApplicationNightMode(UiModeManager.MODE_NIGHT_AUTO) 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 { companion object {
private const val TAG = "[Compatibility]" 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_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_CAMERA = 64 // Matches ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
const val FOREGROUND_SERVICE_TYPE_MICROPHONE = 128 // ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE 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) { fun setBlurRenderEffect(view: View) {
if (Version.sdkAboveOrEqual(Version.API31_ANDROID_12)) { if (Version.sdkAboveOrEqual(Version.API31_ANDROID_12)) {
Api31Compatibility.setBlurRenderEffect(view) Api31Compatibility.setBlurRenderEffect(view)

View file

@ -36,6 +36,7 @@ import androidx.lifecycle.MutableLiveData
import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.crashlytics.FirebaseCrashlytics
import org.linphone.BuildConfig import org.linphone.BuildConfig
import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.compatibility.Compatibility
import org.linphone.contacts.ContactsManager import org.linphone.contacts.ContactsManager
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.notifications.NotificationsManager import org.linphone.notifications.NotificationsManager
@ -279,6 +280,10 @@ class CoreContext @UiThread constructor(val context: Context) : HandlerThread("C
corePreferences.linphoneConfigurationVersion = CorePreferences.CURRENT_VERSION corePreferences.linphoneConfigurationVersion = CorePreferences.CURRENT_VERSION
Log.i("$TAG Report Core created and started") Log.i("$TAG Report Core created and started")
if (corePreferences.keepServiceAlive) {
startKeepAliveService()
}
} }
@WorkerThread @WorkerThread
@ -529,6 +534,30 @@ class CoreContext @UiThread constructor(val context: Context) : HandlerThread("C
context.startActivity(intent) 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 @WorkerThread
fun updateFriendListsSubscriptionDependingOnDefaultAccount() { fun updateFriendListsSubscriptionDependingOnDefaultAccount() {
val account = core.defaultAccount 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) 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 // Calls settings
@get:WorkerThread @set:WorkerThread @get:WorkerThread @set:WorkerThread
@ -104,6 +111,7 @@ class CorePreferences @UiThread constructor(private val context: Context) {
// Conversation settings // Conversation settings
@get:WorkerThread @set:WorkerThread
var exportMediaToNativeGallery: Boolean // TODO: use it! var exportMediaToNativeGallery: Boolean // TODO: use it!
// Keep old name for backward compatibility // Keep old name for backward compatibility
get() = config.getBool("app", "make_downloaded_images_public_in_gallery", true) 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 */ /* Voice Recordings */
@get:WorkerThread @set:WorkerThread
var voiceRecordingMaxDuration: Int var voiceRecordingMaxDuration: Int
get() = config.getInt("app", "voice_recording_max_duration", 600000) // in ms get() = config.getInt("app", "voice_recording_max_duration", 600000) // in ms
set(value) = config.setInt("app", "voice_recording_max_duration", value) 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.ChatRoom
import org.linphone.core.Core import org.linphone.core.Core
import org.linphone.core.CoreForegroundService import org.linphone.core.CoreForegroundService
import org.linphone.core.CoreKeepAliveThirdPartyAccountsService
import org.linphone.core.CoreListenerStub import org.linphone.core.CoreListenerStub
import org.linphone.core.CorePreferences import org.linphone.core.CorePreferences
import org.linphone.core.Friend 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" const val CHAT_NOTIFICATIONS_GROUP = "CHAT_NOTIF_GROUP"
private const val INCOMING_CALL_ID = 1 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 const val MISSED_CALL_ID = 10
} }
private var currentForegroundServiceNotificationId = -1 private var currentForegroundServiceNotificationId = -1
private var currentKeepAliveThirdPartyAccountsForegroundServiceNotificationId = -1
private var currentlyRingingCallRemoteAddress: Address? = null private var currentlyRingingCallRemoteAddress: Address? = null
@ -137,7 +140,7 @@ class NotificationsManager @MainThread constructor(private val context: Context)
val notifiable = getNotifiableForCall(call) val notifiable = getNotifiableForCall(call)
if (notifiable.notificationId == currentForegroundServiceNotificationId) { if (notifiable.notificationId == currentForegroundServiceNotificationId) {
Log.i( 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) startCallForeground(call)
} }
@ -349,6 +352,7 @@ class NotificationsManager @MainThread constructor(private val context: Context)
} }
private var coreService: CoreForegroundService? = null private var coreService: CoreForegroundService? = null
private var keepAliveService: CoreKeepAliveThirdPartyAccountsService? = null
private val callNotificationsMap: HashMap<String, Notifiable> = HashMap() private val callNotificationsMap: HashMap<String, Notifiable> = HashMap()
private val chatNotificationsMap: HashMap<String, Notifiable> = HashMap() private val chatNotificationsMap: HashMap<String, Notifiable> = HashMap()
@ -397,7 +401,7 @@ class NotificationsManager @MainThread constructor(private val context: Context)
stopCallForeground() stopCallForeground()
} else if (currentForegroundServiceNotificationId == -1) { } else if (currentForegroundServiceNotificationId == -1) {
Log.i( 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() val call = core.currentCall ?: core.calls.first()
startCallForeground(call) startCallForeground(call)
@ -411,6 +415,20 @@ class NotificationsManager @MainThread constructor(private val context: Context)
coreService = null 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 @MainThread
private fun createChannels(clearPreviousChannels: Boolean) { private fun createChannels(clearPreviousChannels: Boolean) {
if (clearPreviousChannels) { if (clearPreviousChannels) {
@ -421,7 +439,7 @@ class NotificationsManager @MainThread constructor(private val context: Context)
} }
} }
createServiceChannel() createThirdPartyAccountKeepAliveServiceChannel()
createIncomingCallNotificationChannel() createIncomingCallNotificationChannel()
createMissedCallNotificationChannel() createMissedCallNotificationChannel()
createActiveCallNotificationChannel() createActiveCallNotificationChannel()
@ -608,7 +626,7 @@ class NotificationsManager @MainThread constructor(private val context: Context)
) { ) {
mask = mask or Compatibility.FOREGROUND_SERVICE_TYPE_MICROPHONE mask = mask or Compatibility.FOREGROUND_SERVICE_TYPE_MICROPHONE
Log.i( 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) { 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 mask = mask or Compatibility.FOREGROUND_SERVICE_TYPE_CAMERA
Log.i( 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 val service = coreService
if (service != null) { if (service != null) {
Log.i( 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.stopForeground(STOP_FOREGROUND_REMOVE)
service.stopSelf() service.stopSelf()
currentForegroundServiceNotificationId = -1 currentForegroundServiceNotificationId = -1
} else { } 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) { if (coreService == null && tag == null) {
// We can't notify using CallStyle if there isn't a foreground service running // We can't notify using CallStyle if there isn't a foreground service running
Log.w( 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 { } else {
Log.e("$TAG Illegal Argument Exception occurred: $iae") Log.e("$TAG Illegal Argument Exception occurred: $iae")
@ -1245,6 +1263,66 @@ class NotificationsManager @MainThread constructor(private val context: Context)
.build() .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 @MainThread
private fun createIncomingCallNotificationChannel() { private fun createIncomingCallNotificationChannel() {
val id = context.getString(R.string.notification_channel_incoming_call_id) val id = context.getString(R.string.notification_channel_incoming_call_id)
@ -1302,12 +1380,12 @@ class NotificationsManager @MainThread constructor(private val context: Context)
} }
@MainThread @MainThread
private fun createServiceChannel() { private fun createThirdPartyAccountKeepAliveServiceChannel() {
val id = context.getString(R.string.notification_channel_service_id) val id = context.getString(R.string.notification_channel_service_id)
val name = context.getString(R.string.notification_channel_service_name) val name = context.getString(R.string.notification_channel_service_name)
val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_LOW).apply { val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_LOW).apply {
description = name description = context.getString(R.string.notification_channel_service_desc)
} }
notificationManager.createNotificationChannel(channel) notificationManager.createNotificationChannel(channel)
} }

View file

@ -115,7 +115,9 @@ class SettingsViewModel @UiThread constructor() : ViewModel() {
) )
val availableThemesValues = arrayListOf(-1, 0, 1) val availableThemesValues = arrayListOf(-1, 0, 1)
// Advanced setttings // Advanced settings
val keepAliveThirdPartyAccountsService = MutableLiveData<Boolean>()
val remoteProvisioningUrl = MutableLiveData<String>() val remoteProvisioningUrl = MutableLiveData<String>()
init { init {
@ -159,6 +161,8 @@ class SettingsViewModel @UiThread constructor() : ViewModel() {
theme.postValue(corePreferences.darkMode) theme.postValue(corePreferences.darkMode)
keepAliveThirdPartyAccountsService.postValue(corePreferences.keepServiceAlive)
remoteProvisioningUrl.postValue(core.provisioningUri) 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 @UiThread
fun updateRemoteProvisioningUrl() { fun updateRemoteProvisioningUrl() {
coreContext.postOnCoreThread { core -> coreContext.postOnCoreThread { core ->

View file

@ -57,6 +57,33 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> 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 <androidx.appcompat.widget.AppCompatTextView
style="@style/header_style" style="@style/header_style"
android:id="@+id/remote_provisioning_label" android:id="@+id/remote_provisioning_label"
@ -68,7 +95,7 @@
android:paddingBottom="8dp" android:paddingBottom="8dp"
android:text="@string/settings_advanced_remote_provisioning_url" android:text="@string/settings_advanced_remote_provisioning_url"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/> app:layout_constraintTop_toBottomOf="@id/keep_alive_service_switch"/>
<androidx.appcompat.widget.AppCompatEditText <androidx.appcompat.widget.AppCompatEditText
style="@style/default_text_style" style="@style/default_text_style"
@ -91,6 +118,8 @@
app:layout_constraintStart_toStartOf="@id/remote_provisioning_label" app:layout_constraintStart_toStartOf="@id/remote_provisioning_label"
app:layout_constraintEnd_toEndOf="parent"/> app:layout_constraintEnd_toEndOf="parent"/>
<!-- TODO: apply button ? -->
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView> </ScrollView>

View file

@ -44,6 +44,7 @@
<string name="notification_channel_incoming_call_name">&appName; notifications d\'appels entrants</string> <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_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_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_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_chat_message_reaction_received">A réagi par %s à : %s</string>
<string name="notification_mark_message_as_read">Marquer comme lu</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_dark_theme_label">Sombre</string>
<string name="settings_user_interface_light_theme_label">Clair</string> <string name="settings_user_interface_light_theme_label">Clair</string>
<string name="settings_user_interface_auto_theme_label">Auto</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_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 --> <!-- Account profile & settings -->
<string name="manage_account_title">Votre compte</string> <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_incoming_call_name">&appName; incoming calls notifications</string>
<string name="notification_channel_missed_call_name">&appName; missed 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_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_channel_chat_name">&appName; instant messages notifications</string>
<string name="notification_chat_message_reaction_received">Reacted by %s to: %s</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> <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_dark_theme_label">Dark theme</string>
<string name="settings_user_interface_light_theme_label">Light 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_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> <string name="settings_advanced_remote_provisioning_url">Remote provisioning URL</string>
<!-- Account profile & settings --> <!-- Account profile & settings -->