Skip to content

Commit

Permalink
#200: Added push notifications if observation cannot be started
Browse files Browse the repository at this point in the history
  • Loading branch information
janoliver20 committed May 24, 2024
1 parent 97f02c5 commit 81550e3
Show file tree
Hide file tree
Showing 13 changed files with 137 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import androidx.core.app.ActivityCompat
import io.github.aakira.napier.Napier
import io.reactivex.rxjava3.disposables.Disposable
import io.redlink.more.app.android.MoreApplication
import io.redlink.more.app.android.R
import io.redlink.more.app.android.extensions.stringResource
import io.redlink.more.app.android.observations.pauseObservation
import io.redlink.more.app.android.observations.showPermissionAlertDialog
import io.redlink.more.app.android.services.sensorsListener.BluetoothStateListener
Expand Down Expand Up @@ -71,20 +73,35 @@ class PolarHeartRateObservation :
message = "HR Recording error: ${error.stackTraceToString()}"
)
pauseObservation(PolarVerityHeartRateType(emptySet()))
updateObservationErrors()
showObservationErrorNotification(
stringResource(R.string.observation_bluetooth_error),
stringResource(R.string.observation_error)
)
})
deviceConnectionListener = listenToDeviceConnection()
true
} catch (exception: Exception) {
Napier.e(tag = "PolarHeartRateObservation::start") { exception.stackTraceToString() }
showObservationErrorNotification(
stringResource(R.string.observation_cannot_start),
stringResource(R.string.observation_error)
)
false
}
} ?: run {
Napier.d(tag = "PolarHeartRateObservation::start") { "No connected devices..." }
showObservationErrorNotification(
stringResource(R.string.observation_cannot_start),
stringResource(R.string.observation_error)
)
false
}
}
Napier.d(tag = "PolarHeartRateObservation::start") { "No connected devices..." }
showObservationErrorNotification(
stringResource(R.string.observation_cannot_start),
stringResource(R.string.observation_error)
)
return false
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,19 @@ class LocalPushNotificationService(private val context: Context) : LocalNotifica
override fun displayNotification(notification: NotificationSchema) {
notification.title?.let { title ->
notification.notificationBody?.let { message ->
val intent = Intent(context, ContentActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
intent.action = NotificationBroadcastReceiver.NOTIFICATION_SET_ON_READ_ACTION
intent.putExtra(MSG_ID, notification.notificationId)

notification.deepLink()?.let {
intent.data = Uri.parse(it)
val intent = Intent(context, ContentActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
action = NotificationBroadcastReceiver.NOTIFICATION_SET_ON_READ_ACTION
putExtra(MSG_ID, notification.notificationId)
notification.deepLink()?.let { data = Uri.parse(it) }
}

val pendingIntent = PendingIntent.getActivity(
context, 0, intent,
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
)

val channelId: String =
val channelId =
notification.channelId ?: context.getString(R.string.default_channel_id)
val notificationBuilder = NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.mipmap.ic_more_logo_hf_v2_round)
Expand All @@ -56,22 +54,36 @@ class LocalPushNotificationService(private val context: Context) : LocalNotifica
.setSound(Settings.System.DEFAULT_NOTIFICATION_URI)
.setContentIntent(pendingIntent)

context.getSystemService(NotificationManager::class.java)
?.let { notificationManager ->
val notificationManager = context.getSystemService(NotificationManager::class.java)
if (notificationManager != null) {
val channel = notificationManager.getNotificationChannel(channelId)
if (channel == null) {
val name = context.getString(R.string.notification_channel_name)
val descriptionText =
context.getString(R.string.notification_channel_description)
val importance = NotificationManager.IMPORTANCE_DEFAULT
val mChannel = NotificationChannel(channelId, name, importance)
mChannel.description = descriptionText
val mChannel = NotificationChannel(channelId, name, importance).apply {
description = descriptionText
}
notificationManager.createNotificationChannel(mChannel)

notificationManager.notify(notification.notificationId.hashCode(), notificationBuilder.build())
}

notificationManager.notify(
notification.notificationId.hashCode(),
notificationBuilder.build()
)
} else {
Napier.e(tag = "NotificationError") { "Notification Manager is null" }
}
} ?: run {
Napier.e(tag = "NotificationError") { "Notification message is null" }
}
} ?: run {
Napier.e(tag = "NotificationError") { "Notification title is null" }
}
}


override fun deleteNotificationFromSystem(notificationId: String) {
context.getSystemService(NotificationManager::class.java)?.cancel(notificationId.hashCode())
}
Expand Down
3 changes: 3 additions & 0 deletions androidApp/src/main/res/values-de/error-strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@
<string name="location_disabled">Ortungsdienste sind deaktiviert</string>
<string name="location_permission_not_granted">Keine Berechtigung für den Zugriff auf die Ortungsdienste gewährt</string>
<string name="accelerometer_sensor_not_available">Kann nicht auf den Beschleunigungssensor zugreifen</string>
<string name="observation_error">Aufzeichnungfehler</string>
<string name="observation_cannot_start">Beobachtung kann nicht gestartet werden! Bitte stellen Sie sicher, dass Bluetooth aktiviert ist und alle notwendigen Geräte verbunden sind!</string>
<string name="observation_bluetooth_error">Fehler beim Fortsetzen der Beobachtung! Es gab ein Verbindungsproblem mit einem Bluetooth-Sensor. Bitte stellen Sie sicher, dass Bluetooth aktiviert ist und alle notwendigen Geräte verbunden sind!</string>
</resources>
3 changes: 3 additions & 0 deletions androidApp/src/main/res/values/error-strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@
<string name="location_disabled">Location Servies are disabled</string>
<string name="location_permission_not_granted">No Permission were granted to access the location services</string>
<string name="accelerometer_sensor_not_available">Cannot access Accelerometer sensor</string>
<string name="observation_error">Observation Error</string>
<string name="observation_cannot_start">Cannot start Observation! Please make sure to enable bluetooth and connect all necessary devices!</string>
<string name="observation_bluetooth_error">Error continuing Observation! There was a connection issue to a bluetooth sensor. Please make sure to enable bluetooth and connect all necessary devices!</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ class PolarVerityHeartRateObservation: Observation_ {
private let deviceManager = BluetoothDeviceManager.shared

private var deviceListener: Ktor_ioCloseable?

private let errorStringTable = "Errors"

init(sensorPermissions: Set<String>) {
super.init(observationType: PolarVerityHeartRateType(sensorPermissions: sensorPermissions))
Expand All @@ -57,12 +59,14 @@ class PolarVerityHeartRateObservation: Observation_ {
}, onError: { [weak self] error in
print(error)
if let self {
showObservationErrorNotification(notificationBody: "Error continuing Observation! There was a connection issue to a bluetooth sensor. Please make sure to enable bluetooth and connect all necessary devices!".localize(withComment: "Error continuing Observation! There was a connection issue to a bluetooth sensor. Please make sure to enable bluetooth and connect all necessary devices!", useTable: errorStringTable), fallbackTitle: "Observation Error".localize(withComment: "Observation Error", useTable: errorStringTable))
self.pauseObservation(self.observationType)
}
})
return true
}
}
showObservationErrorNotification(notificationBody: "Cannot start Observation! Please make sure to enable Bluetooth and connect all necessary devices!".localize(withComment: "Cannot start Observation! Please make sure to enable Bluetooth and connect all necessary devices!", useTable: errorStringTable), fallbackTitle: "Observation Error".localize(withComment: "Observation Error", useTable: errorStringTable))
return false
}

