mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-23 12:53:18 +00:00
Add UI to select Instant notifications
Prepare for UnifiedPush support
This commit is contained in:
1 parent
7ebf707540
commit
5a311609d1
9 files changed
+314
-4
No files matched your search
@@ -144,6 +144,11 @@ dependencies {
|
||||
//Camera Permission
|
||||
implementation("com.google.accompanist:accompanist-permissions:0.34.0")
|
||||
|
||||
// Push notifications
|
||||
implementation("org.unifiedpush.android:connector:3.0.10")
|
||||
// Allow using Play Services if available
|
||||
implementation("org.unifiedpush.android:embedded-fcm-distributor:3.0.0")
|
||||
|
||||
//implementation("androidx.compose.material:material-icons-extended:$compose_version")
|
||||
//implementation("androidx.compose.ui:ui-util:$compose_version")
|
||||
|
||||
|
||||
@@ -236,6 +236,16 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
if (mode != NotificationsMode.PERIODIC) {
|
||||
MessagesFetcherWorker.cancelAll()
|
||||
}
|
||||
if (mode == NotificationsMode.INSTANT) {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
SimplexService.initUnifiedPush(this) {
|
||||
// Change notifications mode only if everything is correctly setup
|
||||
chatModel.controller.appPrefs.notificationsMode.set(mode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
chatModel.controller.appPrefs.notificationsMode.set(mode)
|
||||
}
|
||||
SimplexService.showBackgroundServiceNoticeIfNeeded(showOffAlert = false)
|
||||
}
|
||||
|
||||
@@ -245,6 +255,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
NotificationsMode.SERVICE -> CoroutineScope(Dispatchers.Default).launch { platform.androidServiceStart() }
|
||||
NotificationsMode.PERIODIC -> SimplexApp.context.schedulePeriodicWakeUp()
|
||||
NotificationsMode.OFF -> {}
|
||||
NotificationsMode.INSTANT -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,6 +380,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
override fun androidCreateActiveCallState(): Closeable = ActiveCallState()
|
||||
|
||||
override val androidApiLevel: Int get() = Build.VERSION.SDK_INT
|
||||
override val supportsPushNotifications = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import androidx.core.app.ServiceCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.work.*
|
||||
import chat.simplex.app.model.NtfManager
|
||||
import chat.simplex.app.platform.PushManager
|
||||
import chat.simplex.common.AppLock
|
||||
import chat.simplex.common.helpers.requiresIgnoringBattery
|
||||
import chat.simplex.common.model.ChatController
|
||||
@@ -419,6 +420,10 @@ class SimplexService: Service() {
|
||||
|
||||
private fun getPreferences(context: Context): SharedPreferences = context.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE)
|
||||
|
||||
suspend fun initUnifiedPush(scope: CoroutineScope, onSuccess: () -> Unit) {
|
||||
PushManager.initUnifiedPush(androidAppContext, scope, onSuccess)
|
||||
}
|
||||
|
||||
fun showBackgroundServiceNoticeIfNeeded(showOffAlert: Boolean = true) {
|
||||
val appPrefs = ChatController.appPrefs
|
||||
val mode = appPrefs.notificationsMode.get()
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
package chat.simplex.app.platform
|
||||
|
||||
import SectionItemView
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CornerSize
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.getUserServers
|
||||
import chat.simplex.common.platform.Log
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.networkAndServers.showAddServerDialog
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.*
|
||||
import org.unifiedpush.android.connector.UnifiedPush
|
||||
|
||||
/**
|
||||
* Object with functions to interact with push services
|
||||
*/
|
||||
object PushManager {
|
||||
private const val TAG = "PushManager"
|
||||
|
||||
/**
|
||||
* Check if a NTF server is setup and ask user's push distributor
|
||||
*
|
||||
* Show an alert if NTF server isn't define,
|
||||
* If a single distrib is available, use it
|
||||
* If many, ask distributor to use with a dialog
|
||||
* Else alert about missing service
|
||||
*/
|
||||
suspend fun initUnifiedPush(context: Context, scope: CoroutineScope, onSuccess: () -> Unit) {
|
||||
val userServers = getUserServers(null) ?: listOf()
|
||||
if (!userServers.hasNtfServer()) {
|
||||
Log.d(TAG, "User doesn't have any NTF server")
|
||||
showMissingNTFDialog(scope, userServers)
|
||||
// After coming back from the server view, users will have to click on "Instant" again
|
||||
return
|
||||
}
|
||||
val distributors = UnifiedPush.getDistributors(context)
|
||||
when (distributors.size) {
|
||||
0 -> {
|
||||
Log.d(TAG, "No distributor found")
|
||||
showMissingPushServiceDialog()
|
||||
}
|
||||
1 -> {
|
||||
Log.d(TAG, "Found only one distributor installed")
|
||||
UnifiedPush.saveDistributor(context, distributors.first())
|
||||
register(context)
|
||||
onSuccess()
|
||||
}
|
||||
else -> {
|
||||
Log.d(TAG, "Found many distributors installed")
|
||||
showSelectPushServiceIntroDialog {
|
||||
showSelectPushServiceDialog(context, distributors) {
|
||||
UnifiedPush.saveDistributor(context, it)
|
||||
register(context)
|
||||
onSuccess
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun register(context: Context) {
|
||||
// TODO: add VAPID
|
||||
UnifiedPush.register(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any NTF server is recorded
|
||||
*/
|
||||
private fun List<UserOperatorServers>.hasNtfServer(): Boolean {
|
||||
// TODO: check if ntf server has a VAPID key
|
||||
return this.any { it.ntfServers.any() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a dialog to inform about missing NTF server
|
||||
*/
|
||||
private fun showMissingNTFDialog(scope: CoroutineScope, userServers: List<UserOperatorServers>) = AlertManager.shared.showAlert {
|
||||
AlertDialog(
|
||||
onDismissRequest = AlertManager.shared::hideAlert,
|
||||
title = {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_warning),
|
||||
contentDescription = null, // The icon doesn't add any meaning and must not be transcripted with screen readers
|
||||
)
|
||||
Text(
|
||||
stringResource(MR.strings.icon_descr_instant_notifications),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(MR.strings.warning_push_needs_ntf_server))
|
||||
},
|
||||
// Go to user's servers settings
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
AlertManager.shared.hideAlert()
|
||||
showAddServerDialog(scope, userServers)
|
||||
}) { Text(stringResource(MR.strings.smp_servers_add)) }
|
||||
},
|
||||
// Ignore
|
||||
dismissButton = {
|
||||
TextButton(onClick = AlertManager.shared::hideAlert) { Text(stringResource(android.R.string.cancel)) }
|
||||
},
|
||||
shape = RoundedCornerShape(corner = CornerSize(25.dp))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialog to add a server, manually or with a QR code
|
||||
*/
|
||||
private fun showAddServerDialog(scope: CoroutineScope, userServers: List<UserOperatorServers>) {
|
||||
val userServersState = mutableStateOf(userServers)
|
||||
val serverErrors = mutableStateOf(listOf<UserServersError>())
|
||||
showAddServerDialog(scope, userServersState, serverErrors, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a dialog to inform about missing push service
|
||||
*/
|
||||
private fun showMissingPushServiceDialog() = AlertManager.shared.showAlert {
|
||||
AlertDialog(
|
||||
onDismissRequest = AlertManager.shared::hideAlert,
|
||||
title = {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_warning),
|
||||
contentDescription = null, // The icon doesn't add any meaning and must not be transcripted with screen readers
|
||||
)
|
||||
Text(
|
||||
stringResource(MR.strings.icon_descr_instant_notifications),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
append(stringResource(MR.strings.warning_push_needs_push_service))
|
||||
withLink(LinkAnnotation.Url(url = "https://unifiedpush.org")) {
|
||||
withStyle(style = SpanStyle(color = MaterialTheme.colors.primary)) {
|
||||
append("https://unifiedpush.org")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = AlertManager.shared::hideAlert) { Text(stringResource(android.R.string.ok)) }
|
||||
},
|
||||
shape = RoundedCornerShape(corner = CornerSize(25.dp))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Show an intro to explain why they need to select a push service
|
||||
*/
|
||||
private fun showSelectPushServiceIntroDialog(onConfirm: () -> Unit) = AlertManager.shared.showAlert {
|
||||
AlertDialog(
|
||||
onDismissRequest = AlertManager.shared::hideAlert,
|
||||
title = {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_notifications),
|
||||
contentDescription = null, // The icon doesn't add any meaning and must not be transcripted with screen readers
|
||||
)
|
||||
Text(
|
||||
stringResource(MR.strings.icon_descr_instant_notifications),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(MR.strings.select_push_service_intro))
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
AlertManager.shared.hideAlert()
|
||||
onConfirm()
|
||||
}
|
||||
) { Text(stringResource(MR.strings.select_push_service)) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = AlertManager.shared::hideAlert) { Text(stringResource(android.R.string.cancel)) }
|
||||
},
|
||||
shape = RoundedCornerShape(corner = CornerSize(25.dp))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Show a dialog to select push service
|
||||
*
|
||||
* @param distributors List of installed distributors' packageName
|
||||
* @param onSelected run when a distributor is selected, its param is the selected distrib packageName
|
||||
*/
|
||||
private fun showSelectPushServiceDialog(context: Context, distributors: List<String>, onSelected: (String) -> Unit) = AlertManager.shared.showAlert {
|
||||
AlertDialog(
|
||||
onDismissRequest = AlertManager.shared::hideAlert,
|
||||
title = {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_notifications),
|
||||
contentDescription = null, // The icon doesn't add any meaning and must not be transcripted with screen readers
|
||||
)
|
||||
Text(
|
||||
stringResource(MR.strings.select_push_service),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
distributors.map {
|
||||
if (it == context.packageName) it to "Play Services"
|
||||
else it to context.getApplicationName(it)
|
||||
}.forEach {
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
onSelected(it.first)
|
||||
}) {
|
||||
Text(it.second)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = AlertManager.shared::hideAlert) { Text(stringResource(android.R.string.cancel)) }
|
||||
},
|
||||
shape = RoundedCornerShape(corner = CornerSize(25.dp))
|
||||
)
|
||||
}
|
||||
|
||||
private fun Context.getApplicationName(applicationId: String): String {
|
||||
return try {
|
||||
val ai = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
packageManager.getApplicationInfo(
|
||||
applicationId,
|
||||
PackageManager.ApplicationInfoFlags.of(
|
||||
PackageManager.GET_META_DATA.toLong()
|
||||
)
|
||||
)
|
||||
} else {
|
||||
packageManager.getApplicationInfo(applicationId, 0)
|
||||
}
|
||||
packageManager.getApplicationLabel(ai)
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
applicationId
|
||||
} as String
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -9,7 +9,7 @@ val NotificationsMode.requiresIgnoringBatterySinceSdk: Int get() = when(this) {
|
||||
NotificationsMode.OFF -> Int.MAX_VALUE
|
||||
NotificationsMode.PERIODIC -> Build.VERSION_CODES.M
|
||||
NotificationsMode.SERVICE -> Build.VERSION_CODES.S
|
||||
/*INSTANT -> Int.MAX_VALUE - for Firebase notifications */
|
||||
NotificationsMode.INSTANT -> Int.MAX_VALUE
|
||||
}
|
||||
|
||||
val NotificationsMode.requiresIgnoringBattery
|
||||
|
||||
+2
-1
@@ -7323,7 +7323,7 @@ sealed class RemoteCtrlError {
|
||||
}
|
||||
|
||||
enum class NotificationsMode() {
|
||||
OFF, PERIODIC, SERVICE, /*INSTANT - for Firebase notifications */;
|
||||
OFF, PERIODIC, SERVICE, INSTANT;
|
||||
|
||||
companion object {
|
||||
val default: NotificationsMode = SERVICE
|
||||
@@ -7550,6 +7550,7 @@ enum class AppSettingsNotificationMode {
|
||||
companion object {
|
||||
fun from(mode: NotificationsMode): AppSettingsNotificationMode =
|
||||
when (mode) {
|
||||
NotificationsMode.INSTANT -> INSTANT
|
||||
NotificationsMode.SERVICE -> INSTANT
|
||||
NotificationsMode.PERIODIC -> PERIODIC
|
||||
NotificationsMode.OFF -> OFF
|
||||
|
||||
+1
@@ -33,6 +33,7 @@ interface PlatformInterface {
|
||||
@Composable fun androidLockPortraitOrientation() {}
|
||||
suspend fun androidAskToAllowBackgroundCalls(): Boolean = true
|
||||
@Composable fun desktopShowAppUpdateNotice() {}
|
||||
val supportsPushNotifications: Boolean get() = false
|
||||
}
|
||||
/**
|
||||
* Multiplatform project has separate directories per platform + common directory that contains directories per platform + common for all of them.
|
||||
|
||||
+15
-2
@@ -136,6 +136,15 @@ private fun notificationModes(): List<ValueTitleDesc<NotificationsMode>> {
|
||||
AnnotatedString(generalGetString(MR.strings.notifications_mode_service_desc)),
|
||||
)
|
||||
)
|
||||
if (platform.supportsPushNotifications) {
|
||||
res.add(
|
||||
ValueTitleDesc(
|
||||
NotificationsMode.INSTANT,
|
||||
generalGetString(MR.strings.notifications_mode_instant),
|
||||
AnnotatedString(generalGetString(MR.strings.notifications_mode_instant_desc))
|
||||
)
|
||||
)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -167,6 +176,10 @@ fun notificationPreviewModes(): List<ValueTitleDesc<NotificationPreviewMode>> {
|
||||
}
|
||||
|
||||
fun changeNotificationsMode(mode: NotificationsMode, chatModel: ChatModel) {
|
||||
chatModel.controller.appPrefs.notificationsMode.set(mode)
|
||||
platform.androidNotificationsModeChanged(mode)
|
||||
// the preference is updated in androidNotificationsModeChanged for Android
|
||||
if (appPlatform.isAndroid) {
|
||||
platform.androidNotificationsModeChanged(mode)
|
||||
} else {
|
||||
chatModel.controller.appPrefs.notificationsMode.set(mode)
|
||||
}
|
||||
}
|
||||
@@ -260,9 +260,11 @@
|
||||
<string name="notifications_mode_off">Runs when app is open</string>
|
||||
<string name="notifications_mode_periodic">Starts periodically</string>
|
||||
<string name="notifications_mode_service">Always on</string>
|
||||
<string name="notifications_mode_instant">Instant</string>
|
||||
<string name="notifications_mode_off_desc">App can receive notifications only when it\'s running, no background service will be started</string>
|
||||
<string name="notifications_mode_periodic_desc">Checks new messages every 10 minutes for up to 1 minute</string>
|
||||
<string name="notifications_mode_service_desc">Background service is always running – notifications will be shown as soon as the messages are available.</string>
|
||||
<string name="notifications_mode_instant_desc">Receive notifications instantly using an external push service.</string>
|
||||
<string name="notification_preview_mode_message">Message text</string>
|
||||
<string name="notification_preview_mode_contact">Contact name</string>
|
||||
<string name="notification_preview_mode_hidden">Hidden</string>
|
||||
@@ -274,6 +276,10 @@
|
||||
<string name="notification_new_contact_request">New contact request</string>
|
||||
<string name="notification_contact_connected">Connected</string>
|
||||
<string name="error_showing_desktop_notification">Error showing notification, contact developers.</string>
|
||||
<string name="warning_push_needs_ntf_server">You first need to set a NTF server to get push notifications.\n\nIt will send the notifications to your push provider.</string>
|
||||
<string name="warning_push_needs_push_service">You don\'t have any push service installed on your device.\n\nPlease installed one and try again.\n\nFor more information, visit\ </string>
|
||||
<string name="select_push_service">Select Push Service</string>
|
||||
<string name="select_push_service_intro">Multiple push services are installed on your system, please select the service you wish to use.</string>
|
||||
|
||||
<!-- local authentication notice - SimpleXAPI.kt -->
|
||||
<string name="la_notice_title_simplex_lock">SimpleX Lock</string>
|
||||
|
||||
Reference in new issue
Block a user