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) }