Expand Down
3 changes: 3 additions & 0 deletions iosApp/iosApp/Resources/Strings/de.lproj/Errors.strings
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@
"Location Services not enabled" = "Ortungsdienste sind nicht aktiviert";
"Permission not granted to access location of the device" = "Zugriff auf den Standort des Geräts nicht genehmigt";
"errors" = "Fehler";
"Cannot start Observation! Please make sure to enable Bluetooth and connect all necessary devices!" = "Beobachtung kann nicht gestartet werden! Bitte stellen Sie sicher, dass Bluetooth aktiviert ist und alle notwendigen Geräte verbunden sind!";
"Error continuing Observation! There was a connection issue to a bluetooth sensor. Please make sure to enable bluetooth and connect all necessary devices!" = "Fehler beim Fortsetzen der Beobachtung! Es gab ein Verbindungsproblem mit einem Bluetooth-Sensor. Bitte stellen Sie sicher, dass Bluetooth aktiviert ist und alle notwendigen Geräte verbunden sind!";
"Observation Error" = "Aufzeichnungfehler";
3 changes: 3 additions & 0 deletions iosApp/iosApp/Resources/Strings/en.lproj/Errors.strings
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@
"Location Services not enabled" = "Location Services not enabled";
"Permission not granted to access location of the device" = "Permission not granted to access location of the device";
"errors" = "errors";
"Cannot start Observation! Please make sure to enable bluetooth and connect all necessary devices!" = "Cannot start Observation! Please make sure to enable bluetooth and connect all necessary devices!";
"Error continuing Observation! There was a connection issue to a bluetooth sensor. Please make sure to enable bluetooth and connect all necessary devices!" = "Error continuing Observation! There was a connection issue to a bluetooth sensor. Please make sure to enable bluetooth and connect all necessary devices!";
"Observation Error" = "Observation Error";
42 changes: 22 additions & 20 deletions iosApp/iosApp/Services/LocalPushNotificationService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,35 @@
// Digital Health and Prevention - A research institute
// of the Ludwig Boltzmann Gesellschaft,
// Oesterreichische Vereinigung zur Foerderung
// der wissenschaftlichen Forschung
// Licensed under the Apache 2.0 license with Commons Clause
// der wissenschaftlichen Forschung
// Licensed under the Apache 2.0 license with Commons Clause
// (see https://www.apache.org/licenses/LICENSE-2.0 and
// https://commonsclause.com/).
//

