diff --git a/app/src/main/java/org/obd/graphs/activity/Receivers.kt b/app/src/main/java/org/obd/graphs/activity/Receivers.kt
index fbd4f8165..c025bf589 100644
--- a/app/src/main/java/org/obd/graphs/activity/Receivers.kt
+++ b/app/src/main/java/org/obd/graphs/activity/Receivers.kt
@@ -38,7 +38,6 @@ import org.obd.graphs.BACKUP_START
import org.obd.graphs.BACKUP_SUCCESSFUL
import org.obd.graphs.GOOGLE_SIGN_IN_GENERAL_FAILURE
import org.obd.graphs.GOOGLE_SIGN_IN_NO_CREDENTIAL_FAILURE
-import org.obd.graphs.LOCATION_IS_DISABLED
import org.obd.graphs.MODULES_LIST_CHANGED_EVENT
import org.obd.graphs.Permissions
import org.obd.graphs.PowerBroadcastReceiver
@@ -135,8 +134,6 @@ internal fun MainActivity.receive(intent: Intent?) {
REQUEST_NOTIFICATION_PERMISSIONS -> Permissions.requestNotificationPermissions(this)
REQUEST_LOCATION_PERMISSIONS -> Permissions.requestLocationPermissions(this)
- LOCATION_IS_DISABLED -> toast(org.obd.graphs.commons.R.string.main_activity_toast_location_disabled)
-
DATA_LOGGER_WIFI_NOT_CONNECTED -> {
getContext()?.startActivity(Intent(Settings.ACTION_WIFI_SETTINGS))
toast(org.obd.graphs.commons.R.string.main_activity_toast_connection_wifi_not_connected)
@@ -378,7 +375,6 @@ internal fun MainActivity.registerReceiver() {
it.addAction(GOOGLE_SIGN_IN_NO_CREDENTIAL_FAILURE)
it.addAction(REQUEST_NOTIFICATION_PERMISSIONS)
- it.addAction(LOCATION_IS_DISABLED)
it.addAction(NAVIGATION_BUTTONS_VISIBILITY_CHANGED)
it.addAction(DATA_LOGGER_SCHEDULED_STOP_EVENT)
it.addAction(PROFILE_NAME_CHANGED_EVENT)
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 25b43d73e..5baf925a2 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -91,7 +91,7 @@
Fetch from ECU only PIDs shown on current screen to boost performance—recommended for slow adapters. Note: this disables high/low freq PIDs polling.
This setting is disabled because individual query strategy is enabled.
- Collect and log GPS coordinates alongside vehicle data
+ Collect and log GPS coordinates alongside vehicle data. Note that Location services must be enabled for data collection to occur.
Enable GPS Logging
Enable GPS Logging?
diff --git a/common/src/main/java/org/obd/graphs/Constants.kt b/common/src/main/java/org/obd/graphs/Constants.kt
index 73c48c7fb..0e1170a10 100644
--- a/common/src/main/java/org/obd/graphs/Constants.kt
+++ b/common/src/main/java/org/obd/graphs/Constants.kt
@@ -51,6 +51,4 @@ const val GOOGLE_SIGN_IN_NO_CREDENTIAL_FAILURE = "gdrive.authorization.no_creden
const val REQUEST_NOTIFICATION_PERMISSIONS = "org.obd.graphs.logger.permissions.request_notifications"
-const val LOCATION_IS_DISABLED = "org.obd.graphs.logger.gps.LOCATION_IS_DISABLED.event"
-
const val DATA_LOGGER_AUTO_CONNECT_EVENT = "data.logger.auto_connect.start"
diff --git a/common/src/main/java/org/obd/graphs/Formatter.kt b/common/src/main/java/org/obd/graphs/Formatter.kt
index 64fd39ed4..6e2e9aa8a 100644
--- a/common/src/main/java/org/obd/graphs/Formatter.kt
+++ b/common/src/main/java/org/obd/graphs/Formatter.kt
@@ -60,7 +60,7 @@ fun ObdMetric.toDouble(): Double = (toNumber() ?: 0).toDouble()
fun ObdMetric.isNumber(): Boolean = this.value != null && this.value is Number
-private fun ObdMetric.toNumber(): Number? =
+fun ObdMetric.toNumber(): Number? =
if (isNumber()) {
value as Number
} else {
diff --git a/common/src/main/java/org/obd/graphs/Notification.kt b/common/src/main/java/org/obd/graphs/Notification.kt
new file mode 100644
index 000000000..063d70369
--- /dev/null
+++ b/common/src/main/java/org/obd/graphs/Notification.kt
@@ -0,0 +1,95 @@
+ /**
+ * Copyright 2019-2026, Tomasz Żebrowski
+ *
+ *
Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License. You may obtain a
+ * copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *
Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.obd.graphs
+
+import android.Manifest
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.Build
+import android.util.Log
+import androidx.core.app.ActivityCompat
+import androidx.core.app.NotificationCompat
+import org.obd.graphs.commons.R
+
+const val NOTIFICATION_CHANNEL_ID = "data_logger_channel_v2"
+const val NOTIFICATION_ID = 12345
+
+private const val NOTIFICATION_TITLE = "Vehicle Telemetry Service"
+
+object Notification {
+ fun sendBasicNotification(
+ context: Context,
+ contentText: String,
+ pendingIntent: PendingIntent? = null,
+ ) {
+ try {
+ val notificationManager = context.getSystemService(NotificationManager::class.java)
+
+ val channelId = NOTIFICATION_CHANNEL_ID
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channelName = NOTIFICATION_TITLE
+ val descriptionText = contentText
+ val importance = NotificationManager.IMPORTANCE_DEFAULT
+
+ val channel =
+ NotificationChannel(channelId, channelName, importance).apply {
+ description = descriptionText
+ }
+
+ notificationManager.createNotificationChannel(channel)
+ }
+
+ val notification = notification(context, contentText, pendingIntent)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (ActivityCompat.checkSelfPermission(
+ context,
+ Manifest.permission.POST_NOTIFICATIONS,
+ ) == PackageManager.PERMISSION_GRANTED
+ ) {
+ notificationManager.notify(NOTIFICATION_ID, notification)
+ }
+ } else {
+ notificationManager.notify(NOTIFICATION_ID, notification)
+ }
+ } catch (e: Throwable) {
+ Log.e("Notification", " Failed to send notification", e)
+ }
+ }
+
+ fun notification(
+ context: Context,
+ contentText: String,
+ pendingIntent: PendingIntent? = null,
+ ): Notification =
+ NotificationCompat
+ .Builder(context, NOTIFICATION_CHANNEL_ID)
+ .setContentTitle(NOTIFICATION_TITLE)
+ .setContentText(contentText)
+ .setSmallIcon(R.drawable.ic_mygiulia_logo)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .apply {
+ pendingIntent?.let { setContentIntent(it) }
+ }.setOngoing(true)
+ .setAutoCancel(true)
+ .build()
+}
diff --git a/datalogger/src/main/res/drawable/ic_mygiulia_logo.png b/common/src/main/res/drawable/ic_mygiulia_logo.png
similarity index 100%
rename from datalogger/src/main/res/drawable/ic_mygiulia_logo.png
rename to common/src/main/res/drawable/ic_mygiulia_logo.png
diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml
index fcef12fe5..1a6274c56 100644
--- a/common/src/main/res/values/strings.xml
+++ b/common/src/main/res/values/strings.xml
@@ -5,7 +5,6 @@
Error occurred during connection. Please try to connect again. If this does not help please check your connection settings.
Connection with the device has been stopped.
Connection to the device has been established.\n Start collecting data from ECU.
- Location is disabled. GPS data collection wont start.
Connecting to the device.
Error occurred during connection.\nPlease check your network settings.
Network connection might be turned off.\n Please check your settings.
diff --git a/datalogger/src/main/java/org/obd/graphs/bl/datalogger/DataLoggerService.kt b/datalogger/src/main/java/org/obd/graphs/bl/datalogger/DataLoggerService.kt
index b95369d98..854d7c871 100644
--- a/datalogger/src/main/java/org/obd/graphs/bl/datalogger/DataLoggerService.kt
+++ b/datalogger/src/main/java/org/obd/graphs/bl/datalogger/DataLoggerService.kt
@@ -28,12 +28,12 @@ import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.util.Log
-import androidx.core.app.NotificationCompat
+import org.obd.graphs.NOTIFICATION_CHANNEL_ID
+import org.obd.graphs.NOTIFICATION_ID
import org.obd.graphs.Permissions
import org.obd.graphs.REQUEST_LOCATION_PERMISSIONS
import org.obd.graphs.REQUEST_NOTIFICATION_PERMISSIONS
import org.obd.graphs.bl.query.Query
-import org.obd.graphs.datalogger.R
import org.obd.graphs.sendBroadcastEvent
private const val ACTION_START = "org.obd.graphs.logger.START"
@@ -43,8 +43,6 @@ private const val UPDATE_QUERY = "org.obd.graphs.logger.UPDATE_QUERY"
private const val QUERY = "org.obd.graphs.logger.QUERY"
private const val EXECUTE_ROUTINE = "org.obd.graphs.logger.EXECUTE_ROUTINE"
-private const val NOTIFICATION_CHANNEL_ID = "data_logger_channel_v2"
-private const val NOTIFICATION_ID = 12345
class DataLoggerService : Service() {
private val binder = LocalBinder()
@@ -199,15 +197,9 @@ class DataLoggerService : Service() {
)
}
- return NotificationCompat
- .Builder(this, NOTIFICATION_CHANNEL_ID)
- .setContentTitle("Vehicle Telemetry Service")
- .setContentText("Logging OBD & GPS data in background...")
- .setSmallIcon(R.drawable.ic_mygiulia_logo)
- .apply {
- contentIntent?.let { setContentIntent(it) }
- }.setOngoing(true)
- .build()
+ return org.obd.graphs.Notification.notification(this,
+ "Logging OBD data in background...",
+ contentIntent)
}
private fun createNotificationChannel() {
diff --git a/datalogger/src/main/java/org/obd/graphs/bl/datalogger/Pid.kt b/datalogger/src/main/java/org/obd/graphs/bl/datalogger/Pid.kt
index 237ae7f71..7087c6098 100644
--- a/datalogger/src/main/java/org/obd/graphs/bl/datalogger/Pid.kt
+++ b/datalogger/src/main/java/org/obd/graphs/bl/datalogger/Pid.kt
@@ -43,10 +43,6 @@ enum class Pid(val id: Long) {
GAS_PID_ID(7007),
OIL_DEGRADATION_PID_ID(7015),
VEHICLE_STATUS_PID_ID(17091),
- WCA_TEMP_PID_ID(17079),
- PRE_IC_AIR_TEMP_PID_ID(7017),
- SPARK_ADVANCE_PID_ID(7026),
- CALCULATED_MAF_PID_ID(7041),
GPS_LOCATION_PID_ID(9977774L)
}
diff --git a/datalogger/src/main/java/org/obd/graphs/bl/gps/GpsMetricsEmitter.kt b/datalogger/src/main/java/org/obd/graphs/bl/gps/GpsMetricsEmitter.kt
index 3d230550c..40fb87fd8 100644
--- a/datalogger/src/main/java/org/obd/graphs/bl/gps/GpsMetricsEmitter.kt
+++ b/datalogger/src/main/java/org/obd/graphs/bl/gps/GpsMetricsEmitter.kt
@@ -28,14 +28,13 @@ import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
import android.util.Log
-import org.obd.graphs.LOCATION_IS_DISABLED
+import org.obd.graphs.Notification
import org.obd.graphs.Permissions
import org.obd.graphs.bl.datalogger.DataLoggerRepository
import org.obd.graphs.bl.datalogger.MetricsProcessor
import org.obd.graphs.bl.datalogger.Pid
import org.obd.graphs.bl.datalogger.dataLoggerSettings
import org.obd.graphs.getContext
-import org.obd.graphs.sendBroadcastEvent
import org.obd.metrics.api.model.ObdMetric
import org.obd.metrics.api.model.Reply
import org.obd.metrics.api.model.ReplyObserver
@@ -82,10 +81,14 @@ internal class GpsMetricsEmitter : MetricsProcessor {
// Guard Clauses
if (!dataLoggerSettings.instance().adapter.gpsCollecetingEnabled) return
- if (!Permissions.hasLocationPermissions(context)) return
+ if (!Permissions.hasLocationPermissions(context)){
+ Notification.sendBasicNotification(context,"Location permissions are not granted." +
+ "GPS data won't be collected.")
+ return
+ }
if (!Permissions.isLocationEnabled(context)) {
- Log.w(TAG, "Location is disabled. Skipping")
- sendBroadcastEvent(LOCATION_IS_DISABLED)
+ Notification.sendBasicNotification(context,"Location is not enabled. " +
+ "GPS data won't be collected.")
return
}
diff --git a/profile/src/main/java/org/obd/graphs/profile/DefaultProfileService.kt b/profile/src/main/java/org/obd/graphs/profile/DefaultProfileService.kt
index ca0a52cd1..939cf213a 100644
--- a/profile/src/main/java/org/obd/graphs/profile/DefaultProfileService.kt
+++ b/profile/src/main/java/org/obd/graphs/profile/DefaultProfileService.kt
@@ -493,7 +493,6 @@ internal class DefaultProfileService :
Prefs.updateString("pref.about.build_time", buildTime).commit()
Prefs.updateString("pref.about.build_version", "$versionCode").commit()
- Prefs.updateBoolean("pref.debug.logging.enabled", false).commit()
}
}
}
diff --git a/screen_renderer/src/main/java/org/obd/graphs/renderer/cache/MetricStringCache.kt b/screen_renderer/src/main/java/org/obd/graphs/renderer/cache/MetricStringCache.kt
index 93036d5ae..aec1169e8 100644
--- a/screen_renderer/src/main/java/org/obd/graphs/renderer/cache/MetricStringCache.kt
+++ b/screen_renderer/src/main/java/org/obd/graphs/renderer/cache/MetricStringCache.kt
@@ -20,6 +20,8 @@ import kotlin.math.abs
internal class MetricStringCache(size: Int = 100) {
private val pids = LongArray(size)
+
+ // Keep DoubleArray to prevent memory boxing and GC stuttering
private val values = DoubleArray(size)
private val strings = Array(size) { null }
private var count = 0
@@ -27,16 +29,29 @@ internal class MetricStringCache(size: Int = 100) {
// Define a small epsilon for safe floating-point comparison
private val EPSILON = 0.0001
- inline fun get(pid: Long, value: Double, formatFallback: () -> String): String {
+ // Signature updated to accept Number?
+ inline fun get(pid: Long, value: Number?, formatFallback: () -> String): String {
+ // Convert any Number (Int, Float, etc.) to Double. Null stays null.
+ val doubleVal = value?.toDouble()
+
for (i in 0 until count) {
if (pids[i] == pid) {
- if (abs(values[i] - value) < EPSILON && strings[i] != null) {
+ val cachedVal = values[i]
+
+ // Safely compare nulls and converted doubles
+ val isMatch = if (doubleVal == null) {
+ cachedVal.isNaN() // We use NaN internally to represent null
+ } else {
+ !cachedVal.isNaN() && abs(cachedVal - doubleVal) < EPSILON
+ }
+
+ if (isMatch && strings[i] != null) {
return strings[i]!!
}
// Cache miss (value changed), update it
val newStr = formatFallback()
- values[i] = value
+ values[i] = doubleVal ?: Double.NaN
strings[i] = newStr
return newStr
}
@@ -45,7 +60,7 @@ internal class MetricStringCache(size: Int = 100) {
// PID not found in cache, add it if there's room
if (count < pids.size) {
pids[count] = pid
- values[count] = value
+ values[count] = doubleVal ?: Double.NaN
val newStr = formatFallback()
strings[count] = newStr
count++
diff --git a/screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeDrawer.kt b/screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeDrawer.kt
index 1f30f9ff0..c37e388b3 100644
--- a/screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeDrawer.kt
+++ b/screen_renderer/src/main/java/org/obd/graphs/renderer/gauge/GaugeDrawer.kt
@@ -41,8 +41,8 @@ import org.obd.graphs.renderer.cache.TextCache
import org.obd.graphs.renderer.api.GaugeProgressBarType
import org.obd.graphs.renderer.api.ScreenSettings
import org.obd.graphs.round
-import org.obd.graphs.toDouble
import org.obd.graphs.toFloat
+import org.obd.graphs.toNumber
import org.obd.graphs.ui.common.COLOR_WHITE
import org.obd.graphs.ui.common.color
import kotlin.math.abs
@@ -496,7 +496,7 @@ internal class GaugeDrawer(
val calculatedFontSize = calculateFontSize(multiplier = area.width() / 22f, fontSize = fontSize) * 3.8f
val value =
- textCache.value.get(metric.pid.id, metric.source.toDouble()) {
+ textCache.value.get(metric.pid.id, metric.source.toNumber()) {
metric.source.format(castToInt = false)
}
diff --git a/screen_renderer/src/main/java/org/obd/graphs/renderer/giulia/GiuliaDrawer.kt b/screen_renderer/src/main/java/org/obd/graphs/renderer/giulia/GiuliaDrawer.kt
index f4ddbdadb..7e797561d 100644
--- a/screen_renderer/src/main/java/org/obd/graphs/renderer/giulia/GiuliaDrawer.kt
+++ b/screen_renderer/src/main/java/org/obd/graphs/renderer/giulia/GiuliaDrawer.kt
@@ -31,10 +31,10 @@ import org.obd.graphs.mapRange
import org.obd.graphs.renderer.AbstractDrawer
import org.obd.graphs.renderer.cache.TextCache
import org.obd.graphs.renderer.api.ScreenSettings
-import org.obd.graphs.toDouble
import org.obd.graphs.toFloat
import kotlin.math.max
import androidx.core.graphics.toColorInt
+import org.obd.graphs.toNumber
private const val FOOTER_SIZE_RATIO = 1.3f
const val MARGIN_END = 30
@@ -365,7 +365,7 @@ internal class GiuliaDrawer(
valuePaint.textSize = textSize
valuePaint.textAlign = Paint.Align.RIGHT
- val value = textCache.value.get(metric.pid.id, metric.source.toDouble()) {
+ val value = textCache.value.get(metric.pid.id, metric.source.toNumber()) {
metric.source.format(castToInt = castToInt)
}