From aa1da4b671a330b2d10d3c77960f427243667034 Mon Sep 17 00:00:00 2001 From: Nazar Sukhovych Date: Tue, 17 Dec 2024 08:35:30 +0100 Subject: [PATCH] REM-881 - Fix crash on location service start --- .../java/com/elementary/tasks/ReminderApp.kt | 4 +- .../data/repository/ReminderRepository.kt | 8 + .../tasks/core/services/GeolocationService.kt | 125 ++++----------- .../tasks/core/services/KoinModule.kt | 8 + .../usecase/CheckLocationReminderUseCase.kt | 148 ++++++++++++++++++ 5 files changed, 198 insertions(+), 95 deletions(-) create mode 100644 app/src/main/java/com/elementary/tasks/core/services/KoinModule.kt create mode 100644 app/src/main/java/com/elementary/tasks/core/services/usecase/CheckLocationReminderUseCase.kt diff --git a/app/src/main/java/com/elementary/tasks/ReminderApp.kt b/app/src/main/java/com/elementary/tasks/ReminderApp.kt index 03d668fd4..13d20a45f 100644 --- a/app/src/main/java/com/elementary/tasks/ReminderApp.kt +++ b/app/src/main/java/com/elementary/tasks/ReminderApp.kt @@ -11,6 +11,7 @@ import com.elementary.tasks.core.data.adapter.adapterModule import com.elementary.tasks.core.data.repository.repositoryModule import com.elementary.tasks.core.os.osModule import com.elementary.tasks.core.services.action.actionModule +import com.elementary.tasks.core.services.servicesModule import com.elementary.tasks.core.utils.Notifier import com.elementary.tasks.core.utils.completableModule import com.elementary.tasks.core.utils.converterModule @@ -87,7 +88,8 @@ class ReminderApp : MultiDexApplication(), KoinComponent { voiceModule, googleTaskModule, workModule, - noteModule + noteModule, + servicesModule ) ) } diff --git a/app/src/main/java/com/elementary/tasks/core/data/repository/ReminderRepository.kt b/app/src/main/java/com/elementary/tasks/core/data/repository/ReminderRepository.kt index 5c718cb3a..91d006e18 100644 --- a/app/src/main/java/com/elementary/tasks/core/data/repository/ReminderRepository.kt +++ b/app/src/main/java/com/elementary/tasks/core/data/repository/ReminderRepository.kt @@ -19,4 +19,12 @@ class ReminderRepository(private val reminderDao: ReminderDao) { UiReminderType(it.type).isGpsType() } } + + suspend fun getActiveGpsTypes(): List { + return reminderDao.getAllTypes(active = true, removed = false, types = Reminder.gpsTypes()) + } + + suspend fun save(reminder: Reminder) { + reminderDao.insert(reminder) + } } diff --git a/app/src/main/java/com/elementary/tasks/core/services/GeolocationService.kt b/app/src/main/java/com/elementary/tasks/core/services/GeolocationService.kt index b95f3d3d6..35b368921 100644 --- a/app/src/main/java/com/elementary/tasks/core/services/GeolocationService.kt +++ b/app/src/main/java/com/elementary/tasks/core/services/GeolocationService.kt @@ -1,39 +1,30 @@ package com.elementary.tasks.core.services +import android.app.ForegroundServiceStartNotAllowedException import android.app.Service import android.content.Intent import android.location.Location +import android.os.Build import android.os.IBinder -import android.text.TextUtils import androidx.core.app.NotificationCompat import com.elementary.tasks.R -import com.elementary.tasks.core.data.dao.ReminderDao -import com.elementary.tasks.core.data.models.Reminder import com.elementary.tasks.core.location.LocationTracker import com.elementary.tasks.core.services.action.reminder.ReminderActionProcessor +import com.elementary.tasks.core.services.usecase.CheckLocationReminderUseCase import com.elementary.tasks.core.utils.Module import com.elementary.tasks.core.utils.Notifier -import com.elementary.tasks.core.utils.datetime.DateTimeManager import com.elementary.tasks.core.utils.launchDefault -import com.elementary.tasks.core.utils.params.Prefs -import com.elementary.tasks.core.utils.ui.DefaultDistanceFormatter import com.elementary.tasks.core.utils.withUIContext +import com.github.naz013.logging.Logger import org.koin.android.ext.android.inject import org.koin.core.parameter.parametersOf import timber.log.Timber -import kotlin.math.roundToInt class GeolocationService : Service() { - private var isNotificationEnabled: Boolean = false - private var stockRadius: Int = 0 - - private val prefs by inject() - private val reminderDao by inject() private val notifier by inject() - private val dateTimeManager by inject() private val reminderActionProcessor by inject() - private val distanceFormatter = DefaultDistanceFormatter(this, prefs.useMetric) + private val checkLocationReminderUseCase by inject() private val locationTracker by inject { parametersOf(locationListener) } private var locationListener: LocationTracker.Listener = object : LocationTracker.Listener { @@ -64,80 +55,15 @@ class GeolocationService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { Timber.d("onStartCommand: ") showDefaultNotification() - isNotificationEnabled = prefs.isDistanceNotificationEnabled - stockRadius = prefs.radius locationTracker.startUpdates() return START_STICKY } - private fun checkReminders(locationA: Location) { + private fun checkReminders(location: Location) { launchDefault { - for (reminder in reminderDao.getAll(active = true, removed = false)) { - if (Reminder.isGpsType(reminder.type)) { - checkDistance(locationA, reminder) - } - } - } - } - - private suspend fun checkDistance(locationA: Location, reminder: Reminder) { - if (!TextUtils.isEmpty(reminder.eventTime)) { - if (dateTimeManager.isCurrent(reminder.eventTime)) { - selectBranch(locationA, reminder) - } - } else { - selectBranch(locationA, reminder) - } - } - - private suspend fun selectBranch(locationA: Location, reminder: Reminder) { - if (reminder.isNotificationShown) return - when { - Reminder.isBase(reminder.type, Reminder.BY_OUT) -> checkOut(locationA, reminder) - else -> checkSimple(locationA, reminder) - } - } - - private suspend fun checkSimple(locationA: Location, reminder: Reminder) { - val place = reminder.places[0] - val locationB = Location("point B") - locationB.latitude = place.latitude - locationB.longitude = place.longitude - val distance = locationA.distanceTo(locationB) - val roundedDistance = distance.roundToInt() - if (roundedDistance <= getRadius(place.radius)) { - showReminder(reminder) - } else { - showNotification(roundedDistance, reminder) - } - } - - private fun getRadius(r: Int): Int { - var radius = r - if (radius == -1) radius = stockRadius - return radius - } - - private suspend fun checkOut(locationA: Location, reminder: Reminder) { - val place = reminder.places[0] - val locationB = Location("point B") - locationB.latitude = place.latitude - locationB.longitude = place.longitude - val distance = locationA.distanceTo(locationB) - val roundedDistance = distance.roundToInt() - if (reminder.isLocked) { - if (roundedDistance > getRadius(place.radius)) { - showReminder(reminder) - } else { - if (isNotificationEnabled) { - showNotification(roundedDistance, reminder) - } - } - } else { - if (roundedDistance < getRadius(place.radius)) { - reminder.isLocked = true - reminderDao.insert(reminder) - } + val result = checkLocationReminderUseCase(location) + result.showDistanceNotifications.forEach { showNotification(it) } + result.showReminderNotifications.forEach { showReminder(it) } } } @@ -145,22 +71,22 @@ class GeolocationService : Service() { reminderActionProcessor.process(id) } - private suspend fun showReminder(reminder: Reminder) { - if (reminder.isNotificationShown) return - reminder.isNotificationShown = true - reminderDao.insert(reminder) - withUIContext { reminderAction(reminder.uuId) } + private suspend fun showReminder( + showReminderNotification: CheckLocationReminderUseCase.ShowReminderNotification + ) { + withUIContext { reminderAction(showReminderNotification.uuId) } } - private fun showNotification(roundedDistance: Int, reminder: Reminder) { - if (!isNotificationEnabled) return + private fun showNotification( + notification: CheckLocationReminderUseCase.ShowDistanceNotification + ) { val builder = NotificationCompat.Builder(applicationContext, Notifier.CHANNEL_SILENT) - builder.setContentTitle(reminder.summary) - builder.setContentText(distanceFormatter.format(roundedDistance)) + builder.setContentTitle(notification.title) + builder.setContentText(notification.text) builder.priority = NotificationCompat.PRIORITY_MIN builder.setSmallIcon(R.drawable.ic_builder_map_my_location) builder.setCategory(NotificationCompat.CATEGORY_NAVIGATION) - notifier.notify(reminder.uniqueId, builder.build()) + notifier.notify(notification.uniqueId, builder.build()) } private fun showDefaultNotification() { @@ -172,7 +98,18 @@ class GeolocationService : Service() { } builder.setContentTitle(getString(R.string.location_tracking_service_running)) builder.setSmallIcon(R.drawable.ic_builder_map_my_location) - startForeground(NOTIFICATION_ID, builder.build()) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + try { + startForeground(NOTIFICATION_ID, builder.build()) + Logger.i("GeolocationService", "Start foreground: success") + } catch (e: ForegroundServiceStartNotAllowedException) { + Logger.i("GeolocationService", "Start foreground: not allowed") + stopSelf() + } + } else { + startForeground(NOTIFICATION_ID, builder.build()) + Logger.i("GeolocationService", "Start foreground: success") + } } companion object { diff --git a/app/src/main/java/com/elementary/tasks/core/services/KoinModule.kt b/app/src/main/java/com/elementary/tasks/core/services/KoinModule.kt new file mode 100644 index 000000000..6e57bfaf8 --- /dev/null +++ b/app/src/main/java/com/elementary/tasks/core/services/KoinModule.kt @@ -0,0 +1,8 @@ +package com.elementary.tasks.core.services + +import com.elementary.tasks.core.services.usecase.CheckLocationReminderUseCase +import org.koin.dsl.module + +val servicesModule = module { + factory { CheckLocationReminderUseCase(get(), get(), get(), get()) } +} diff --git a/app/src/main/java/com/elementary/tasks/core/services/usecase/CheckLocationReminderUseCase.kt b/app/src/main/java/com/elementary/tasks/core/services/usecase/CheckLocationReminderUseCase.kt new file mode 100644 index 000000000..d10fd9ed8 --- /dev/null +++ b/app/src/main/java/com/elementary/tasks/core/services/usecase/CheckLocationReminderUseCase.kt @@ -0,0 +1,148 @@ +package com.elementary.tasks.core.services.usecase + +import android.content.Context +import android.location.Location +import com.elementary.tasks.core.data.models.Reminder +import com.elementary.tasks.core.data.repository.ReminderRepository +import com.elementary.tasks.core.utils.datetime.DateTimeManager +import com.elementary.tasks.core.utils.params.Prefs +import com.elementary.tasks.core.utils.ui.DefaultDistanceFormatter +import kotlin.math.roundToInt + +class CheckLocationReminderUseCase( + context: Context, + private val reminderRepository: ReminderRepository, + private val dateTimeManager: DateTimeManager, + prefs: Prefs +) { + + private val distanceFormatter: DefaultDistanceFormatter = DefaultDistanceFormatter( + context = context, + useMetric = prefs.useMetric + ) + private val stockRadius: Int = prefs.radius + private val isNotificationEnabled: Boolean = prefs.isDistanceNotificationEnabled + + suspend operator fun invoke(location: Location): Result { + val showDistanceNotifications = mutableListOf() + val showReminderNotifications = mutableListOf() + + for (reminder in reminderRepository.getActiveGpsTypes()) { + if (reminder.isNotificationShown) continue + if (shouldCheckDistance(reminder)) { + when { + destinationReached(location, reminder) -> { + reminder.isNotificationShown = true + reminderRepository.save(reminder) + showReminderNotifications.add(ShowReminderNotification(reminder.uuId)) + } + + shouldShowDistanceNotification(reminder) -> { + showDistanceNotifications.add( + ShowDistanceNotification( + uniqueId = reminder.uniqueId, + title = reminder.summary, + text = getDistanceText(location, reminder) + ) + ) + } + + shouldLockReminder(location, reminder) -> { + reminder.isLocked = true + reminderRepository.save(reminder) + } + } + } + } + + return Result(showDistanceNotifications, showReminderNotifications) + } + + private fun getDistanceText(location: Location, reminder: Reminder): String { + return distanceFormatter.format(getDistance(location, reminder)) + } + + private fun shouldLockReminder(location: Location, reminder: Reminder): Boolean { + return if (reminder.isLeavingType()) { + val distance = getDistance(location, reminder) + val place = reminder.places[0] + !reminder.isLocked && distance < getRadius(place.radius) + } else { + false + } + } + + private fun shouldShowDistanceNotification(reminder: Reminder): Boolean { + if (!isNotificationEnabled) return false + return if (reminder.isLeavingType()) { + reminder.isLocked + } else { + true + } + } + + private fun destinationReached(location: Location, reminder: Reminder): Boolean { + val distance = getDistance(location, reminder) + val place = reminder.places[0] + return if (reminder.isLeavingType()) { + reminder.isLocked && distance > getRadius(place.radius) + } else { + distance <= getRadius(place.radius) + } + } + + private fun getRadius(r: Int): Int { + var radius = r + if (radius == -1) radius = stockRadius + return radius + } + + private fun getDistance(location: Location, reminder: Reminder): Int { + return if (reminder.isLeavingType()) { + val place = reminder.places[0] + val loc = Location("point B").apply { + latitude = place.latitude + longitude = place.longitude + } + + val distance = location.distanceTo(loc) + distance.roundToInt() + } else { + val place = reminder.places[0] + val loc = Location("point B").apply { + latitude = place.latitude + longitude = place.longitude + } + + val distance = location.distanceTo(loc) + distance.roundToInt() + } + } + + private fun shouldCheckDistance(reminder: Reminder): Boolean { + return if (reminder.eventTime.isEmpty()) { + true + } else { + dateTimeManager.isCurrent(reminder.eventTime) + } + } + + private fun Reminder.isLeavingType(): Boolean { + return Reminder.isBase(type, Reminder.BY_OUT) + } + + data class Result( + val showDistanceNotifications: List, + val showReminderNotifications: List + ) + + data class ShowReminderNotification( + val uuId: String + ) + + data class ShowDistanceNotification( + val uniqueId: Int, + val title: String, + val text: String + ) +}