import FirebaseMessaging
import Foundation
import UserNotifications
import shared
import FirebaseMessaging
import UIKit
import UserNotifications

class LocalPushNotifications: LocalNotificationListener {
func clearNotifications() {
UNUserNotificationCenter.current().removeAllDeliveredNotifications()
}

func deleteNotificationFromSystem(notificationId: String) {
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [notificationId])
}

func createNewFCMToken(onCompletion: @escaping (String) -> Void) {
let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.getNotificationSettings { settings in
if settings.authorizationStatus == .authorized {
DispatchQueue.main.async {
if !UIApplication.shared.isRegisteredForRemoteNotifications {
AppDelegate.registerForNotifications()
}
}
}
Messaging.messaging().token { token, error in
if let error {
Expand All @@ -47,36 +47,38 @@ class LocalPushNotifications: LocalNotificationListener {
}
}
}

func deleteFCMToken() {
Messaging.messaging().deleteToken { error in
if let error = error {
print("Erro rdeleting FCM registration token: \(error)")
}
}
}

func displayNotification(notification: NotificationSchema) {
if let title = notification.title, let body = notification.notificationBody {
requestLocalNotification(identifier: notification.notificationId, title: title, subtitle: body)
}
}

private func requestLocalNotification(identifier: String, title: String, subtitle: String, timeInterval: TimeInterval = 0, repeates: Bool = false) {
let content = UNMutableNotificationContent()
content.title = title
content.subtitle = subtitle
content.sound = .default

var trigger: UNTimeIntervalNotificationTrigger? = nil

if timeInterval > 0 {
trigger = UNTimeIntervalNotificationTrigger(timeInterval: timeInterval, repeats: repeates)
}


let adjustedTimeInterval = max(timeInterval, 1)
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: adjustedTimeInterval, repeats: repeates)

let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)

UNUserNotificationCenter.current().add(request)
print("Local Notification requested!")

UNUserNotificationCenter.current().add(request) { error in
if let error = error {
print("Error adding notification: \(error)")
} else {
print("Local Notification requested!")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@ class Shared(
observationDataManager.listenToDatapointCountChanges()
updateTaskStates()
observationManager.activateScheduleUpdate()

}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ import io.realm.kotlin.types.RealmInstant
import io.realm.kotlin.types.RealmObject
import io.realm.kotlin.types.annotations.PrimaryKey
import io.redlink.more.more_app_mutliplatform.extensions.toRealmInstant
import io.redlink.more.more_app_mutliplatform.getPlatform
import io.redlink.more.more_app_mutliplatform.services.network.openapi.model.PushNotification
import io.redlink.more.more_app_mutliplatform.services.notification.NotificationManager
import io.redlink.more.more_app_mutliplatform.util.createUUID
import kotlinx.datetime.Instant

class NotificationSchema : RealmObject {
Expand Down Expand Up @@ -51,6 +53,14 @@ class NotificationSchema : RealmObject {
}

companion object {
fun build(title: String, notificationBody: String): NotificationSchema =
NotificationSchema().apply {
this.notificationId = createUUID()
this.title = title
this.notificationBody = notificationBody
this.priority = if (getPlatform().name.contains("Android")) 2 else 1
}

fun toSchema(
notificationId: String,
channelId: String?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ package io.redlink.more.more_app_mutliplatform.observations

import io.github.aakira.napier.Napier
import io.redlink.more.more_app_mutliplatform.database.repository.ScheduleRepository
import io.redlink.more.more_app_mutliplatform.database.schemas.NotificationSchema
import io.redlink.more.more_app_mutliplatform.database.schemas.ObservationDataSchema
import io.redlink.more.more_app_mutliplatform.models.ScheduleState
import io.redlink.more.more_app_mutliplatform.observations.observationTypes.ObservationType
Expand All @@ -21,7 +22,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant

Expand Down Expand Up @@ -70,14 +74,14 @@ abstract class Observation(val observationType: ObservationType) {
stop {
saveAndSend()
observationShutdown(scheduleId)
updateObservationErrors()
}
} else {
saveAndSend()
}
if (removeNotification) {
handleNotification(scheduleId)
}
updateObservationErrors()
}

fun observationDataManagerAdded() = dataManager != null
Expand Down Expand Up @@ -160,8 +164,8 @@ abstract class Observation(val observationType: ObservationType) {
stop {
saveAndSend()
observationShutdown(scheduleId)
updateObservationErrors()
}
updateObservationErrors()
}

fun stopAndSetState(state: ScheduleState = ScheduleState.ACTIVE, scheduleId: String?) {
Expand All @@ -172,8 +176,8 @@ abstract class Observation(val observationType: ObservationType) {
scheduleId?.let {
observationShutdown(it)
}
updateObservationErrors()
}
updateObservationErrors()
}

fun stopAndSetDone(scheduleId: String) {
Expand All @@ -184,6 +188,7 @@ abstract class Observation(val observationType: ObservationType) {
observationShutdown(scheduleId)
removeDataCount()
handleNotification(scheduleId)
updateObservationErrors()
}
}

Expand All @@ -209,6 +214,34 @@ abstract class Observation(val observationType: ObservationType) {
}
}

protected fun showNotification(title: String, notificationBody: String) {
val notification = NotificationSchema.build(title, notificationBody)
Napier.d(tag = "Observation::showNotification") { "Showing notification: $notification" }
notificationManager?.storeAndDisplayNotification(notification, true)
}

protected fun showObservationErrorNotification(
notificationBody: String,
fallbackTitle: String = "Error"
) {
val schedulesSchemaFlows = scheduleIds.keys.map {
scheduleRepository.scheduleWithId(it)
}
val combinedFlow = combine(schedulesSchemaFlows) { values ->
values.mapNotNull { it }
}

StudyScope.launch {
val scheduleSchemas = combinedFlow.first()
val title =
if (scheduleSchemas.isNotEmpty()) scheduleSchemas.map { it.observationTitle }
.joinToString(", ", limit = 5) else fallbackTitle
withContext(Dispatchers.Main) {
showNotification(title, notificationBody)
}
}
}

protected fun saveAndSend() {
Napier.d(tag = "Observation::finish") { "Saving and sending data for observation of type ${observationType.observationType}." }
dataManager?.saveAndSend()
Expand Down
Loading

0 comments on commit 81550e3

Please sign in to comment.