diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 00000000..a78441d9
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,24 @@
+name: Build
+
+on:
+ push:
+ branches:
+ - '*'
+
+jobs:
+ build:
+ name: Build
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: set up JDK 11
+ uses: actions/setup-java@v1
+ with:
+ java-version: 11
+
+ - name: Make gradlew executable
+ run: chmod +x ./gradlew
+
+ - name: Build with Gradle
+ run: ./gradlew build
diff --git a/README.md b/README.md
index 0348900a..914d24e0 100644
--- a/README.md
+++ b/README.md
@@ -18,15 +18,17 @@ Currently the app is not available in the public Play Store. This is due to stri
Once the app has reached a more finished state I will try to reach out to Polestar directly to potentially make it available through them. Just like ABRP or the Vivaldi browser which also do not comply with Google's restrictions.
## Support the development of the app:
-Any form of support and feedback is very welcome! If you like the app and want to buy me a beer, feel free (but never obliged! This is a hobby for me) to use this link: https://paypal.me/Ixam
+Any form of support and feedback is very welcome! If you like the app and want to buy me a beer, feel free (but never obliged! This is a hobby for me) to use this link: https://paypal.me/Ixam
+Please let me now if you do not want to be listed in the supporters list.
Many thanks to everyone who has supported the development of Car Stats Viewer!
Supporters
- * Ahti Hinnov
- * Robin Hellström
- * Benjamin Stegmann
+ * Ahti Hinnov
+ * Robin Hellström
+ * Benjamin Stegmann
+ * Horst Zimmermann
@@ -46,6 +48,7 @@ Currently the following languages are already available:
- :netherlands: Dutch
- :sweden: Swedish
- :norway: Norwegian
+ - :denmark: Danish
### Rules for contributing code:
@@ -65,6 +68,7 @@ Please also be aware that I will not just include everything. It has to fit into
- Dutch translation: DoubleYouEl
- Swedish translation: Robin Hellström, jschlyter
- Norwegian translation: Oddvarr
+- Danish translation: Emil Blixt Hansen
- FreshDave29
- rdu
@@ -80,6 +84,18 @@ Discussion in the international Polestar forums: [Polestar Forum](https://www.po
## Changelog [DE]:
+### 0.23.0 (24.02.2023)
+- Datenstruktur grundlegend überarbeitet, um die Stabilität und Sklaierbarkeit zu verbessern
+- Dänische Übersetzung hinzugefügt
+- "Über Car Stats Viewer" hinzugefügt, (inkl. grundlegende Überarbeitung der ReadMe mit Hinweisen zur Unterstützung und Mitwirkung)
+- Es können neben dem manuellen Trip mehrere, automatisch zurückgesetzte Trips ausgewählt werden
+- Verschiedene optische Anpassungen an den Diagrammen
+- Einzelne Werte eines Diagramms können per Doppeltipp hervorgehoben werden
+- Möglichkeit zum verschicken von Debug-Logs per SMTP (experimentell!)
+- Stabilisierung des Verhaltens der Ladekurve, wenn die Ausführung der App zwischenzeitlich unterbrochen wird
+- Optimierung der Fahrerablenkung
+- Zahlreiche weitere Bugfixes und Stabilitätsverbesserungen.
+
### 0.22.1 (02.02.2023)
- Falsche Lokalisierung für Norwegisch behoben.
diff --git a/automotive/build.gradle b/automotive/build.gradle
index a80552ec..d2bf23dc 100644
--- a/automotive/build.gradle
+++ b/automotive/build.gradle
@@ -1,6 +1,7 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
+apply plugin: 'com.mikepenz.aboutlibraries.plugin'
android {
compileSdkVersion 33
@@ -8,10 +9,10 @@ android {
defaultConfig {
applicationId "com.ixam97.carStatsViewer"
- minSdkVersion 28
+ minSdkVersion 29
targetSdkVersion 33
- versionCode 38
- versionName "0.22.1"
+ versionCode 68
+ versionName "0.23.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -23,6 +24,26 @@ android {
}
}
+ packagingOptions {
+ pickFirst 'META-INF/LICENSE.md' // picks the Angus Mail license file
+ pickFirst 'META-INF/NOTICE.md' // picks the Angus Mail notice file
+ }
+
+ lintOptions {
+ // Returns whether lint should be quiet (for example, not write informational messages such as paths to report files written)
+ quiet true
+
+ // Whether lint should set the exit code of the process if errors are found
+ abortOnError false
+
+ // Returns whether lint will only check for errors (ignoring warnings)
+ ignoreWarnings true
+
+ // Returns whether lint should check for fatal errors during release builds. Default is true.
+ // If issues with severity "fatal" are found, the release build is aborted.
+ checkReleaseBuilds false
+ }
+
// android.car exists since Android 10 (API level 29) Revision 5.
useLibrary 'android.car'
namespace 'com.ixam97.carStatsViewer'
@@ -36,6 +57,11 @@ dependencies {
implementation 'androidx.fragment:fragment-ktx:1.5.5'
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
+ implementation 'org.eclipse.angus:jakarta.mail:2.0.0'
+ implementation 'com.google.android.gms:play-services-location:17.0.0'
+ implementation 'com.mikepenz:aboutlibraries-core:8.9.4'
+ implementation 'com.airbnb.android:paris:2.0.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
// to fix unresolved references to android.car
def sdkDir = project.android.sdkDirectory.canonicalPath
diff --git a/automotive/src/main/AndroidManifest.xml b/automotive/src/main/AndroidManifest.xml
index cad4ffe8..89965586 100644
--- a/automotive/src/main/AndroidManifest.xml
+++ b/automotive/src/main/AndroidManifest.xml
@@ -2,20 +2,24 @@
+
+
+
-
-
-
+ -->
+ android:required="true"/>
@@ -37,8 +42,9 @@
+ android:exported="false">
+
@@ -47,7 +53,14 @@
android:exported="false">
-
+
+
+
+
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/InAppLogger.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/InAppLogger.kt
index d146da09..fb0ca7ac 100644
--- a/automotive/src/main/java/com/ixam97/carStatsViewer/InAppLogger.kt
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/InAppLogger.kt
@@ -1,21 +1,30 @@
package com.ixam97.carStatsViewer
-import com.ixam97.carStatsViewer.objects.*
import android.app.Activity
+import android.app.AlertDialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
-import android.widget.TextView
+import android.util.Patterns
+import android.view.LayoutInflater
+import android.widget.EditText
import android.widget.Toast
+import com.google.gson.GsonBuilder
+import com.ixam97.carStatsViewer.appPreferences.AppPreferences
+import com.ixam97.carStatsViewer.dataManager.DataManagers
+import com.ixam97.carStatsViewer.mailSender.MailSender
import kotlinx.android.synthetic.main.activity_log.*
+import kotlinx.coroutines.*
+import java.io.File
import java.text.SimpleDateFormat
import java.util.*
-import kotlin.collections.ArrayList
+
object InAppLogger {
var logArray = ArrayList()
+ private val logArrayMaxSize = 10_000
private var lastVHALCallback = ""
private var numVHALCallbacks = 0
@@ -28,10 +37,10 @@ object InAppLogger {
private var numNotificationUpdates = 0
fun log(message: String) {
- var messageTime = SimpleDateFormat("dd.MM.yyyy hh:mm:ss.SSS").format(Date())
+ val messageTime = SimpleDateFormat("dd.MM.yyyy hh:mm:ss.SSS").format(Date())
val logMessage = String.format("%s: %s", messageTime, message)
android.util.Log.d("InAppLogger:", logMessage)
- if (logArray.size > 1_000) logArray.removeAt(0)
+ if (logArray.size > logArrayMaxSize) logArray.removeAt(0)
logArray.add(logMessage)
}
@@ -75,7 +84,7 @@ object InAppLogger {
clipboardString += (logArray[i] + "\n")
}
- clipboardString += getVHALLog() + "\n" + getUILog() + "\n" + getNotificationLog()
+ // clipboardString += getVHALLog() + "\n" + getUILog() + "\n" + getNotificationLog()
val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clipData = ClipData.newPlainText("CarStatsViewerLog", clipboardString)
@@ -89,6 +98,8 @@ class LogActivity : Activity() {
private lateinit var appPreferences: AppPreferences
+ fun CharSequence?.isValidEmail() = !isNullOrEmpty() && Patterns.EMAIL_ADDRESS.matcher(this).matches()
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
InAppLogger.log("LogActivity.onCreate")
@@ -97,32 +108,134 @@ class LogActivity : Activity() {
setContentView(R.layout.activity_log)
- log_switch_deep_log.isChecked = appPreferences.deepLog
-
- var logString = ""
-
- for (i in 0 until InAppLogger.logArray.size) {
- logString += "${InAppLogger.logArray[i]}\n"
- }
+ log_text_target_mail.setText(appPreferences.logTargetAddress)
+ log_text_sender.setText(appPreferences.logUserName)
- logString += "${InAppLogger.getVHALLog()}\n${InAppLogger.getUILog()}\n${InAppLogger.getNotificationLog()}\n"
- logString += "v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})"
+ // log_switch_deep_log.isChecked = appPreferences.deepLog
- val logTextView = TextView(this)
- logTextView.text = logString
- log_log.addView(logTextView)
+ // val logTextView = TextView(this)
+ // logTextView.typeface
+ log_text_view.text = getLogString()
log_button_back.setOnClickListener {
finish()
}
- log_button_copy.setOnClickListener {
- InAppLogger.copyToClipboard(this)
+ log_button_send.setOnClickListener {
+
+ val mailAdr = log_text_target_mail.text.toString()
+ val senderName = log_text_sender.text.toString()
+
+ var senderMail = ""
+
+ appPreferences.logTargetAddress = mailAdr
+ appPreferences.logUserName = senderName
+
+ if (!mailAdr.isValidEmail())
+ Toast.makeText(this, "Invalid mail address!", Toast.LENGTH_SHORT).show()
+ else {
+ CoroutineScope(Dispatchers.Default).launch() {
+ try {
+ val sender = if (appPreferences.smtpAddress != "" && appPreferences.smtpPassword != "" && appPreferences.smtpServer != "") {
+ senderMail = appPreferences.smtpAddress
+ MailSender(appPreferences.smtpAddress, appPreferences.smtpPassword, appPreferences.smtpServer)
+ } else {
+ if (resources.getIdentifier("logmail_email_address", "string", applicationContext.packageName) != 0) {
+ senderMail = getString(resources.getIdentifier("logmail_email_address", "string", applicationContext.packageName))
+ MailSender(
+ senderMail,
+ getString(resources.getIdentifier("logmail_password", "string", applicationContext.packageName)),
+ getString(resources.getIdentifier("logmail_server", "string", applicationContext.packageName)))
+ } else {
+ runOnUiThread {
+ Toast.makeText(this@LogActivity, "No SMTP login", Toast.LENGTH_LONG).show()
+ }
+ null
+ }
+ }
+
+ if (sender == null) return@launch
+
+ enumValues().forEach {
+ try {
+ val dir = File(applicationContext.filesDir, "TripData")
+ if (!dir.exists()) {
+ InAppLogger.log("TRIP DATA: Directory TripData does not exist!")
+
+ } else {
+ val gpxFile = File(dir, "${it.dataManager.printableName}.json")
+ if (!gpxFile.exists() && gpxFile.length() > 0) {
+ InAppLogger.log("TRIP_DATA File ${it.dataManager.printableName}.json does not exist!")
+ }
+ else {
+ sender.addAttachment(gpxFile)
+ }
+ }
+ } catch(e: java.lang.Exception) {
+ InAppLogger.log("Can't attach file ${it.dataManager.printableName}")
+ }
+
+ }
+ sender.sendMail("Debug Log ${Date()} from $senderName", getLogString(), senderMail, mailAdr)
+ runOnUiThread {
+ Toast.makeText(this@LogActivity, "Log and JSON sent to $mailAdr", Toast.LENGTH_LONG).show()
+ }
+ } catch (e: java.lang.Exception) {
+ runOnUiThread {
+ Toast.makeText(this@LogActivity, "Sending E-Mail failed. See log.", Toast.LENGTH_LONG).show()
+ }
+ InAppLogger.log(e.stackTraceToString())
+ }
+ }
+ }
+ }
+
+ log_button_show_json.setOnClickListener {
+ val currentText = log_button_show_json.text
+ when (currentText) {
+ "JSON" -> {
+ log_button_show_json.text = "LOG"
+ val gson = GsonBuilder()
+ .setExclusionStrategies(appPreferences.exclusionStrategy)
+ .setPrettyPrinting()
+ .create()
+ val textValue = "MARKERS: \n" + gson.toJson(DataManagers.CURRENT_TRIP.dataManager.tripData?.markers?: 0) + "\n\nCHARGE CURVE:\n" + gson.toJson(DataManagers.CURRENT_TRIP.dataManager.tripData?.chargePlotLine?: 0)
+ log_text_view.text = textValue
+ }
+ "LOG" -> {
+ log_button_show_json.text = "JSON"
+ log_text_view.text = getLogString()
+ }
+ }
+ }
+
+ log_button_login.setOnClickListener {
+ // copyToClipboard(log_text_view.text.toString())
+ val credentialsDialog = AlertDialog.Builder(this@LogActivity).apply {
+ val layout = LayoutInflater.from(this@LogActivity).inflate(R.layout.dialog_smpt_credentials, null)
+ val smtp_dialog_address = layout.findViewById(R.id.smtp_dialog_address)
+ smtp_dialog_address.setText(appPreferences.smtpAddress)
+ val smtp_dialog_password = layout.findViewById(R.id.smtp_dialog_password)
+ smtp_dialog_password.setText(appPreferences.smtpPassword)
+ val smtp_dialog_server = layout.findViewById(R.id.smtp_dialog_server)
+ smtp_dialog_server.setText(appPreferences.smtpServer)
+
+ setView(layout)
+
+ setPositiveButton("OK") { dialog, _ ->
+ appPreferences.smtpAddress = smtp_dialog_address.text.toString()
+ appPreferences.smtpPassword = smtp_dialog_password.text.toString()
+ appPreferences.smtpServer = smtp_dialog_server.text.toString()
+ }
+ setTitle("SMTP Login")
+ setCancelable(true)
+ create()
+ }
+ credentialsDialog.show()
}
log_button_reload.setOnClickListener {
- finish()
- startActivity(intent)
+ log_text_view.text = getLogString()
}
log_reset_log.setOnClickListener {
@@ -132,9 +245,9 @@ class LogActivity : Activity() {
startActivity(intent)
}
- log_switch_deep_log.setOnClickListener {
- appPreferences.deepLog = log_switch_deep_log.isChecked
- }
+ // log_switch_deep_log.setOnClickListener {
+ // appPreferences.deepLog = log_switch_deep_log.isChecked
+ // }
}
@@ -142,4 +255,25 @@ class LogActivity : Activity() {
super.onDestroy()
InAppLogger.log("LogActivity.onDestroy")
}
+
+ private fun getLogString(): String {
+ var logString = ""
+
+ for (i in 0 until InAppLogger.logArray.size) {
+ logString += "${InAppLogger.logArray[i]}\n"
+ }
+
+ // logString += "${InAppLogger.getVHALLog()}\n${InAppLogger.getUILog()}\n${InAppLogger.getNotificationLog()}\n"
+ logString += "v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})"
+
+ return logString
+ }
+
+ private fun copyToClipboard(clipboardString: String) {
+ val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ val clipData = ClipData.newPlainText("CarStatsViewerLog", clipboardString)
+ clipboardManager.setPrimaryClip(clipData)
+
+ Toast.makeText(this,"Copied to clipboard", Toast.LENGTH_LONG).show()
+ }
}
\ No newline at end of file
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/activities/AboutActivity.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/activities/AboutActivity.kt
new file mode 100644
index 00000000..f4474751
--- /dev/null
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/activities/AboutActivity.kt
@@ -0,0 +1,72 @@
+package com.ixam97.carStatsViewer.activities
+
+import android.app.Activity
+import android.app.AlertDialog
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import com.ixam97.carStatsViewer.BuildConfig
+import com.ixam97.carStatsViewer.R
+import kotlinx.android.synthetic.main.activity_about.*
+
+class AboutActivity : Activity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_about)
+
+ about_button_back.setOnClickListener {
+ finish()
+ }
+
+ about_support_container.setOnClickListener {
+ startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.readme_link))))
+ }
+
+ about_polestar_forum_container.setOnClickListener {
+ startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.polestar_forum_link))))
+ }
+
+ about_polestar_fans_container.setOnClickListener {
+ startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.polestar_fans_link))))
+ }
+
+ about_github_issues_container.setOnClickListener {
+ startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.githug_issues_link))))
+ }
+
+ about_libs.setOnClickListener {
+ startActivity(Intent(this, LibsActivity::class.java))
+ }
+
+ about_version_text.text = "%s (%s)".format(BuildConfig.VERSION_NAME, BuildConfig.APPLICATION_ID)
+
+ var contributors = ""
+ val contributorsArray = resources.getStringArray(R.array.contributors)
+ for ((index, contributor) in contributorsArray.withIndex()) {
+ contributors += contributor
+ if (index < contributorsArray.size -1) contributors += "\n"
+ }
+ about_contributors_text.text = contributors
+
+ about_supporters.setOnClickListener {
+ val supportersDialog = AlertDialog.Builder(this).apply {
+ setPositiveButton(getString(R.string.dialog_close)) { dialog, _ ->
+ dialog.cancel()
+ }
+ setTitle(getString(R.string.about_thank_you))
+ val supportersArray = resources.getStringArray(R.array.supporters)
+ var supporters = getString(R.string.about_supporters_message) + "\n\n"
+ for ((index, supporter) in supportersArray.withIndex()) {
+ supporters += supporter
+ if (index < supportersArray.size - 1) supporters += ", "
+ }
+ setMessage(supporters)
+ setCancelable(true)
+ create()
+ }
+ supportersDialog.show()
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/activities/LibsActivity.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/activities/LibsActivity.kt
new file mode 100644
index 00000000..d3a99289
--- /dev/null
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/activities/LibsActivity.kt
@@ -0,0 +1,88 @@
+package com.ixam97.carStatsViewer.activities
+
+import android.app.Activity
+import android.content.Intent
+import android.graphics.Color
+import android.net.Uri
+import android.os.Bundle
+import android.util.Log
+import android.view.View
+import android.widget.LinearLayout
+import android.widget.TextView
+import android.widget.Toolbar.LayoutParams
+import com.airbnb.paris.extensions.style
+import com.ixam97.carStatsViewer.R
+import com.mikepenz.aboutlibraries.Libs
+import com.mikepenz.aboutlibraries.util.toStringArray
+import kotlinx.android.synthetic.main.activity_libs.*
+
+class LibsActivity: Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_libs)
+
+ libs_button_back.setOnClickListener {
+ finish()
+ }
+
+ val libraries = Libs(this, R.string::class.java.fields.toStringArray()).libraries
+ for ((index, lib) in libraries.withIndex()) {
+ val container = LinearLayout(this).apply {
+ layoutParams = LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
+ orientation = LinearLayout.VERTICAL
+ }
+ val libName = TextView(this).apply {
+ style(R.style.menu_row_top_text)
+ text = lib.libraryName
+ textSize = 30f
+ }
+ container.addView(libName)
+
+ val licenseWebsite = if (lib.licenses != null) lib.licenses!!.first().licenseWebsite else ""
+ val libWebsite =
+ if (lib.authorWebsite.isNotEmpty()) lib.authorWebsite
+ else if (lib.libraryWebsite.isNotEmpty()) lib.libraryWebsite
+ else if (licenseWebsite.isNotEmpty()) licenseWebsite
+ else ""
+
+ val authorInfo = lib.author + if (libWebsite.isNotEmpty()) " | $libWebsite" else ""
+
+ val authorName = TextView(this).apply {
+ style(R.style.menu_row_content_text)
+ textSize = 20f
+ text = authorInfo
+ }
+ if (lib.author.isNotEmpty()) container.addView(authorName)
+
+ if (lib.licenses != null) {
+ for (license in lib.licenses!!) {
+ val libLicense = TextView(this).apply {
+ style(R.style.menu_row_content_text)
+ text = license.licenseName
+ textSize = 20f
+ }
+ if (license.licenseName.isNotEmpty()) container.addView(libLicense)
+ }
+ }
+
+
+ val dividerLine = View(this).apply {
+ style(R.style.menu_divider_style)
+ setBackgroundColor(Color.DKGRAY)
+ }
+ if (libWebsite.isNotEmpty()) {
+ container.setOnClickListener {
+ Log.i("LibLink", libWebsite)
+ startActivity(Intent( Intent.ACTION_VIEW, Uri.parse(libWebsite)))
+ }
+ }
+
+ libs_container.addView(container)
+ if (index < libraries.size - 1) libs_container.addView(dividerLine)
+ else {
+ dividerLine.setBackgroundColor(Color.TRANSPARENT)
+ libs_container.addView(dividerLine)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/activities/MainActivity.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/activities/MainActivity.kt
index 738af79a..002f2aee 100644
--- a/automotive/src/main/java/com/ixam97/carStatsViewer/activities/MainActivity.kt
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/activities/MainActivity.kt
@@ -1,29 +1,36 @@
package com.ixam97.carStatsViewer.activities
import com.ixam97.carStatsViewer.*
-import com.ixam97.carStatsViewer.objects.*
-import com.ixam97.carStatsViewer.services.*
+import com.ixam97.carStatsViewer.dataManager.*
import android.app.Activity
+import android.app.AlertDialog
import android.app.PendingIntent
import android.car.VehicleGear
+import android.car.VehiclePropertyIds
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
-import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
+import android.text.format.DateFormat
import android.view.View
import android.widget.Toast
+import com.ixam97.carStatsViewer.appPreferences.AppPreferences
import com.ixam97.carStatsViewer.plot.enums.*
import com.ixam97.carStatsViewer.plot.graphics.PlotPaint
import com.ixam97.carStatsViewer.views.PlotView
+import com.ixam97.carStatsViewer.dataManager.DataManagers
+import com.ixam97.carStatsViewer.plot.graphics.PlotLinePaint
+import com.ixam97.carStatsViewer.plot.objects.PlotGlobalConfiguration
+import com.ixam97.carStatsViewer.services.LocCollector
+import com.ixam97.carStatsViewer.utils.StringFormatters
+import com.ixam97.carStatsViewer.views.GageView
import kotlinx.android.synthetic.main.activity_main.*
-import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.math.absoluteValue
import kotlin.math.roundToInt
@@ -33,16 +40,22 @@ var emulatorPowerSign = -1
class MainActivity : Activity() {
companion object {
- private const val UI_UPDATE_INTERVAL = 500L
+ private const val UI_UPDATE_INTERVAL = 1000L
const val DISTANCE_TRIP_DIVIDER = 5_000L
+ const val CONSUMPTION_DISTANCE_RESTRICTION = 10_000L
}
/** values and variables */
+ private lateinit var appPreferences: AppPreferences
+ private lateinit var consumptionPlotLinePaint : PlotLinePaint
+ private lateinit var chargePlotLinePaint : PlotLinePaint
private lateinit var timerHandler: Handler
private lateinit var starterIntent: Intent
private lateinit var context: Context
- private lateinit var appPreferences: AppPreferences
+ // private lateinit var appPreferences: AppPreferences
+
+ private var selectedDataManager = DataManagers.CURRENT_TRIP.dataManager
private var updateUi = false
private var lastPlotUpdate: Long = 0L
@@ -68,16 +81,30 @@ class MainActivity : Activity() {
override fun onResume() {
super.onResume()
- InAppLogger.log("MainActivity.onResume")
+ val preferenceDataManager = DataManagers.values()[appPreferences.mainViewTrip].dataManager
+ if (selectedDataManager != preferenceDataManager) {
+ finish()
+ startActivity(intent)
+ }
- if (appPreferences.consumptionUnit) {
- DataHolder.consumptionPlotLine.Configuration.Unit = "Wh/km"
- DataHolder.consumptionPlotLine.Configuration.LabelFormat = PlotLineLabelFormat.NUMBER
- DataHolder.consumptionPlotLine.Configuration.Divider = 1f
- } else {
- DataHolder.consumptionPlotLine.Configuration.Unit = "kWh/100km"
- DataHolder.consumptionPlotLine.Configuration.LabelFormat = PlotLineLabelFormat.FLOAT
- DataHolder.consumptionPlotLine.Configuration.Divider = 10f
+ // InAppLogger.log("MainActivity.onResume")
+
+ PlotGlobalConfiguration.updateDistanceUnit(appPreferences.distanceUnit)
+
+ main_consumption_plot.dimensionRestriction = appPreferences.distanceUnit.asUnit(CONSUMPTION_DISTANCE_RESTRICTION)
+
+ for (manager in DataManagers.values()) {
+ manager.dataManager.consumptionPlotLine.Configuration.UnitFactor = appPreferences.distanceUnit.toFactor()
+
+ if (appPreferences.consumptionUnit) {
+ manager.dataManager.consumptionPlotLine.Configuration.Unit = "Wh/%s".format(appPreferences.distanceUnit.unit())
+ manager.dataManager.consumptionPlotLine.Configuration.LabelFormat = PlotLineLabelFormat.NUMBER
+ manager.dataManager.consumptionPlotLine.Configuration.Divider = appPreferences.distanceUnit.toFactor() * 1f
+ } else {
+ manager.dataManager.consumptionPlotLine.Configuration.Unit = "kWh/100%s".format(appPreferences.distanceUnit.unit())
+ manager.dataManager.consumptionPlotLine.Configuration.LabelFormat = PlotLineLabelFormat.FLOAT
+ manager.dataManager.consumptionPlotLine.Configuration.Divider = appPreferences.distanceUnit.toFactor() * 10f
+ }
}
main_power_gage.maxValue = if (appPreferences.consumptionPlotSingleMotor) 170f else 300f
@@ -89,27 +116,30 @@ class MainActivity : Activity() {
main_charge_gage.barVisibility = appPreferences.chargePlotVisibleGages
main_checkbox_speed.isChecked = appPreferences.plotSpeed
- main_consumption_plot.secondaryDimension = when (appPreferences.plotSpeed) {
+ main_consumption_plot.secondaryDimension = when (appPreferences.secondaryConsumptionDimension) {
+ 1 -> {
+ main_button_secondary_dimension.text = getString(R.string.main_secondary_axis, getString(R.string.main_speed))
+ PlotSecondaryDimension.SPEED
+ }
+ 2 -> {
+ main_button_secondary_dimension.text = getString(R.string.main_secondary_axis, getString(R.string.main_SoC))
+ PlotSecondaryDimension.STATE_OF_CHARGE
+ }
+ else -> {
+ main_button_secondary_dimension.text = getString(R.string.main_secondary_axis, "-")
+ null
+ }
+ }
+
+ /* when (appPreferences.plotSpeed) {
true -> PlotSecondaryDimension.SPEED
else -> null
}
- main_button_speed.text = when {
+ main_button_secondary_dimension.text = when {
main_consumption_plot.secondaryDimension != null -> getString(R.string.main_button_hide_speed)
else -> getString(R.string.main_button_show_speed)
- }
-
- DataHolder.consumptionPlotLine.plotPaint = PlotPaint.byColor(getColor(R.color.primary_plot_color), PlotView.textSize)
- DataHolder.consumptionPlotLine.secondaryPlotPaint = when {
- appPreferences.consumptionPlotSecondaryColor -> PlotPaint.byColor(getColor(R.color.secondary_plot_color_alt), PlotView.textSize)
- else -> PlotPaint.byColor(getColor(R.color.secondary_plot_color), PlotView.textSize)
- }
-
- DataHolder.chargePlotLine.plotPaint = PlotPaint.byColor(getColor(R.color.charge_plot_color), PlotView.textSize)
- DataHolder.chargePlotLine.secondaryPlotPaint = when {
- appPreferences.chargePlotSecondaryColor -> PlotPaint.byColor(getColor(R.color.secondary_plot_color_alt), PlotView.textSize)
- else -> PlotPaint.byColor(getColor(R.color.secondary_plot_color), PlotView.textSize)
- }
+ }*/
main_consumption_plot.invalidate()
@@ -118,16 +148,47 @@ class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
-
- InAppLogger.log("MainActivity.onCreate")
+ // InAppLogger.log("MainActivity.onCreate")
context = applicationContext
+ val displayMetrics = context.resources.displayMetrics
+ InAppLogger.log("Display size: ${displayMetrics.widthPixels/displayMetrics.density}x${displayMetrics.heightPixels/displayMetrics.density}")
+
+ PlotView.textSize = resources.getDimension(R.dimen.reduced_font_size)
+ PlotView.xMargin = resources.getDimension(R.dimen.plot_x_margin).toInt()
+ PlotView.yMargin = resources.getDimension(R.dimen.plot_y_margin).toInt()
+ GageView.valueTextSize = resources.getDimension(R.dimen.gage_value_text_size)
+ GageView.descriptionTextSize = resources.getDimension(R.dimen.gage_desc_text_size)
appPreferences = AppPreferences(context)
+ StringFormatters.initFormatter(
+ DateFormat.getDateFormat(context),
+ DateFormat.getTimeFormat(context),
+ appPreferences.consumptionUnit,
+ appPreferences.distanceUnit
+ )
+
+ consumptionPlotLinePaint = PlotLinePaint(
+ PlotPaint.byColor(getColor(R.color.primary_plot_color), PlotView.textSize),
+ PlotPaint.byColor(getColor(R.color.secondary_plot_color), PlotView.textSize),
+ PlotPaint.byColor(getColor(R.color.secondary_plot_color_alt), PlotView.textSize)
+ ) { appPreferences.consumptionPlotSecondaryColor }
+
+ chargePlotLinePaint = PlotLinePaint(
+ PlotPaint.byColor(getColor(R.color.charge_plot_color), PlotView.textSize),
+ PlotPaint.byColor(getColor(R.color.secondary_plot_color), PlotView.textSize),
+ PlotPaint.byColor(getColor(R.color.secondary_plot_color_alt), PlotView.textSize)
+ ) { appPreferences.chargePlotSecondaryColor }
+
+ selectedDataManager = DataManagers.values()[appPreferences.mainViewTrip].dataManager
+ InAppLogger.log("selected Trip: ${selectedDataManager.printableName}")
+
+ PlotGlobalConfiguration.updateDistanceUnit(appPreferences.distanceUnit)
startForegroundService(Intent(this, DataCollector::class.java))
+ startService(Intent(this, LocCollector::class.java))
- mainActivityPendingIntent = PendingIntent.getActivity(
+ DataCollector.mainActivityPendingIntent = PendingIntent.getActivity(
this, 0, intent, PendingIntent.FLAG_IMMUTABLE
)
@@ -150,154 +211,122 @@ class MainActivity : Activity() {
main_button_performance.colorFilter = PorterDuffColorFilter(getColor(R.color.disabled_tint), PorterDuff.Mode.SRC_IN)
enableUiUpdates()
+
+ if (appPreferences.versionString != BuildConfig.VERSION_NAME) {
+ val changelogDialog = AlertDialog.Builder(this).apply {
+ setPositiveButton(getString(R.string.dialog_close)) { dialog, _ ->
+ dialog.cancel()
+ }
+ setTitle(getString(R.string.main_changelog_dialog_title, BuildConfig.VERSION_NAME))
+ val changesArray = resources.getStringArray(R.array.changes_0_23)
+ var changelog = ""
+ for ((index, change) in changesArray.withIndex()) {
+ changelog += "• $change"
+ if (index < changesArray.size - 1) changelog += "\n\n"
+ }
+ setMessage(changelog)
+ setCancelable(true)
+ create()
+ }
+ changelogDialog.show()
+ appPreferences.versionString = BuildConfig.VERSION_NAME
+ }
}
override fun onDestroy() {
super.onDestroy()
disableUiUpdates()
unregisterReceiver(broadcastReceiver)
- InAppLogger.log("MainActivity.onDestroy")
+ // InAppLogger.log("MainActivity.onDestroy")
}
override fun onPause() {
super.onPause()
disableUiUpdates()
- InAppLogger.log("MainActivity.onPause")
+ // InAppLogger.log("MainActivity.onPause")
}
/** Private functions */
private fun updateActivity() {
- InAppLogger.logUIUpdate()
- /** Use data from DataHolder to Update MainActivity text */
-
- setUiVisibilities()
- updateGages()
-
- main_gage_avg_consumption_text_view.text = " Ø %s".format(getAvgConsumptionString())
- main_gage_distance_text_view.text = " %s".format(getTraveledDistanceString())
- main_gage_used_power_text_view.text = " %s".format(getUsedEnergyString())
- main_gage_time_text_view.text = " %s".format(getElapsedTimeString(DataHolder.travelTimeMillis))
-
- main_gage_charged_energy_text_view.text = " %s".format(getChargedEnergyString())
- main_gage_charge_time_text_view.text = " %s".format(getElapsedTimeString(DataHolder.chargeTimeMillis))
- }
-
- private fun getCurrentSpeedString(): String {
- return "${(DataHolder.currentSpeedSmooth * 3.6).toInt()} km/h"
- }
-
- private fun getAvgSpeedString(): String {
- return " %d km/h".format(
- ((DataHolder.traveledDistance / 1000) / (DataHolder.travelTimeMillis.toFloat() / 3_600_000)).toInt())
- }
-
- private fun getCurrentPowerString(detailed : Boolean = true): String {
- val rawPower = DataHolder.currentPowerSmooth / 1_000_000
-
- return when {
- !detailed && rawPower.absoluteValue >= 10 -> "%d kW".format(Locale.ENGLISH, rawPower.roundToInt())
- else -> "%.1f kW".format(Locale.ENGLISH, rawPower)
- }
- }
-
- private fun getUsedEnergyString(): String {
- if (!appPreferences.consumptionUnit) {
- return "%.1f kWh".format(
- Locale.ENGLISH,
- DataHolder.usedEnergy / 1000)
- }
- return "${DataHolder.usedEnergy.toInt()} Wh"
- }
+ // InAppLogger.logUIUpdate()
+ /** Use data from DataManager to Update MainActivity text */
- private fun getChargedEnergyString(): String {
- if (!appPreferences.consumptionUnit) {
- return "%.1f kWh".format(
- Locale.ENGLISH,
- DataHolder.chargedEnergy / 1000)
+ DataCollector.gagePowerValue = selectedDataManager.currentPower
+ DataCollector.gageConsValue = ((selectedDataManager.currentPower / 1000)/(selectedDataManager.currentSpeed * 3.6f)).let {
+ if (it.isFinite()) it
+ else 0F
}
- return "${DataHolder.chargedEnergy.toInt()} Wh"
- }
- private fun getInstConsumptionString(): String {
- if (DataHolder.currentSpeed <= 0) {
- return "-/-"
- }
- if (!appPreferences.consumptionUnit) {
- return "%.1f kWh/100km".format(
- Locale.ENGLISH,
- ((DataHolder.currentPowerSmooth / 1000) / (DataHolder.currentSpeedSmooth * 3.6))/10)
- }
- return "${((DataHolder.currentPowerSmooth / 1000) / (DataHolder.currentSpeedSmooth * 3.6)).toInt()} Wh/km"
- }
-
- private fun getAvgConsumptionString(): String {
- val unitString = if (appPreferences.consumptionUnit) "Wh/km" else "kWh/100km"
- if (DataHolder.traveledDistance <= 0) {
- return "-/- $unitString"
- }
- if (!appPreferences.consumptionUnit) {
- return "%.1f %s".format(
- Locale.ENGLISH,
- (DataHolder.usedEnergy /(DataHolder.traveledDistance /1000))/10,
- unitString)
- }
- return "${(DataHolder.usedEnergy /(DataHolder.traveledDistance /1000)).toInt()} $unitString"
- }
+ setUiVisibilities()
+ updateGages()
- private fun getTraveledDistanceString(): String {
- return "%.1f km".format(Locale.ENGLISH, DataHolder.traveledDistance / 1000)
+ main_gage_avg_consumption_text_view.text = " Ø %s".format(StringFormatters.getAvgConsumptionString(selectedDataManager.usedEnergy, selectedDataManager.traveledDistance))
+ main_gage_distance_text_view.text = " %s".format(StringFormatters.getTraveledDistanceString(selectedDataManager.traveledDistance))
+ main_gage_used_power_text_view.text = " %s".format(StringFormatters.getEnergyString(selectedDataManager.usedEnergy))
+ main_gage_time_text_view.text = " %s".format(StringFormatters.getElapsedTimeString(selectedDataManager.travelTime))
+ main_gage_charged_energy_text_view.text = " %s".format(StringFormatters.getEnergyString(DataManagers.CURRENT_TRIP.dataManager.chargedEnergy))
+ main_gage_charge_time_text_view.text = " %s".format(StringFormatters.getElapsedTimeString(DataManagers.CURRENT_TRIP.dataManager.chargeTime))
+ main_gage_remaining_range_text_view.text = " -/- %s".format(appPreferences.distanceUnit.unit())
+ main_gage_ambient_temperature_text_view.text = " %s".format( StringFormatters.getTemperatureString(selectedDataManager.ambientTemperature))
}
private fun setUiVisibilities() {
- if (main_button_dismiss_charge_plot.isEnabled == DataHolder.chargePortConnected)
- main_button_dismiss_charge_plot.isEnabled = !DataHolder.chargePortConnected
+ if (main_button_dismiss_charge_plot.isEnabled == selectedDataManager.chargePortConnected)
+ main_button_dismiss_charge_plot.isEnabled = !selectedDataManager.chargePortConnected
- if (main_charge_layout.visibility == View.GONE && DataHolder.chargePortConnected) {
+ if (main_charge_layout.visibility == View.GONE && selectedDataManager.chargePortConnected) {
main_consumption_layout.visibility = View.GONE
main_charge_layout.visibility = View.VISIBLE
}
}
private fun updateGages() {
- if ((DataHolder.currentPowerSmooth / 1_000_000).absoluteValue > 10 && true) { // Add Setting!
- val newValue = (DataHolder.currentPowerSmooth / 1_000_000).toInt()
- main_power_gage.setValue(newValue)
+ if ((selectedDataManager.currentPower / 1_000_000).absoluteValue >= 100 && true) { // Add Setting!
+ main_power_gage.setValue((DataCollector.gagePowerValue / 1_000_000).toInt())
+ main_charge_gage.setValue((-DataCollector.gagePowerValue / 1_000_000).toInt())
} else {
- main_power_gage.setValue(DataHolder.currentPowerSmooth / 1_000_000)
+ main_power_gage.setValue(DataCollector.gagePowerValue / 1_000_000)
+ main_charge_gage.setValue(-DataCollector.gagePowerValue / 1_000_000)
}
- main_charge_gage.setValue(-DataHolder.currentPowerSmooth / 1_000_000)
- main_SoC_gage.setValue((100f / DataHolder.maxBatteryCapacity * DataHolder.currentBatteryCapacity).roundToInt())
+ main_SoC_gage.setValue(selectedDataManager.stateOfCharge)
- var consumptionValue: Float? = null
+ val nullValue: Float? = null
if (appPreferences.consumptionUnit) {
- main_consumption_gage.gageUnit = "Wh/km"
- main_consumption_gage.minValue = -300f
- main_consumption_gage.maxValue = 600f
- if (DataHolder.currentSpeed * 3.6 > 3) {
- main_consumption_gage.setValue(((DataHolder.currentPowerSmooth / 1000) / (DataHolder.currentSpeedSmooth * 3.6)).toInt())
+ main_consumption_gage.gageUnit = "Wh/%s".format(appPreferences.distanceUnit.unit())
+ main_consumption_gage.minValue = appPreferences.distanceUnit.asUnit(-300f)
+ main_consumption_gage.maxValue = appPreferences.distanceUnit.asUnit(600f)
+
+ if (selectedDataManager.currentSpeed * 3.6 > 3) {
+ main_consumption_gage.setValue(appPreferences.distanceUnit.asUnit(DataCollector.gageConsValue).roundToInt())
} else {
- main_consumption_gage.setValue(consumptionValue)
+ main_consumption_gage.setValue(nullValue)
}
} else {
- main_consumption_gage.gageUnit = "kWh/100km"
- main_consumption_gage.minValue = -30f
- main_consumption_gage.maxValue = 60f
- if (DataHolder.currentSpeed * 3.6 > 3) {
- main_consumption_gage.setValue(((DataHolder.currentPowerSmooth / 1000) / (DataHolder.currentSpeedSmooth * 3.6f))/10)
+ main_consumption_gage.gageUnit = "kWh/100%s".format(appPreferences.distanceUnit.unit())
+ main_consumption_gage.minValue = appPreferences.distanceUnit.asUnit(-30f)
+ main_consumption_gage.maxValue = appPreferences.distanceUnit.asUnit(60f)
+
+ if (selectedDataManager.currentSpeed * 3.6 > 3) {
+ main_consumption_gage.setValue(appPreferences.distanceUnit.asUnit(DataCollector.gageConsValue) / 10)
} else {
- main_consumption_gage.setValue(consumptionValue)
+ main_consumption_gage.setValue(nullValue)
}
}
+ main_consumption_gage.invalidate()
+ main_power_gage.invalidate()
+ main_charge_gage.invalidate()
+ main_SoC_gage.invalidate()
+ // Log.i("Gages", "updated")
}
private fun updatePlots(){
// if (appPreferences.plotDistance == 3) main_consumption_plot.dimensionRestriction = dimensionRestrictionById(appPreferences.plotDistance)
- main_charge_plot.dimensionRestriction = TimeUnit.MINUTES.toNanos((TimeUnit.MILLISECONDS.toMinutes(DataHolder.chargeTimeMillis) / 5) + 1) * 5 + TimeUnit.MILLISECONDS.toNanos(1)
+ main_charge_plot.dimensionRestriction = TimeUnit.MINUTES.toMillis((TimeUnit.MILLISECONDS.toMinutes(selectedDataManager.chargeTime) / 5) + 1) * 5 + 1
if (SystemClock.elapsedRealtime() - lastPlotUpdate > 1_000L) {
if (main_consumption_layout.visibility == View.VISIBLE) {
@@ -312,81 +341,52 @@ class MainActivity : Activity() {
}
}
- private fun getElapsedTimeString(elapsedTime: Long): String {
- return String.format("%02d:%02d:%02d",
- TimeUnit.MILLISECONDS.toHours(elapsedTime),
- TimeUnit.MILLISECONDS.toMinutes(elapsedTime) % TimeUnit.HOURS.toMinutes(1),
- TimeUnit.MILLISECONDS.toSeconds(elapsedTime) % TimeUnit.MINUTES.toSeconds(1))
- }
-
- // private fun dimensionRestrictionById(id : Int) : Long {
- // return when (id) {
- // 1 -> DISTANCE_1
- // 2 -> DISTANCE_2
- // 3 -> ((DataHolder.traveledDistance / DISTANCE_TRIP_DIVIDER).toInt() + 1) * DISTANCE_TRIP_DIVIDER + 1
- // else -> DISTANCE_2
- // }
- // }
-
private fun resetStats() {
finish()
startActivity(intent)
- InAppLogger.log("MainActivity.resetStats")
- DataHolder.resetDataHolder()
+ // InAppLogger.log("MainActivity.resetStats")
+ selectedDataManager.reset()
sendBroadcast(Intent(getString(R.string.save_trip_data_broadcast)))
}
private fun setupDefaultUi() {
- var plotDistanceId = when (appPreferences.plotDistance) {
- 1 -> main_radio_10.id
- 2 -> main_radio_25.id
- 3 -> main_radio_50.id
- else -> main_radio_10.id
- }
-
- main_radio_group_distance.check(plotDistanceId)
+ PlotGlobalConfiguration.updateDistanceUnit(appPreferences.distanceUnit)
main_consumption_plot.reset()
- main_consumption_plot.addPlotLine(DataHolder.consumptionPlotLine)
- // main_consumption_plot.setPlotMarkers(DataHolder.plotMarkers)
- // main_consumption_plot.visibleMarkerTypes.add(PlotMarkerType.CHARGE)
- // main_consumption_plot.visibleMarkerTypes.add(PlotMarkerType.PARK)
-
- main_button_speed.text = when {
- main_consumption_plot.secondaryDimension != null -> getString(R.string.main_button_hide_speed)
- else -> getString(R.string.main_button_show_speed)
- }
+ main_consumption_plot.addPlotLine(selectedDataManager.consumptionPlotLine, consumptionPlotLinePaint)
- DataHolder.consumptionPlotLine.secondaryPlotPaint = when {
- appPreferences.consumptionPlotSecondaryColor -> PlotPaint.byColor(getColor(R.color.secondary_plot_color_alt), PlotView.textSize)
- else -> PlotPaint.byColor(getColor(R.color.secondary_plot_color), PlotView.textSize)
- }
+ main_button_secondary_dimension.text = "Toggle secondary dimension"
main_consumption_plot.dimension = PlotDimension.DISTANCE
- main_consumption_plot.dimensionRestriction = 10_001L
+ main_consumption_plot.dimensionRestriction = appPreferences.distanceUnit.asUnit(CONSUMPTION_DISTANCE_RESTRICTION)
main_consumption_plot.dimensionSmoothingPercentage = 0.02f
- //main_consumption_plot.dimensionShiftTouchInterval = 1_000L
- //main_consumption_plot.dimensionRestrictionTouchInterval = 5_000L
- main_consumption_plot.secondaryDimension = when (appPreferences.plotSpeed) {
- true -> PlotSecondaryDimension.SPEED
- else -> null
+ main_consumption_plot.sessionGapRendering = PlotSessionGapRendering.JOIN
+ main_consumption_plot.secondaryDimension = when (appPreferences.secondaryConsumptionDimension) {
+ 1 -> {
+ main_button_secondary_dimension.text =
+ getString(R.string.main_secondary_axis, getString(R.string.main_speed))
+ PlotSecondaryDimension.SPEED
+ }
+ 2 -> {
+ main_button_secondary_dimension.text =
+ getString(R.string.main_secondary_axis, getString(R.string.main_SoC))
+ PlotSecondaryDimension.STATE_OF_CHARGE
+ }
+ else -> {
+ main_button_secondary_dimension.text = getString(R.string.main_secondary_axis, "-")
+ null
+ }
}
main_consumption_plot.invalidate()
main_charge_plot.reset()
- main_charge_plot.addPlotLine(DataHolder.chargePlotLine)
-
- DataHolder.chargePlotLine.secondaryPlotPaint = when {
- appPreferences.chargePlotSecondaryColor -> PlotPaint.byColor(getColor(R.color.secondary_plot_color_alt), PlotView.textSize)
- else -> PlotPaint.byColor(getColor(R.color.secondary_plot_color), PlotView.textSize)
- }
+ main_charge_plot.addPlotLine(DataManagers.CURRENT_TRIP.dataManager.chargePlotLine, chargePlotLinePaint)
main_charge_plot.dimension = PlotDimension.TIME
main_charge_plot.dimensionRestriction = null
- main_charge_plot.dimensionSmoothingPercentage = 0.01f
- main_charge_plot.sessionGapRendering = PlotSessionGapRendering.NONE
+ main_charge_plot.sessionGapRendering = PlotSessionGapRendering.GAP
main_charge_plot.secondaryDimension = PlotSecondaryDimension.STATE_OF_CHARGE
main_charge_plot.invalidate()
@@ -423,22 +423,26 @@ class MainActivity : Activity() {
main_title.setOnClickListener {
if (emulatorMode) {
- DataHolder.currentGear = when (DataHolder.currentGear) {
- VehicleGear.GEAR_PARK -> {
- Toast.makeText(this, "Drive", Toast.LENGTH_SHORT).show()
- // DataHolder.resetTimestamp += (System.nanoTime() - DataHolder.parkTimestamp)
- DataHolder.plotMarkers.endMarker(SystemClock.elapsedRealtimeNanos())
- VehicleGear.GEAR_DRIVE
+ val gearSimulationIntent = Intent(getString(R.string.VHAL_emulator_broadcast)).apply {
+ putExtra(
+ EmulatorIntentExtras.PROPERTY_ID,
+ VehiclePropertyIds.GEAR_SELECTION
+ )
+ putExtra(EmulatorIntentExtras.TYPE, EmulatorIntentExtras.TYPE_INT)
+ if (selectedDataManager.currentGear == VehicleGear.GEAR_PARK) {
+ putExtra(
+ EmulatorIntentExtras.VALUE,
+ VehicleGear.GEAR_DRIVE
+ )
+ Toast.makeText(applicationContext, "Drive", Toast.LENGTH_SHORT).show()
}
- else -> {
- Toast.makeText(this, "Park", Toast.LENGTH_SHORT).show()
- // DataHolder.parkTimestamp = System.nanoTime()
- DataHolder.plotMarkers.addMarker(PlotMarkerType.PARK, SystemClock.elapsedRealtimeNanos())
- VehicleGear.GEAR_PARK
+ else {
+ putExtra(EmulatorIntentExtras.VALUE, VehicleGear.GEAR_PARK)
+ Toast.makeText(applicationContext, "Park", Toast.LENGTH_SHORT).show()
}
}
- sendBroadcast(Intent(getString(R.string.gear_update_broadcast)))
- if (DataHolder.currentGear == VehicleGear.GEAR_PARK) sendBroadcast(Intent(getString(R.string.save_trip_data_broadcast)))
+ sendBroadcast(gearSimulationIntent)
+ sendBroadcast(Intent(getString(R.string.save_trip_data_broadcast)))
}
}
main_title_icon.setOnClickListener {
@@ -448,102 +452,70 @@ class MainActivity : Activity() {
Toast.makeText(this, "Power sign: ${if(emulatorPowerSign<0) "-" else "+"}", Toast.LENGTH_SHORT).show()
}
}
-/*
- main_button_reset.setOnClickListener {
-
- val builder = AlertDialog.Builder(this@MainActivity)
- builder.setTitle(getString(R.string.dialog_reset_title))
- .setMessage(getString(R.string.dialog_reset_message))
- .setCancelable(true)
- .setPositiveButton(getString(R.string.dialog_confirm)) { dialog, id ->
- resetStats()
- }
- .setNegativeButton(getString(R.string.dialog_dismiss)) { dialog, id ->
- // Dismiss the dialog
- InAppLogger.log("Dismiss reset but refresh MainActivity")
- finish()
- startActivity(intent)
- }
- val alert = builder.create()
- alert.show()
- }
-
- */
main_button_settings.setOnClickListener {
startActivity(Intent(this, SettingsActivity::class.java))
}
- /** cycle through consumption plot distances when tapping the plot */
- // main_consumption_plot.setOnClickListener {
- // main_consumption_plot.dimensionRestriction = 5_000L
- // }
-
- /* main_radio_group_distance.setOnCheckedChangeListener { group, checkedId ->
- var id = when (checkedId) {
- main_radio_10.id -> 1
- main_radio_25.id -> 2
- main_radio_50.id -> 3
- else -> 1
- }
-
- main_consumption_plot.dimensionRestriction = dimensionRestrictionById(id)
-
- appPreferences.plotDistance = id
- }
-
- main_checkbox_speed.setOnClickListener {
- if (main_checkbox_speed.isChecked && !DataHolder.speedPlotLine.Visible) {
- DataHolder.speedPlotLine.Visible = true
- } else if (!main_checkbox_speed.isChecked && DataHolder.speedPlotLine.Visible) {
- DataHolder.speedPlotLine.Visible = false
- }
-
- appPreferences.plotSpeed = main_checkbox_speed.isChecked
- main_consumption_plot.invalidate()
- } */
-
- main_button_speed.setOnClickListener {
- main_consumption_plot.secondaryDimension = when (main_consumption_plot.secondaryDimension) {
- null -> PlotSecondaryDimension.SPEED
- else -> null
- }
-
- appPreferences.plotSpeed = main_consumption_plot.secondaryDimension != null
- main_consumption_plot.invalidate()
- main_button_speed.text = when {
- main_consumption_plot.secondaryDimension != null -> getString(R.string.main_button_hide_speed)
- else -> getString(R.string.main_button_show_speed)
+ main_button_secondary_dimension.setOnClickListener {
+ var currentIndex = appPreferences.secondaryConsumptionDimension
+ currentIndex++
+ if (currentIndex > 2) currentIndex = 0
+ appPreferences.secondaryConsumptionDimension = currentIndex
+ main_consumption_plot.secondaryDimension = when (appPreferences.secondaryConsumptionDimension) {
+ 1 -> {
+ main_button_secondary_dimension.text =
+ getString(R.string.main_secondary_axis, getString(R.string.main_speed))
+ PlotSecondaryDimension.SPEED
+ }
+ 2 -> {
+ main_button_secondary_dimension.text =
+ getString(R.string.main_secondary_axis, getString(R.string.main_SoC))
+ PlotSecondaryDimension.STATE_OF_CHARGE
+ }
+ else -> {
+ main_button_secondary_dimension.text = getString(R.string.main_secondary_axis, "-")
+ null
+ }
}
+ //main_consumption_plot.secondaryDimension = when (main_consumption_plot.secondaryDimension) {
+ // null -> PlotSecondaryDimension.SPEED
+ // else -> null
+ //}
+ //
+ //appPreferences.plotSpeed = main_consumption_plot.secondaryDimension != null
+ //main_consumption_plot.invalidate()
+ //main_button_speed.text = when {
+ // main_consumption_plot.secondaryDimension != null -> getString(R.string.main_button_hide_speed)
+ // else -> getString(R.string.main_button_show_speed)
+ //}
}
main_button_summary.setOnClickListener {
- // sendBroadcast(Intent(getString(R.string.save_trip_data_broadcast)))
- startActivity(Intent(this, SummaryActivity::class.java))
+ val summaryIntent = Intent(this, SummaryActivity::class.java)
+ // summaryIntent.putExtra("dataManager", DataManagers.values().indexOf(DataManagers.CURRENT_TRIP))
+ summaryIntent.putExtra("dataManager", appPreferences.mainViewTrip)
+ startActivity(summaryIntent)
}
main_button_summary_charge.setOnClickListener {
- // sendBroadcast(Intent(getString(R.string.save_trip_data_broadcast)))
- startActivity(Intent(this, SummaryActivity::class.java))
+ val summaryIntent = Intent(this, SummaryActivity::class.java)
+ // summaryIntent.putExtra("dataManager", DataManagers.values().indexOf(DataManagers.CURRENT_TRIP))
+ summaryIntent.putExtra("dataManager", appPreferences.mainViewTrip)
+ startActivity(summaryIntent)
}
main_button_dismiss_charge_plot.setOnClickListener {
main_charge_layout.visibility = View.GONE
main_consumption_layout.visibility = View.VISIBLE
main_consumption_plot.invalidate()
- DataHolder.chargedEnergy = 0f
- DataHolder.chargeTimeMillis = 0L
- }
-/*
- main_button_reset_charge_plot.setOnClickListener {
- //main_charge_plot.reset()
- DataHolder.chargePlotLine.reset()
+ // DataManager.chargedEnergy = 0f
+ // DataManager.chargeTime = 0L
}
-*/
}
private fun enableUiUpdates() {
- InAppLogger.log("Enabling UI updates")
+ // InAppLogger.log("Enabling UI updates")
updateUi = true
if (this::timerHandler.isInitialized) {
timerHandler.removeCallbacks(updateActivityTask)
@@ -553,11 +525,11 @@ class MainActivity : Activity() {
}
private fun disableUiUpdates() {
- InAppLogger.log("Disabling UI Updates")
+ // InAppLogger.log("Disabling UI Updates")
updateUi = false
if (this::timerHandler.isInitialized) {
timerHandler.removeCallbacks(updateActivityTask)
}
}
-}
\ No newline at end of file
+}
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/activities/PermissionsActivity.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/activities/PermissionsActivity.kt
index f9c3e160..8c64714a 100644
--- a/automotive/src/main/java/com/ixam97/carStatsViewer/activities/PermissionsActivity.kt
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/activities/PermissionsActivity.kt
@@ -12,7 +12,13 @@ import kotlin.system.exitProcess
class PermissionsActivity: Activity() {
companion object {
- private val PERMISSIONS = arrayOf(Car.PERMISSION_ENERGY, Car.PERMISSION_SPEED)
+ private val PERMISSIONS = arrayOf(
+ Car.PERMISSION_ENERGY,
+ Car.PERMISSION_SPEED,
+ android.Manifest.permission.ACCESS_FINE_LOCATION,
+ android.Manifest.permission.ACCESS_COARSE_LOCATION
+ //android.Manifest.permission.ACCESS_BACKGROUND_LOCATION
+ )
}
override fun onCreate(savedInstanceState: Bundle?) {
@@ -30,8 +36,8 @@ class PermissionsActivity: Activity() {
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
InAppLogger.log("onRequestPermissionResult")
- if(grantResults[0] == PackageManager.PERMISSION_GRANTED && grantResults[1] == PackageManager.PERMISSION_GRANTED)
- {
+
+ if (unGrantedPermissions().isEmpty()) {
finish()
startActivity(Intent(applicationContext, MainActivity::class.java))
} else {
@@ -53,13 +59,41 @@ class PermissionsActivity: Activity() {
private fun checkPermissions(): Boolean {
InAppLogger.log("Checking permissions...")
- if (checkSelfPermission(Car.PERMISSION_ENERGY) != PackageManager.PERMISSION_GRANTED || checkSelfPermission(
- Car.PERMISSION_SPEED) != PackageManager.PERMISSION_GRANTED) {
+ val unGrantedPermissions = unGrantedPermissions()
+ if (unGrantedPermissions.isNotEmpty()) {
InAppLogger.log("Requesting missing Permissions...")
- requestPermissions(PERMISSIONS, 0)
+ requestPermissions(unGrantedPermissions.toTypedArray(), 0)
return false
}
InAppLogger.log("Permissions already granted.")
return true
}
-}
\ No newline at end of file
+
+ private fun unGrantedPermissions(): List {
+ return PERMISSIONS.filter { checkSelfPermission(it) != PackageManager.PERMISSION_GRANTED }
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/activities/SettingsActivity.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/activities/SettingsActivity.kt
index 2da8c900..60f0c4a7 100644
--- a/automotive/src/main/java/com/ixam97/carStatsViewer/activities/SettingsActivity.kt
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/activities/SettingsActivity.kt
@@ -3,10 +3,8 @@ package com.ixam97.carStatsViewer.activities
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import com.ixam97.carStatsViewer.*
-import com.ixam97.carStatsViewer.objects.*
import android.app.Activity
import android.app.AlertDialog
-import android.car.VehicleGear
import android.content.BroadcastReceiver
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_settings.*
@@ -14,17 +12,25 @@ import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.IntentFilter
+import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
+import android.util.TypedValue
import android.view.View
-import android.widget.SeekBar
-import android.widget.SeekBar.OnSeekBarChangeListener
import android.widget.TextView
+import android.widget.Toast
+import androidx.core.graphics.drawable.toDrawable
+import androidx.core.graphics.toColor
+import androidx.core.view.children
+import androidx.core.view.get
+import com.ixam97.carStatsViewer.appPreferences.AppPreferences
+import com.ixam97.carStatsViewer.dataManager.ChargeCurve
import com.ixam97.carStatsViewer.plot.enums.*
-import com.ixam97.carStatsViewer.plot.objects.PlotLine
-import com.ixam97.carStatsViewer.plot.objects.PlotLineConfiguration
import com.ixam97.carStatsViewer.plot.graphics.PlotPaint
-import com.ixam97.carStatsViewer.plot.objects.PlotRange
+import com.ixam97.carStatsViewer.dataManager.DataCollector
+import com.ixam97.carStatsViewer.dataManager.DataManagers
+import com.ixam97.carStatsViewer.enums.DistanceUnitEnum
+import com.ixam97.carStatsViewer.plot.objects.PlotGlobalConfiguration
import com.ixam97.carStatsViewer.views.PlotView
import kotlin.system.exitProcess
@@ -32,11 +38,12 @@ class SettingsActivity : Activity() {
private lateinit var context : Context
private lateinit var appPreferences: AppPreferences
+ private lateinit var primaryColor: Color
private val broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
- getString(R.string.gear_update_broadcast) -> setEnableByGear(DataHolder.currentGear)
+ getString(R.string.distraction_optimization_broadcast) -> setDistractionOptimization(appPreferences.doDistractionOptimization)
}
}
}
@@ -44,43 +51,80 @@ class SettingsActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- InAppLogger.log("SettingsActivity.onCreate")
+ // InAppLogger.log("SettingsActivity.onCreate")
context = applicationContext
appPreferences = AppPreferences(context)
setContentView(R.layout.activity_settings)
+ val typedValue = TypedValue()
+ applicationContext.theme.resolveAttribute(android.R.attr.colorControlActivated, typedValue, true)
+ primaryColor = typedValue.data.toColor()
+
setupSettingsMaster()
setupSettingsConsumptionPlot()
setupSettingsChargePlot()
- registerReceiver(broadcastReceiver, IntentFilter(getString(R.string.gear_update_broadcast)))
+ registerReceiver(broadcastReceiver, IntentFilter(getString(R.string.distraction_optimization_broadcast)))
- setEnableByGear(DataHolder.currentGear)
+ setDistractionOptimization(appPreferences.doDistractionOptimization)
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(broadcastReceiver)
- InAppLogger.log("SettingsActivity.onDestroy")
+ // InAppLogger.log("SettingsActivity.onDestroy")
}
- private fun setEnableByGear(gear: Int) {
- if (gear != VehicleGear.GEAR_PARK) {
+ private fun setDistractionOptimization(doOptimize: Boolean) {
+ if (doOptimize) {
if (settings_charge_plot_layout.visibility == View.VISIBLE) animateTransition(settings_charge_plot_layout, settings_master_layout)
if (settings_consumption_plot_layout.visibility == View.VISIBLE) animateTransition(settings_consumption_plot_layout, settings_master_layout)
setMenuRowIsEnabled(false, settings_charge_plot)
setMenuRowIsEnabled(false, settings_consumption_plot)
+ setMenuRowIsEnabled(false, settings_about)
} else {
setMenuRowIsEnabled(true, settings_charge_plot)
setMenuRowIsEnabled(true, settings_consumption_plot)
+ setMenuRowIsEnabled(true, settings_about)
}
}
private fun setupSettingsMaster() {
settings_switch_notifications.isChecked = appPreferences.notifications
settings_switch_consumption_unit.isChecked = appPreferences.consumptionUnit
+ settings_switch_distance_unit.isChecked = appPreferences.distanceUnit == DistanceUnitEnum.MILES
+
+ settings_selected_trip_bar[appPreferences.mainViewTrip].background = primaryColor.toDrawable()
+
+ settings_main_trip_name_text.text = getString(resources.getIdentifier(
+ DataManagers.values()[appPreferences.mainViewTrip].dataManager.printableName, "string", packageName
+ ))
+
+ settings_button_main_trip_prev.setOnClickListener {
+ var tripIndex = appPreferences.mainViewTrip
+ tripIndex--
+ if (tripIndex < 0) tripIndex = 3
+ appPreferences.mainViewTrip = tripIndex
+ settings_main_trip_name_text.text = getString(resources.getIdentifier(
+ DataManagers.values()[appPreferences.mainViewTrip].dataManager.printableName, "string", packageName
+ ))
+ for (view in settings_selected_trip_bar.children) view.background = getColor(R.color.disable_background).toDrawable()
+ settings_selected_trip_bar[tripIndex].background = primaryColor.toDrawable()
+ }
+
+ settings_button_main_trip_next.setOnClickListener {
+ var tripIndex = appPreferences.mainViewTrip
+ tripIndex++
+ if (tripIndex > 3) tripIndex = 0
+ appPreferences.mainViewTrip = tripIndex
+ settings_main_trip_name_text.text = getString(resources.getIdentifier(
+ DataManagers.values()[appPreferences.mainViewTrip].dataManager.printableName, "string", packageName
+ ))
+ for (view in settings_selected_trip_bar.children) view.background = getColor(R.color.disable_background).toDrawable()
+ settings_selected_trip_bar[tripIndex].background = primaryColor.toDrawable()
+ }
settings_version_text.text = "Car Stats Viewer Version %s (%s)".format(BuildConfig.VERSION_NAME, BuildConfig.APPLICATION_ID)
@@ -115,6 +159,15 @@ class SettingsActivity : Activity() {
settings_switch_consumption_unit.setOnClickListener {
appPreferences.consumptionUnit = settings_switch_consumption_unit.isChecked
}
+
+ if (emulatorMode) settings_switch_distance_unit.visibility = View.VISIBLE
+ settings_switch_distance_unit.setOnClickListener {
+ appPreferences.distanceUnit = when (settings_switch_distance_unit.isChecked) {
+ true -> DistanceUnitEnum.MILES
+ else -> DistanceUnitEnum.KM
+ }
+ PlotGlobalConfiguration.updateDistanceUnit(appPreferences.distanceUnit)
+ }
settings_consumption_plot.setOnClickListener {
gotoConsumptionPlot()
@@ -127,6 +180,10 @@ class SettingsActivity : Activity() {
settings_version_text.setOnClickListener {
startActivity(Intent(this, LogActivity::class.java))
}
+
+ settings_about.setOnClickListener {
+ startActivity(Intent(this, AboutActivity::class.java))
+ }
}
private fun setupSettingsConsumptionPlot() {
@@ -142,10 +199,6 @@ class SettingsActivity : Activity() {
settings_consumption_plot_switch_secondary_color.setOnClickListener {
appPreferences.consumptionPlotSecondaryColor = settings_consumption_plot_switch_secondary_color.isChecked
- DataHolder.consumptionPlotLine.secondaryPlotPaint = when {
- appPreferences.consumptionPlotSecondaryColor -> PlotPaint.byColor(getColor(R.color.secondary_plot_color_alt), PlotView.textSize)
- else -> PlotPaint.byColor(getColor(R.color.secondary_plot_color), PlotView.textSize)
- }
}
settings_consumption_plot_speed_switch.setOnClickListener {
@@ -165,6 +218,7 @@ class SettingsActivity : Activity() {
settings_charge_plot_switch_secondary_color.isChecked = appPreferences.chargePlotSecondaryColor
settings_charge_plot_switch_state_of_charge_dimension.isChecked = appPreferences.chargePlotDimension == PlotDimension.STATE_OF_CHARGE
+ settings_charge_plot_switch_visible_gages.isChecked = appPreferences.chargePlotVisibleGages
settings_charge_plot_button_back.setOnClickListener {
gotoMaster(settings_charge_plot_layout)
@@ -172,11 +226,27 @@ class SettingsActivity : Activity() {
settings_charge_plot_switch_secondary_color.setOnClickListener {
appPreferences.chargePlotSecondaryColor = settings_charge_plot_switch_secondary_color.isChecked
- val plotPaint = when {
- appPreferences.chargePlotSecondaryColor -> PlotPaint.byColor(getColor(R.color.secondary_plot_color_alt), PlotView.textSize)
- else -> PlotPaint.byColor(getColor(R.color.secondary_plot_color), PlotView.textSize)
+ }
+
+ settings_charge_plot_switch_visible_gages.setOnClickListener {
+ appPreferences.chargePlotVisibleGages = settings_charge_plot_switch_visible_gages.isChecked
+ }
+
+ settings_save_charge_curve.setOnClickListener {
+ if (DataCollector.CurrentTripDataManager.chargePlotLine.getDataPoints(PlotDimension.TIME).isNotEmpty()) {
+ DataCollector.CurrentTripDataManager.chargeCurves.add(
+ ChargeCurve(
+ DataCollector.CurrentTripDataManager.chargePlotLine.getDataPoints(PlotDimension.TIME),
+ DataCollector.CurrentTripDataManager.chargeTime,
+ DataCollector.CurrentTripDataManager.chargedEnergy,
+ DataCollector.CurrentTripDataManager.ambientTemperature
+ )
+ )
+ sendBroadcast(Intent(getString(R.string.save_trip_data_broadcast)))
+ Toast.makeText(this, "Saved charge curve", Toast.LENGTH_SHORT).show()
+ } else {
+ Toast.makeText(this, "No charge curve to save", Toast.LENGTH_SHORT).show()
}
- DataHolder.chargePlotLine.secondaryPlotPaint = plotPaint
}
/*
settings_charge_plot_switch_state_of_charge_dimension.setOnClickListener {
@@ -226,8 +296,6 @@ class SettingsActivity : Activity() {
}
})
}
-
- InAppLogger.log(resources.getInteger(android.R.integer.config_shortAnimTime).toString())
}
private fun setMenuRowIsEnabled(enabled: Boolean, view: View) {
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/activities/SummaryActivity.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/activities/SummaryActivity.kt
index e8ee671d..17b9ec41 100644
--- a/automotive/src/main/java/com/ixam97/carStatsViewer/activities/SummaryActivity.kt
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/activities/SummaryActivity.kt
@@ -4,23 +4,29 @@ import android.app.Activity
import android.app.AlertDialog
import android.car.VehicleGear
import android.content.*
+import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.os.Bundle
-import android.text.format.DateFormat
import android.util.TypedValue
import android.view.View
import android.widget.SeekBar
+import androidx.core.graphics.drawable.toDrawable
+import androidx.core.graphics.toColor
+import androidx.core.view.children
+import androidx.core.view.get
+import com.ixam97.carStatsViewer.InAppLogger
import com.ixam97.carStatsViewer.R
-import com.ixam97.carStatsViewer.objects.AppPreferences
-import com.ixam97.carStatsViewer.objects.DataHolder
-import com.ixam97.carStatsViewer.objects.TripData
+import com.ixam97.carStatsViewer.appPreferences.AppPreferences
+import com.ixam97.carStatsViewer.dataManager.*
import com.ixam97.carStatsViewer.plot.enums.*
import com.ixam97.carStatsViewer.plot.graphics.*
import com.ixam97.carStatsViewer.plot.objects.*
+import com.ixam97.carStatsViewer.utils.StringFormatters
import com.ixam97.carStatsViewer.views.PlotView
+import kotlinx.android.synthetic.main.activity_main.*
+import kotlinx.android.synthetic.main.activity_settings.*
import kotlinx.android.synthetic.main.activity_summary.*
-import java.util.*
import java.util.concurrent.TimeUnit
class SummaryActivity: Activity() {
@@ -30,19 +36,23 @@ class SummaryActivity: Activity() {
private var chargePlotLine = PlotLine(
PlotLineConfiguration(
PlotRange(0f, 20f, 0f, 160f, 20f),
- PlotLineLabelFormat.NUMBER,
+ PlotLineLabelFormat.FLOAT,
PlotLabelPosition.LEFT,
PlotHighlightMethod.AVG_BY_TIME,
"kW"
)
)
+ private lateinit var consumptionPlotLine: PlotLine
+
private lateinit var tripData: TripData
private lateinit var appPreferences: AppPreferences
+ private lateinit var consumptionPlotLinePaint : PlotLinePaint
+ private lateinit var chargePlotLinePaint : PlotLinePaint
private lateinit var disabledTint: PorterDuffColorFilter
- private lateinit var enabledTint: PorterDuffColorFilter
+ private lateinit var enabledTint: android.graphics.ColorFilter
private val seekBarChangeListener = object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
@@ -54,9 +64,10 @@ class SummaryActivity: Activity() {
private val broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
+ InAppLogger.log(intent.action?: "")
when (intent.action) {
- getString(R.string.gear_update_broadcast) -> {
- updateDistractionOptimization(true)
+ getString(R.string.distraction_optimization_broadcast) -> {
+ updateDistractionOptimization()
}
else -> {}
}
@@ -69,16 +80,80 @@ class SummaryActivity: Activity() {
appPreferences = AppPreferences(applicationContext)
- val tripDataFileName = intent.getStringExtra("FileName").toString()
- tripData = if (tripDataFileName != "null") DataHolder.getTripData(tripDataFileName)
- else DataHolder.getTripData()
+ consumptionPlotLinePaint = PlotLinePaint(
+ PlotPaint.byColor(getColor(R.color.primary_plot_color), PlotView.textSize),
+ PlotPaint.byColor(getColor(R.color.secondary_plot_color), PlotView.textSize),
+ PlotPaint.byColor(getColor(R.color.secondary_plot_color_alt), PlotView.textSize)
+ ) { appPreferences.consumptionPlotSecondaryColor }
+
+ chargePlotLinePaint = PlotLinePaint(
+ PlotPaint.byColor(getColor(R.color.charge_plot_color), PlotView.textSize),
+ PlotPaint.byColor(getColor(R.color.secondary_plot_color), PlotView.textSize),
+ PlotPaint.byColor(getColor(R.color.secondary_plot_color_alt), PlotView.textSize)
+ ) { appPreferences.chargePlotSecondaryColor }
+
+
+ //val tripDataFileName = intent.getStringExtra("FileName").toString()
+ // tripData = if (tripDataFileName != "null") DataManager.getTripData(tripDataFileName)
+ // else DataManager.getTripData()
+
+ val dataManager :DataManager = if (intent.hasExtra("dataManager")) {
+ DataManagers.values()[intent.getIntExtra("dataManager", 0)].dataManager
+ } else DataManagers.values()[appPreferences.mainViewTrip].dataManager
+
+ if (appPreferences.mainViewTrip != 0) {
+ summary_button_reset.isEnabled = false
+ summary_button_reset.setColorFilter(getColor(R.color.disabled_tint))
+ }
+
+ tripData = dataManager.tripData!!
+ consumptionPlotLine = dataManager.consumptionPlotLine
val typedValue = TypedValue()
applicationContext.theme.resolveAttribute(android.R.attr.colorControlActivated, typedValue, true)
primaryColor = typedValue.data
+ summary_selected_trip_bar[appPreferences.mainViewTrip].background = primaryColor!!.toColor().toDrawable()
+
+ summary_button_trip_prev.setOnClickListener {
+ var tripIndex = appPreferences.mainViewTrip
+ tripIndex--
+ if (tripIndex < 0) tripIndex = 3
+ appPreferences.mainViewTrip = tripIndex
+ refreshActivity(tripIndex)
+ }
+
+ summary_button_trip_next.setOnClickListener {
+ var tripIndex = appPreferences.mainViewTrip
+ tripIndex++
+ if (tripIndex > 3) tripIndex = 0
+ appPreferences.mainViewTrip = tripIndex
+ refreshActivity(tripIndex)
+ }
+
+ summary_title.setOnClickListener {
+ var isMainDataManager = false
+ for (mainDataManager in DataManagers.values()) {
+ isMainDataManager = dataManager == mainDataManager.dataManager
+ if (isMainDataManager) break
+ }
+ if (isMainDataManager) {
+ var tripIndex = appPreferences.mainViewTrip
+ tripIndex++
+ if (tripIndex > 3) tripIndex = 0
+ appPreferences.mainViewTrip = tripIndex
+ refreshActivity(tripIndex)
+ } else
+ TODO("Don't use onClick when showing a specific trip, not one of the 4 main trips")
+ }
+
+ // enabledTint = summary_charge_plot_button_next.foreground!!
disabledTint = PorterDuffColorFilter(getColor(R.color.disabled_tint), PorterDuff.Mode.SRC_IN)
- enabledTint = PorterDuffColorFilter(getColor(android.R.color.white), PorterDuff.Mode.SRC_IN)
+ enabledTint = PorterDuffColorFilter(primaryColor!!, PorterDuff.Mode.SRC_IN)
+
+ summary_title.text = getString(resources.getIdentifier(
+ dataManager.printableName, "string", packageName
+ ))
summary_button_back.setOnClickListener {
finish()
@@ -88,7 +163,7 @@ class SummaryActivity: Activity() {
createResetDialog()
}
- summary_trip_date_text.text = getDateString(tripData.tripStartDate)
+ summary_trip_date_text.text = getString(R.string.summary_trip_start_date).format(StringFormatters.getDateString(tripData.tripStartDate))
summary_button_show_consumption_container.isSelected = true
@@ -105,75 +180,140 @@ class SummaryActivity: Activity() {
setupConsumptionLayout()
setupChargeLayout()
- registerReceiver(broadcastReceiver, IntentFilter(getString(R.string.gear_update_broadcast)))
+ registerReceiver(broadcastReceiver, IntentFilter(getString(R.string.distraction_optimization_broadcast)))
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ unregisterReceiver(broadcastReceiver)
}
private fun setupConsumptionLayout() {
- summary_consumption_plot.addPlotLine(DataHolder.consumptionPlotLine)
- summary_consumption_plot.secondaryDimension = PlotSecondaryDimension.SPEED
+ val plotMarkers = PlotMarkers()
+ plotMarkers.addMarkers(tripData.markers)
+ summary_consumption_plot.addPlotLine(consumptionPlotLine, consumptionPlotLinePaint)
+ summary_consumption_plot.sessionGapRendering = PlotSessionGapRendering.JOIN
+ summary_consumption_plot.secondaryDimension = when (appPreferences.secondaryConsumptionDimension) {
+ 1 -> {
+ summary_button_secondary_dimension.text =
+ getString(R.string.main_secondary_axis, getString(R.string.main_speed))
+ PlotSecondaryDimension.SPEED
+ }
+ 2 -> {
+ summary_button_secondary_dimension.text =
+ getString(R.string.main_secondary_axis, getString(R.string.main_SoC))
+ PlotSecondaryDimension.STATE_OF_CHARGE
+ }
+ else -> {
+ summary_button_secondary_dimension.text = getString(R.string.main_secondary_axis, "-")
+ null
+ }
+ }
summary_consumption_plot.dimension = PlotDimension.DISTANCE
- summary_consumption_plot.dimensionRestriction = ((DataHolder.traveledDistance / MainActivity.DISTANCE_TRIP_DIVIDER).toInt() + 1) * MainActivity.DISTANCE_TRIP_DIVIDER + 1
+ summary_consumption_plot.dimensionRestriction = appPreferences.distanceUnit.asUnit(((appPreferences.distanceUnit.toUnit(tripData.traveledDistance) / MainActivity.DISTANCE_TRIP_DIVIDER).toInt() + 1) * MainActivity.DISTANCE_TRIP_DIVIDER) + 1
+ summary_consumption_plot.dimensionRestrictionMin = appPreferences.distanceUnit.asUnit(MainActivity.DISTANCE_TRIP_DIVIDER)
summary_consumption_plot.dimensionSmoothingPercentage = 0.02f
- summary_consumption_plot.setPlotMarkers(DataHolder.plotMarkers)
+ summary_consumption_plot.setPlotMarkers(plotMarkers)
summary_consumption_plot.visibleMarkerTypes.add(PlotMarkerType.CHARGE)
summary_consumption_plot.visibleMarkerTypes.add(PlotMarkerType.PARK)
- summary_consumption_plot.dimensionShiftTouchInterval = 1_000L
- summary_consumption_plot.dimensionRestrictionTouchInterval = 5_000L
summary_consumption_plot.invalidate()
- summary_distance_value_text.text = getTraveledDistanceString()
- summary_used_energy_value_text.text = getUsedEnergyString()
- summary_avg_consumption_value_text.text = getAvgConsumptionString()
- summary_travel_time_value_text.text = getElapsedTimeString(DataHolder.travelTimeMillis)
+ summary_distance_value_text.text = StringFormatters.getTraveledDistanceString(tripData.traveledDistance)
+ summary_used_energy_value_text.text = StringFormatters.getEnergyString(tripData.usedEnergy)
+ summary_avg_consumption_value_text.text = StringFormatters.getAvgConsumptionString(tripData.usedEnergy, tripData.traveledDistance)
+ summary_travel_time_value_text.text = StringFormatters.getElapsedTimeString(tripData.travelTime)
+
+ summary_button_secondary_dimension.setOnClickListener {
+ var currentIndex = appPreferences.secondaryConsumptionDimension
+ currentIndex++
+ if (currentIndex > 2) currentIndex = 0
+ appPreferences.secondaryConsumptionDimension = currentIndex
+ summary_consumption_plot.secondaryDimension = when (appPreferences.secondaryConsumptionDimension) {
+ 1 -> {
+ summary_button_secondary_dimension.text =
+ getString(R.string.main_secondary_axis, getString(R.string.main_speed))
+ PlotSecondaryDimension.SPEED
+ }
+ 2 -> {
+ summary_button_secondary_dimension.text =
+ getString(R.string.main_secondary_axis, getString(R.string.main_SoC))
+ PlotSecondaryDimension.STATE_OF_CHARGE
+ }
+ else -> {
+ summary_button_secondary_dimension.text = getString(R.string.main_secondary_axis, "-")
+ null
+ }
+ }
+ }
+ }
+
+ private fun refreshActivity(index: Int?) {
+ val refreshIntent = Intent(this, SummaryActivity::class.java).apply {
+ if (index != null) {
+ putExtra("dataManager", index)
+ }
+ }
+ finish()
+ overridePendingTransition(0, 0)
+ startActivity(refreshIntent)
+ overridePendingTransition(0, 0)
}
private fun setupChargeLayout() {
- summary_charge_plot_sub_title_curve.text = "%s (%d/%d)".format(
+ summary_charge_plot_sub_title_curve.text = "%s (%d/%d, %s)".format(
getString(R.string.settings_sub_title_last_charge_plot),
- DataHolder.chargeCurves.size,
- DataHolder.chargeCurves.size)
+ tripData.chargeCurves.size,
+ tripData.chargeCurves.size,
+ StringFormatters.getDateString(
+ if (tripData.chargeCurves.isNotEmpty()) tripData.chargeCurves.last().chargeStartDate
+ else null
+ )
+ )
- chargePlotLine.plotPaint = PlotPaint.byColor(getColor(R.color.charge_plot_color), PlotView.textSize)
- chargePlotLine.secondaryPlotPaint = when {
- appPreferences.chargePlotSecondaryColor -> PlotPaint.byColor(getColor(R.color.secondary_plot_color_alt), PlotView.textSize)
- else -> PlotPaint.byColor(getColor(R.color.secondary_plot_color), PlotView.textSize)
- }
chargePlotLine.reset()
- if (DataHolder.chargeCurves.isNotEmpty()) {
- chargePlotLine.addDataPoints(DataHolder.chargeCurves[DataHolder.chargeCurves.size - 1].chargePlotLine)
+ if (tripData.chargeCurves.isNotEmpty()) {
+ chargePlotLine.addDataPoints(tripData.chargeCurves.last().chargePlotLine)
summary_charge_plot_button_next.isEnabled = false
summary_charge_plot_button_next.colorFilter = disabledTint
summary_charge_plot_button_prev.isEnabled = true
summary_charge_plot_button_prev.colorFilter = enabledTint
- summary_charged_energy_value_text.text = getChargedEnergyString(summary_charge_plot_seek_bar.progress)
- summary_charge_time_value_text.text = getElapsedTimeString(tripData.chargeCurves[summary_charge_plot_seek_bar.progress].chargeTime)
- summary_charge_plot_view.dimensionRestriction = TimeUnit.MINUTES.toNanos((TimeUnit.MILLISECONDS.toMinutes(tripData.chargeCurves[summary_charge_plot_seek_bar.progress].chargeTime) / 5) + 1) * 5 + TimeUnit.MILLISECONDS.toNanos(1)
+ if (tripData.chargeCurves.last().chargePlotLine.filter { it.Marker == PlotLineMarkerType.END_SESSION }.size > 1)
+ // Charge has been interrupted
+ summary_charged_energy_warning_text.visibility = View.VISIBLE
+ else summary_charged_energy_warning_text.visibility = View.GONE
+ summary_charged_energy_value_text.text = chargedEnergy(tripData.chargeCurves.last())
+ summary_charge_time_value_text.text = StringFormatters.getElapsedTimeString(tripData.chargeCurves.last().chargeTime)
+ summary_charge_ambient_temp.text = StringFormatters.getTemperatureString(tripData.chargeCurves.last().ambientTemperature)
+ summary_charge_plot_view.dimensionRestriction = TimeUnit.MINUTES.toMillis((TimeUnit.MILLISECONDS.toMinutes(tripData.chargeCurves.last().chargeTime) / 5) + 1) * 5 + 1
+ summary_charge_plot_view.dimensionRestrictionMin = TimeUnit.MINUTES.toMillis(5L)
}
- if (DataHolder.chargeCurves.size < 2){
+ if (tripData.chargeCurves.size < 2){
summary_charge_plot_button_next.isEnabled = false
summary_charge_plot_button_next.colorFilter = disabledTint
summary_charge_plot_button_prev.isEnabled = false
summary_charge_plot_button_prev.colorFilter = disabledTint
}
- summary_charge_plot_view.addPlotLine(chargePlotLine)
+ summary_charge_plot_view.addPlotLine(chargePlotLine, chargePlotLinePaint)
summary_charge_plot_view.dimension = PlotDimension.TIME
- summary_charge_plot_view.dimensionSmoothingPercentage = 0.01f
+ // summary_charge_plot_view.dimensionSmoothingPercentage = 0.01f
+ summary_charge_plot_view.sessionGapRendering = PlotSessionGapRendering.GAP
summary_charge_plot_view.secondaryDimension = PlotSecondaryDimension.STATE_OF_CHARGE
summary_charge_plot_view.invalidate()
- summary_charge_plot_seek_bar.max = (DataHolder.chargeCurves.size - 1).coerceAtLeast(0)
- summary_charge_plot_seek_bar.progress = (DataHolder.chargeCurves.size - 1).coerceAtLeast(0)
+ summary_charge_plot_seek_bar.max = (tripData.chargeCurves.size - 1).coerceAtLeast(0)
+ summary_charge_plot_seek_bar.progress = (tripData.chargeCurves.size - 1).coerceAtLeast(0)
summary_charge_plot_seek_bar.setOnSeekBarChangeListener(seekBarChangeListener)
summary_charge_plot_button_next.setOnClickListener {
val newProgress = summary_charge_plot_seek_bar.progress + 1
- if (newProgress <= (DataHolder.chargeCurves.size - 1)) {
+ if (newProgress <= (tripData.chargeCurves.size - 1)) {
summary_charge_plot_seek_bar.progress = newProgress
}
+ summary_charge_plot_view.dimensionShift = 0L
}
summary_charge_plot_button_prev.setOnClickListener {
@@ -181,16 +321,18 @@ class SummaryActivity: Activity() {
if (newProgress >= 0) {
summary_charge_plot_seek_bar.progress = newProgress
}
+ summary_charge_plot_view.dimensionShift = 0L
}
}
private fun setVisibleChargeCurve(progress: Int) {
- summary_charge_plot_sub_title_curve.text = "%s (%d/%d)".format(
+ summary_charge_plot_sub_title_curve.text = "%s (%d/%d, %s)".format(
getString(R.string.settings_sub_title_last_charge_plot),
- DataHolder.chargeCurves.size,
- DataHolder.chargeCurves.size)
+ tripData.chargeCurves.size,
+ tripData.chargeCurves.size,
+ StringFormatters.getDateString(tripData.chargeCurves.last().chargeStartDate))
- if (DataHolder.chargeCurves.size - 1 == 0) {
+ if (tripData.chargeCurves.size - 1 == 0) {
summary_charge_plot_sub_title_curve.text = "%s (0/0)".format(
getString(R.string.settings_sub_title_last_charge_plot))
@@ -200,10 +342,11 @@ class SummaryActivity: Activity() {
summary_charge_plot_button_prev.colorFilter = disabledTint
} else {
- summary_charge_plot_sub_title_curve.text = "%s (%d/%d)".format(
+ summary_charge_plot_sub_title_curve.text = "%s (%d/%d, %s)".format(
getString(R.string.settings_sub_title_last_charge_plot),
progress + 1,
- DataHolder.chargeCurves.size)
+ tripData.chargeCurves.size,
+ StringFormatters.getDateString(tripData.chargeCurves.last().chargeStartDate))
when (progress) {
0 -> {
@@ -212,7 +355,7 @@ class SummaryActivity: Activity() {
summary_charge_plot_button_next.isEnabled = true
summary_charge_plot_button_next.colorFilter = enabledTint
}
- DataHolder.chargeCurves.size - 1 -> {
+ tripData.chargeCurves.size - 1 -> {
summary_charge_plot_button_next.isEnabled = false
summary_charge_plot_button_next.colorFilter = disabledTint
summary_charge_plot_button_prev.isEnabled = true
@@ -226,13 +369,18 @@ class SummaryActivity: Activity() {
}
}
}
+ if (tripData.chargeCurves[progress].chargePlotLine.filter { it.Marker == PlotLineMarkerType.END_SESSION }.size > 1)
+ // Charge has been interrupted
+ summary_charged_energy_warning_text.visibility = View.VISIBLE
+ else summary_charged_energy_warning_text.visibility = View.GONE
- summary_charged_energy_value_text.text = getChargedEnergyString(progress)
- summary_charge_time_value_text.text = getElapsedTimeString(tripData.chargeCurves[progress].chargeTime)
+ summary_charged_energy_value_text.text = chargedEnergy(tripData.chargeCurves[progress])
+ summary_charge_time_value_text.text = StringFormatters.getElapsedTimeString(tripData.chargeCurves[progress].chargeTime)
+ summary_charge_ambient_temp.text = StringFormatters.getTemperatureString(tripData.chargeCurves[progress].ambientTemperature)
chargePlotLine.reset()
- chargePlotLine.addDataPoints(DataHolder.chargeCurves[progress].chargePlotLine)
- summary_charge_plot_view.dimensionRestriction = TimeUnit.MINUTES.toNanos((TimeUnit.MILLISECONDS.toMinutes(tripData.chargeCurves[progress].chargeTime) / 5) + 1) * 5 + TimeUnit.MILLISECONDS.toNanos(1)
+ chargePlotLine.addDataPoints(tripData.chargeCurves[progress].chargePlotLine)
+ summary_charge_plot_view.dimensionRestriction = TimeUnit.MINUTES.toMillis((TimeUnit.MILLISECONDS.toMinutes(tripData.chargeCurves[progress].chargeTime) / 5) + 1) * 5 + 1
summary_charge_plot_view.invalidate()
}
@@ -259,12 +407,14 @@ class SummaryActivity: Activity() {
.setMessage(getString(R.string.dialog_reset_message))
.setCancelable(true)
.setPositiveButton(getString(R.string.dialog_reset_do_save)) { _, _ ->
- DataHolder.resetDataHolder()
+ DataCollector.CurrentTripDataManager.reset()
sendBroadcast(Intent(getString(R.string.save_trip_data_broadcast)))
this@SummaryActivity.finish()
}
.setNegativeButton(R.string.dialog_reset_no_save) { _, _ ->
- DataHolder.resetDataHolder()
+ // DataCollector.CurrentTripDataManager.reset()
+ // enumValues().forEach { it.dataManager.reset() }
+ DataManagers.CURRENT_TRIP.dataManager.reset()
sendBroadcast(Intent(getString(R.string.save_trip_data_broadcast)))
this@SummaryActivity.finish()
}
@@ -278,63 +428,20 @@ class SummaryActivity: Activity() {
}
- private fun getDateString(tripStartDate: Date): String {
- val dateFormat = DateFormat.getDateFormat(applicationContext)
- val timeFormat = DateFormat.getTimeFormat(applicationContext)
- return "${getString(R.string.summary_trip_start_date)} ${dateFormat.format(tripStartDate)}, ${timeFormat.format(tripStartDate)}"
- }
-
- private fun getUsedEnergyString(): String {
- if (!appPreferences.consumptionUnit) {
- return "%.1f kWh".format(
- Locale.ENGLISH,
- DataHolder.usedEnergy / 1000)
- }
- return "${DataHolder.usedEnergy.toInt()} Wh"
- }
-
- private fun getChargedEnergyString(index: Int): String {
- if (!appPreferences.consumptionUnit) {
- return "%.1f kWh".format(
- Locale.ENGLISH,
- DataHolder.chargeCurves[index].chargedEnergyWh / 1000)
- }
- return "${DataHolder.chargeCurves[index].chargedEnergyWh.toInt()} Wh"
- }
-
- private fun getTraveledDistanceString(): String {
- return "%.1f km".format(Locale.ENGLISH, DataHolder.traveledDistance / 1000)
- }
-
- private fun getAvgConsumptionString(): String {
- val unitString = if (appPreferences.consumptionUnit) "Wh/km" else "kWh/100km"
- if (DataHolder.traveledDistance <= 0) {
- return "-/- $unitString"
- }
- if (!appPreferences.consumptionUnit) {
- return "%.1f %s".format(
- Locale.ENGLISH,
- (DataHolder.usedEnergy /(DataHolder.traveledDistance /1000))/10,
- unitString)
- }
- return "${(DataHolder.usedEnergy /(DataHolder.traveledDistance /1000)).toInt()} $unitString"
- }
-
- private fun getElapsedTimeString(elapsedTime: Long): String {
- return String.format("%02d:%02d:%02d",
- TimeUnit.MILLISECONDS.toHours(elapsedTime),
- TimeUnit.MILLISECONDS.toMinutes(elapsedTime) % TimeUnit.HOURS.toMinutes(1),
- TimeUnit.MILLISECONDS.toSeconds(elapsedTime) % TimeUnit.MINUTES.toSeconds(1))
- }
-
- private fun updateDistractionOptimization(update: Boolean = false) {
- if (update) {
- finish()
- startActivity(intent)
- return
- }
+ private fun updateDistractionOptimization() {
+ InAppLogger.log("Distraction optimization: ${appPreferences.doDistractionOptimization}")
summary_parked_warning.visibility =
- if (DataHolder.currentGear != VehicleGear.GEAR_PARK) View.VISIBLE
+ if (appPreferences.doDistractionOptimization) View.VISIBLE
else View.GONE
+ summary_content_container.visibility =
+ if (appPreferences.doDistractionOptimization) View.GONE
+ else View.VISIBLE
+ }
+
+ private fun chargedEnergy(chargeCurve: ChargeCurve): String {
+ return "%s (%d%%)".format(
+ StringFormatters.getEnergyString(chargeCurve.chargedEnergy),
+ (chargeCurve.chargePlotLine.last().StateOfCharge - chargeCurve.chargePlotLine.first().StateOfCharge).toInt()
+ )
}
-}
+}
\ No newline at end of file
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/appPreferences/AppPreference.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/appPreferences/AppPreference.kt
new file mode 100644
index 00000000..aa55574e
--- /dev/null
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/appPreferences/AppPreference.kt
@@ -0,0 +1,34 @@
+package com.ixam97.carStatsViewer.appPreferences
+
+import android.content.SharedPreferences
+import com.ixam97.carStatsViewer.enums.DistanceUnitEnum
+import com.ixam97.carStatsViewer.plot.enums.PlotDimension
+
+class AppPreference(
+ private val key: String,
+ private val default: T,
+ private val sharedPref: SharedPreferences) {
+
+ var value: T
+ get() {
+ return when (default) {
+ is Boolean -> sharedPref.getBoolean(key, default) as T
+ is Int -> sharedPref.getInt(key, default) as T
+ is String -> sharedPref.getString(key, default) as T
+ is PlotDimension -> PlotDimension.valueOf(sharedPref.getString(key, default.name)!!) as T
+ is DistanceUnitEnum -> DistanceUnitEnum.valueOf(sharedPref.getString(key, default.name)!!) as T
+ else -> default
+ }
+ }
+ set(value) {
+ when (default) {
+ is Boolean -> sharedPref.edit().putBoolean(key, value as Boolean).apply()
+ is Int -> sharedPref.edit().putInt(key, value as Int).apply()
+ is String -> sharedPref.edit().putString(key, value as String).apply()
+ is PlotDimension -> sharedPref.edit().putString(key, (value as PlotDimension).name).apply()
+ is DistanceUnitEnum ->sharedPref.edit().putString(key, (value as DistanceUnitEnum).name).apply()
+ //else ->
+ }
+ }
+}
+
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/appPreferences/AppPreferences.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/appPreferences/AppPreferences.kt
new file mode 100644
index 00000000..89fda155
--- /dev/null
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/appPreferences/AppPreferences.kt
@@ -0,0 +1,82 @@
+package com.ixam97.carStatsViewer.appPreferences
+
+import android.content.Context
+import android.content.SharedPreferences
+import com.google.gson.ExclusionStrategy
+import com.google.gson.FieldAttributes
+import com.ixam97.carStatsViewer.R
+import com.ixam97.carStatsViewer.enums.DistanceUnitEnum
+import com.ixam97.carStatsViewer.plot.enums.PlotDimension
+import com.ixam97.carStatsViewer.utils.Exclude
+
+class AppPreferences(
+ val context: Context
+) {
+ private var sharedPref: SharedPreferences = context.getSharedPreferences(
+ context.getString(R.string.preferences_file_key), Context.MODE_PRIVATE
+ )
+
+ private val VersionString = AppPreference(context.getString(R.string.preference_version_key),"", sharedPref)
+
+ private val Debug = AppPreference(context.getString(R.string.preference_debug_key), false, sharedPref)
+ private val Notification = AppPreference(context.getString(R.string.preference_notifications_key), false, sharedPref)
+ private val ConsumptionUnit = AppPreference(context.getString(R.string.preference_consumption_unit_key), false, sharedPref)
+ // private //val ExperimentalLayout = AppPreference(context.getString(R.string.preferences_notifications_key), false, sharedPref)
+ // private //val DeepLog = AppPreference(context.getString(R.string.preferences_notifications_key), false, sharedPref)
+ private val PlotSpeed = AppPreference(context.getString(R.string.preference_plot_speed_key), false, sharedPref)
+ // private //val PlotDistance = AppPreference(context.getString(R.string.preferences_notifications_key), false, sharedPref)
+ private val ConsumptionPlotSingleMotor = AppPreference(context.getString(R.string.preference_consumption_plot_single_motor_key), false, sharedPref)
+ private val ConsumptionPlotSecondaryColor = AppPreference(context.getString(R.string.preference_consumption_plot_secondary_color_key), false, sharedPref)
+ private val ConsumptionPlotVisibleGages = AppPreference(context.getString(R.string.preference_consumption_plot_visible_gages_key), true, sharedPref)
+ private val ChagrPlotSecondaryColor = AppPreference(context.getString(R.string.preference_charge_plot_secondary_color_key), false, sharedPref)
+ private val ChargePlotVisibleGages = AppPreference(context.getString(R.string.preference_charge_plot_visible_gages_key), true, sharedPref)
+ private val ChargePlotDimension = AppPreference(context.getString(R.string.preference_charge_plot_dimension_key), PlotDimension.TIME, sharedPref)
+ private val DistanceUnit = AppPreference(context.getString(R.string.preference_distance_unit_key), DistanceUnitEnum.KM, sharedPref)
+ private val SecondaryConsumptionDimension = AppPreference(context.getString(R.string.preference_secondary_dimension_key), 0, sharedPref)
+ private val MainViewTrip = AppPreference(context.getString(R.string.preference_main_view_trip_key), 0, sharedPref)
+ private val SmtpAddress = AppPreference(context.getString(R.string.preference_smtp_address_key), "", sharedPref)
+ private val SmtpPassword = AppPreference(context.getString(R.string.preference_smtp_password_key), "", sharedPref)
+ private val SmtpServer = AppPreference(context.getString(R.string.preference_smtp_server_key), "", sharedPref)
+ private val LogTargetAddress = AppPreference(context.getString(R.string.preference_log_target_address_key), "ixam97@ixam97.de", sharedPref)
+ private val LogUserName = AppPreference(context.getString(R.string.preference_log_user_name_key), "", sharedPref)
+
+
+ var versionString: String get() = VersionString.value; set(value) {VersionString.value = value}
+
+ var debug: Boolean get() = Debug.value; set(value) {Debug.value = value}
+ var notifications: Boolean get() = Notification.value; set(value) {Notification.value = value}
+ var consumptionUnit: Boolean get() = ConsumptionUnit.value; set(value) {ConsumptionUnit.value = value}
+ var plotSpeed: Boolean get() = PlotSpeed.value; set(value) {PlotSpeed.value = value}
+ var consumptionPlotSingleMotor: Boolean get() = ConsumptionPlotSingleMotor.value; set(value) {ConsumptionPlotSingleMotor.value = value}
+ var consumptionPlotSecondaryColor: Boolean get() = ConsumptionPlotSecondaryColor.value; set(value) {ConsumptionPlotSecondaryColor.value = value}
+ var consumptionPlotVisibleGages: Boolean get() = ConsumptionPlotVisibleGages.value; set(value) {ConsumptionPlotVisibleGages.value = value}
+ var chargePlotSecondaryColor: Boolean get() = ChagrPlotSecondaryColor.value; set(value) {ChagrPlotSecondaryColor.value = value}
+ var chargePlotVisibleGages: Boolean get() = ChargePlotVisibleGages.value; set(value) {ChargePlotVisibleGages.value = value}
+ var chargePlotDimension: PlotDimension get() = ChargePlotDimension.value; set(value) {ChargePlotDimension.value = value}
+ var distanceUnit: DistanceUnitEnum get() = DistanceUnit.value; set(value) {DistanceUnit.value = value}
+ var secondaryConsumptionDimension: Int get() = SecondaryConsumptionDimension.value; set(value) {SecondaryConsumptionDimension.value = value}
+ var mainViewTrip: Int get() = MainViewTrip.value; set(value) {MainViewTrip.value = value}
+ var smtpAddress: String get() = SmtpAddress.value; set(value) {SmtpAddress.value = value}
+ var smtpPassword: String get() = SmtpPassword.value; set(value) {SmtpPassword.value = value}
+ var smtpServer: String get() = SmtpServer.value; set(value) {SmtpServer.value = value}
+ var logUserName: String get() = LogUserName.value; set(value) {LogUserName.value = value}
+ var logTargetAddress: String get() = LogTargetAddress.value; set(value) {LogTargetAddress.value = value}
+
+ // Preferences not saved permanently:
+ val exclusionStrategy = AppPreferences.exclusionStrategy
+ var doDistractionOptimization: Boolean get() = AppPreferences.doDistractionOptimization; set(value) {AppPreferences.doDistractionOptimization = value}
+
+ companion object {
+ val exclusionStrategy: ExclusionStrategy = object : ExclusionStrategy {
+ override fun shouldSkipClass(clazz: Class<*>?): Boolean {
+ return false
+ }
+
+ override fun shouldSkipField(field: FieldAttributes): Boolean {
+ return field.getAnnotation(Exclude::class.java) != null
+ }
+ }
+
+ var doDistractionOptimization = false
+ }
+}
\ No newline at end of file
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/dataManager/ChargeCurve.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/dataManager/ChargeCurve.kt
new file mode 100644
index 00000000..10ddca8f
--- /dev/null
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/dataManager/ChargeCurve.kt
@@ -0,0 +1,12 @@
+package com.ixam97.carStatsViewer.dataManager
+
+import com.ixam97.carStatsViewer.plot.objects.PlotLineItem
+import java.util.*
+
+data class ChargeCurve(
+ val chargePlotLine: List,
+ val chargeTime: Long,
+ val chargedEnergy: Float,
+ val ambientTemperature: Float? = null,
+ val chargeStartDate: Date? = null
+) {}
\ No newline at end of file
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/dataManager/DataCollector.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/dataManager/DataCollector.kt
new file mode 100644
index 00000000..d73c4814
--- /dev/null
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/dataManager/DataCollector.kt
@@ -0,0 +1,751 @@
+package com.ixam97.carStatsViewer.dataManager
+
+import android.app.*
+import android.car.Car
+import android.car.VehicleIgnitionState
+import android.car.VehiclePropertyIds
+import android.car.VehicleUnit
+import android.car.hardware.CarPropertyValue
+import android.car.hardware.property.CarPropertyManager
+import android.content.*
+import android.graphics.BitmapFactory
+import android.os.*
+import android.util.Log
+import android.widget.Toast
+import androidx.core.app.NotificationManagerCompat
+import com.google.gson.Gson
+import com.google.gson.GsonBuilder
+import com.ixam97.carStatsViewer.*
+import com.ixam97.carStatsViewer.activities.emulatorMode
+import com.ixam97.carStatsViewer.appPreferences.AppPreferences
+import com.ixam97.carStatsViewer.enums.DistanceUnitEnum
+import com.ixam97.carStatsViewer.plot.enums.*
+import kotlinx.coroutines.*
+import java.io.File
+import java.io.FileWriter
+import java.lang.Runnable
+import java.util.*
+import java.util.concurrent.TimeUnit
+import kotlin.math.absoluteValue
+
+class DataCollector : Service() {
+ companion object {
+ lateinit var mainActivityPendingIntent: PendingIntent
+ val CurrentTripDataManager = DataManagers.CURRENT_TRIP.dataManager
+ private const val DO_LOG = false
+ private const val CHANNEL_ID = "TestChannel"
+ private const val STATS_NOTIFICATION_ID = 1
+ private const val FOREGROUND_NOTIFICATION_ID = 2
+ private const val NOTIFICATION_TIMER_HANDLER_DELAY_MILLIS = 5_000L
+ private const val CONSUMPTION_PLOT_UPDATE_DISTANCE = 100
+ private const val CHARGE_PLOT_UPDATE_INTERVAL_MILLIS = 2_000L
+ private const val CHARGE_PLOT_MARKER_THRESHOLD_NANOS = 10_000_000_000L // 2 times CHARGE_PLOT_UPDATE_INTERVAL_MILLIS in nanos
+ private const val AUTO_SAVE_INTERVAL_MILLIS = 30_000L
+ private const val AUTO_RESET_TIME_HOURS = 5L
+ private const val POWER_GAGE_HYSTERESIS = 1_000_000F
+ private const val CONS_GAGE_HYSTERESIS = 10F
+
+ var gagePowerValue: Float = 0F
+ var gageConsValue: Float = 0F
+ var gageSoCValue: Int = 0
+ }
+
+ private var lastSoC: Int = 0
+
+ private var startupTimestamp: Long = 0L
+
+ private var lastNotificationTimeMillis = 0L
+
+ private var notificationCounter = 0
+
+ private lateinit var appPreferences: AppPreferences
+
+ private lateinit var gson: Gson
+
+ private var notificationsEnabled = true
+
+ private lateinit var car: Car
+ private lateinit var carPropertyManager: CarPropertyManager
+
+ private lateinit var notificationTitleString: String
+
+ private lateinit var notificationTimerHandler: Handler
+ private lateinit var saveTripDataTimerHandler: Handler
+
+ init {
+ startupTimestamp = System.nanoTime()
+ }
+
+ private val broadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ when (intent.action) {
+ getString(R.string.save_trip_data_broadcast) -> {
+ enumValues().filter{ it.doTrack}.forEach {
+ writeTripDataToFile(it.dataManager.tripData!!, it.dataManager.printableName)
+ }
+ }
+ else -> {}
+ }
+ }
+ }
+
+ private val saveTripDataTask = object : Runnable {
+ override fun run() {
+ enumValues().filter{ it.doTrack}.forEach {
+ writeTripDataToFile(it.dataManager.tripData!!, it.dataManager.printableName)
+ }
+ saveTripDataTimerHandler.postDelayed(this, AUTO_SAVE_INTERVAL_MILLIS)
+ }
+ }
+
+ private val updateStatsNotificationTask = object : Runnable {
+ override fun run() {
+ updateStatsNotification()
+ // InAppLogger.logNotificationUpdate()
+ val currentNotificationTimeMillis = System.currentTimeMillis()
+ lastNotificationTimeMillis = currentNotificationTimeMillis
+
+ // Log additional data:
+ /*
+ InAppLogger.log("Additional data:\n" +
+ " Speed in m/s: ${DataManagers.CURRENT_TRIP.dataManager.CurrentSpeed.value}\n"+
+ " Power in mW: ${DataManagers.CURRENT_TRIP.dataManager.CurrentPower.value}\n"+
+ " Gear selection: ${DataManagers.CURRENT_TRIP.dataManager.CurrentGear.value}\n"+
+ " Connection status: ${DataManagers.CURRENT_TRIP.dataManager.ChargePortConnected.value}\n"+
+ " Battery level in Wh: ${DataManagers.CURRENT_TRIP.dataManager.BatteryLevel.value}\n"+
+ " Ignition state: ${DataManagers.CURRENT_TRIP.dataManager.CurrentIgnitionState.value}\n"+
+ " Ambient temperature: ${DataManagers.CURRENT_TRIP.dataManager.CurrentAmbientTemperature.value}"
+ )
+ */
+
+ notificationTimerHandler.postDelayed(this, NOTIFICATION_TIMER_HANDLER_DELAY_MILLIS)
+ }
+ }
+
+ private lateinit var statsNotification: Notification.Builder
+ private lateinit var foregroundServiceNotification: Notification.Builder
+
+ override fun onBind(intent: Intent): IBinder? {
+ return null
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+
+ var tripRestoreComplete = false
+ CoroutineScope(Dispatchers.IO).launch {
+ enumValues().forEach {
+ val mPrevTripData = readTripDataFromFile(it.dataManager.printableName)
+ if (mPrevTripData != null) {
+ it.dataManager.tripData = mPrevTripData
+ sendBroadcast(Intent(getString(R.string.ui_update_plot_broadcast)))
+ } else {
+ InAppLogger.log("No trip file read!")
+ // Toast.makeText(applicationContext ,R.string.toast_file_read_error, Toast.LENGTH_LONG).show()
+ }
+ }
+ tripRestoreComplete = true
+ }
+
+ while (!tripRestoreComplete) {
+ // Wait for completed restore before doing anything
+ }
+
+ createNotificationChannel()
+
+ foregroundServiceNotification = Notification.Builder(applicationContext, CHANNEL_ID)
+ .setContentTitle(getString(R.string.app_name))
+ .setContentText(getString(R.string.foreground_service_info))
+ .setSmallIcon(R.drawable.ic_launcher_foreground)
+ .setOngoing(true)
+
+ statsNotification = Notification.Builder(this, CHANNEL_ID)
+ .setContentTitle("Title")
+ .setContentText("Test Notification from Car Stats Viewer")
+ .setSmallIcon(R.drawable.ic_launcher_foreground)
+ .setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.ic_launcher_foreground))
+ .setStyle(Notification.MediaStyle())
+ .setCategory(Notification.CATEGORY_TRANSPORT)
+ .setOngoing(true)
+
+ // InAppLogger.log(String.format( "DataCollector.onCreate in Thread: %s", Thread.currentThread().name))
+
+ appPreferences = AppPreferences(applicationContext)
+
+ gson = GsonBuilder()
+ .setExclusionStrategies(appPreferences.exclusionStrategy)
+ .setPrettyPrinting()
+ .create()
+
+ notificationsEnabled = appPreferences.notifications
+
+ car = Car.createCar(this)
+ carPropertyManager = car.getCarManager(Car.PROPERTY_SERVICE) as CarPropertyManager
+
+ /** Get vehicle name to enable dev mode in emulator */
+ val carName = carPropertyManager.getProperty(VehiclePropertyIds.INFO_MODEL, 0).value.toString()
+ val carManufacturer = carPropertyManager.getProperty(VehiclePropertyIds.INFO_MAKE, 0).value.toString()
+ val carModelYear = carPropertyManager.getIntProperty(VehiclePropertyIds.INFO_MODEL_YEAR, 0).toString()
+ if (carName == "Speedy Model") {
+ Toast.makeText(this, "Emulator Mode", Toast.LENGTH_LONG).show()
+ emulatorMode = true
+ // CurrentTripDataManager.update(VehicleGear.GEAR_PARK, System.nanoTime(), CurrentTripDataManager.CurrentGear.propertyId)
+ }
+
+ val displayUnit = carPropertyManager.getProperty(VehiclePropertyIds.DISTANCE_DISPLAY_UNITS, 0).value
+
+
+ appPreferences.distanceUnit = if (!emulatorMode) {
+ when (displayUnit) {
+ VehicleUnit.MILE -> DistanceUnitEnum.MILES
+ else -> DistanceUnitEnum.KM
+ }
+ } else DistanceUnitEnum.KM
+
+ InAppLogger.log("Display distance unit: $displayUnit")
+ InAppLogger.log("Car name: $carName, $carManufacturer, $carModelYear")
+ InAppLogger.log("Max battery Capacity: ${carPropertyManager.getFloatProperty(VehiclePropertyIds.INFO_EV_BATTERY_CAPACITY, 0)}")
+ InAppLogger.log("Fuel level: ${carPropertyManager.getFloatProperty(VehiclePropertyIds.FUEL_LEVEL, 0)}")
+ InAppLogger.log("Fuel Capacity: ${carPropertyManager.getFloatProperty(VehiclePropertyIds.INFO_FUEL_CAPACITY, 0)}")
+
+ notificationTitleString = resources.getString(R.string.notification_title)
+ statsNotification.setContentTitle(notificationTitleString).setContentIntent(mainActivityPendingIntent)
+
+ if (notificationsEnabled) {
+ with(NotificationManagerCompat.from(this)) {
+ notify(STATS_NOTIFICATION_ID, statsNotification.build())
+ }
+ }
+
+ registerCarPropertyCallbacks()
+
+ notificationTimerHandler = Handler(Looper.getMainLooper())
+ notificationTimerHandler.post(updateStatsNotificationTask)
+ saveTripDataTimerHandler = Handler(Looper.getMainLooper())
+ saveTripDataTimerHandler.postDelayed(saveTripDataTask, AUTO_SAVE_INTERVAL_MILLIS)
+
+ registerReceiver(broadcastReceiver, IntentFilter(getString(R.string.save_trip_data_broadcast)))
+ registerReceiver(carPropertyEmulatorReceiver, IntentFilter(getString(R.string.VHAL_emulator_broadcast)))
+
+ DataManagers.values().filter { it.doTrack }.forEach {
+ for (propertyId in DataManager.propertyIds) {
+ refreshProperty(propertyId, it.dataManager)
+ }
+ it.dataManager.consumptionPlotLine.baseLineAt.add(0f)
+ it.dataManager.maxBatteryLevel = carPropertyManager.getFloatProperty(VehiclePropertyIds.INFO_EV_BATTERY_CAPACITY, 0)
+ InAppLogger.log("Max battery Capacity: ${it.dataManager.maxBatteryLevel}")
+
+ driveStateUpdater(it.dataManager)
+ speedUpdater(it.dataManager)
+ powerUpdater(it.dataManager)
+ }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ // .log("DataCollector.onDestroy")
+ sendBroadcast(Intent(getString(R.string.save_trip_data_broadcast)))
+ unregisterReceiver(broadcastReceiver)
+ car.disconnect()
+ }
+
+ override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
+ // InAppLogger.log("DataCollector.onStartCommand")
+ startForeground(FOREGROUND_NOTIFICATION_ID, foregroundServiceNotification.build())
+ return START_STICKY
+ }
+
+ /** Update DataManagers on new VHAL event */
+ private val carPropertyListener = object : CarPropertyManager.CarPropertyEventCallback {
+ override fun onChangeEvent(carPropertyValue: CarPropertyValue<*>) {
+ DataManagers.values().forEach {
+ if (it.dataManager.update(
+ carPropertyValue,
+ DO_LOG,
+ valueMustChange = false,
+ allowInvalidTimestamps = DataManager.allowInvalidTimestampsMap[carPropertyValue.propertyId] == true
+ ) == it.dataManager.VALID) {
+ handleCarPropertyListenerEvent(carPropertyValue.propertyId, it.dataManager)
+ }
+ }
+ }
+ override fun onErrorEvent(propertyId: Int, zone: Int) {
+ Log.w("carPropertyGenericListener","Received error car property event, propId=$propertyId")
+ }
+ }
+
+ /** Simulate VHAL event by using broadcast for emulator use */
+ private val carPropertyEmulatorReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if (emulatorMode) {
+ if (DO_LOG) Log.i("EMULATOR","Received Emulated VHAL update")
+ val propertyId = intent.getIntExtra(EmulatorIntentExtras.PROPERTY_ID, 0)
+ val valueType = intent.getStringExtra(EmulatorIntentExtras.TYPE)
+ val timestamp = System.nanoTime()
+ val value: Any? = when (valueType) {
+ EmulatorIntentExtras.TYPE_FLOAT -> intent.getFloatExtra(EmulatorIntentExtras.VALUE, 0.0f)
+ EmulatorIntentExtras.TYPE_INT -> intent.getIntExtra(EmulatorIntentExtras.VALUE, 0)
+ EmulatorIntentExtras.TYPE_BOOLEAN -> intent.getBooleanExtra(EmulatorIntentExtras.VALUE, false)
+ EmulatorIntentExtras.TYPE_STRING -> intent.getStringExtra(EmulatorIntentExtras.VALUE)
+ else -> null
+ }
+ if (value != null) {
+ DataManagers.values().filter { it.doTrack }.forEach {
+ if (it.dataManager.update(value, timestamp, propertyId, DO_LOG, valueMustChange = false) == it.dataManager.VALID)
+ handleCarPropertyListenerEvent(propertyId, it.dataManager)
+ }
+ }
+ }
+ }
+ }
+
+ /** Handle incoming property changes by property ID */
+ private fun handleCarPropertyListenerEvent(propertyId: Int, dataManager: DataManager) {
+ when (propertyId) {
+ dataManager.BatteryLevel.propertyId -> {
+ if (dataManager.currentIgnitionState < 3 && dataManager == DataManagers.CURRENT_TRIP.dataManager) InAppLogger.log("BatteryLevel while ignition off: ${dataManager.stateOfCharge}%")
+ if (lastSoC != dataManager.stateOfCharge) {
+ InAppLogger.log("Old SoC: $lastSoC, new SoC: ${dataManager.stateOfCharge} | ${dataManager.batteryLevel} / ${dataManager.maxBatteryLevel} = ${dataManager.batteryLevel/dataManager.maxBatteryLevel}")
+ lastSoC = dataManager.stateOfCharge
+ }
+ }
+ dataManager.CurrentPower.propertyId -> powerUpdater(dataManager)
+ dataManager.CurrentSpeed.propertyId -> speedUpdater(dataManager)
+ dataManager.CurrentIgnitionState.propertyId -> ignitionUpdater(dataManager)
+ dataManager.ChargePortConnected.propertyId -> driveStateUpdater(dataManager)
+ }
+ }
+
+ private fun powerUpdater(dataManager: DataManager) {
+ if (dataManager == DataManagers.CURRENT_TRIP.dataManager && !dataManager.CurrentPower.isInitialValue) {
+ var gageValueChanged = false
+ if ((dataManager.currentPower - gagePowerValue).absoluteValue > POWER_GAGE_HYSTERESIS) {
+ gagePowerValue = dataManager.currentPower
+ gageValueChanged = true
+ }
+ if (((dataManager.currentPower / 1000)/(dataManager.currentSpeed * 3.6) - gageConsValue).absoluteValue > CONS_GAGE_HYSTERESIS) {
+ gageConsValue = ((dataManager.currentPower / 1000)/(dataManager.currentSpeed * 3.6f)).let {
+ if (it.isFinite()) it
+ else 0F
+ }
+ gageValueChanged = true
+ }
+ if (gageValueChanged) sendBroadcast(Intent(getString(R.string.ui_update_gages_broadcast)))
+ }
+ when (dataManager.driveState) {
+ DrivingState.DRIVE -> {
+ if (!dataManager.CurrentPower.isInitialValue) {
+ val usedEnergyDelta = (dataManager.currentPower / 1_000) * ((dataManager.CurrentPower.timeDelta / 3.6E12).toFloat())
+ dataManager.usedEnergy += usedEnergyDelta
+ dataManager.consumptionPlotEnergyDelta += usedEnergyDelta
+ }
+ }
+ DrivingState.CHARGE -> {
+ refreshProperty(dataManager.BatteryLevel.propertyId, dataManager)
+ if (!dataManager.CurrentPower.isInitialValue && !dataManager.BatteryLevel.isInitialValue && dataManager.CurrentPower.timeDelta < CHARGE_PLOT_MARKER_THRESHOLD_NANOS && dataManager.BatteryLevel.timeDelta < CHARGE_PLOT_MARKER_THRESHOLD_NANOS) {
+ val chargedEnergyDelta = (dataManager.currentPower / 1_000) * ((dataManager.CurrentPower.timeDelta / 3.6E12).toFloat())
+ dataManager.chargedEnergy -= chargedEnergyDelta
+ dataManager.chargePlotTimeDelta += dataManager.CurrentPower.timeDelta
+
+ if (dataManager.chargePlotTimeDelta >= CHARGE_PLOT_UPDATE_INTERVAL_MILLIS * 1_000_000) {
+ addChargeDataPoint(dataManager = dataManager)
+ dataManager.chargePlotTimeDelta = 0L
+ sendBroadcast(Intent(getString(R.string.ui_update_plot_broadcast)))
+ }
+
+ } else {
+ if (dataManager == DataManagers.values().first().dataManager) {
+ var printableName1 =
+ (if (dataManager.CurrentPower.timeDelta > CHARGE_PLOT_MARKER_THRESHOLD_NANOS) dataManager.CurrentPower.printableName else "")
+ val printableName2 = (if (dataManager.BatteryLevel.timeDelta > CHARGE_PLOT_MARKER_THRESHOLD_NANOS) dataManager.BatteryLevel.printableName else "")
+ if (printableName2.isNotEmpty()) printableName1 += " and "
+ printableName1 += printableName2
+
+ if (printableName1.isNotEmpty()) InAppLogger.log("DATA COLLECTOR: Discarded charge plot data Point due to large time delta of $printableName1!")
+ else InAppLogger.log("DATA COLLECTOR: Discarded charge plot data Point due to initial values!")
+ }
+ }
+ }
+ else -> {
+ // Supplemental energy usage?
+ }
+ }
+ }
+
+ private fun speedUpdater(dataManager: DataManager) {
+ if (dataManager == DataManagers.CURRENT_TRIP.dataManager && !dataManager.CurrentPower.isInitialValue) {
+ var gageValueChanged = false
+ if ((dataManager.currentPower - gagePowerValue).absoluteValue > POWER_GAGE_HYSTERESIS) {
+ gagePowerValue = dataManager.currentPower
+ gageValueChanged = true
+ }
+ if (((dataManager.currentPower / 1000)/(dataManager.currentSpeed * 3.6) - gageConsValue).absoluteValue > CONS_GAGE_HYSTERESIS) {
+ gageConsValue = ((dataManager.currentPower / 1000)/(dataManager.currentSpeed * 3.6f)).let {
+ if (it.isFinite()) it
+ else 0F
+ }
+ gageValueChanged = true
+ }
+ if (gageValueChanged) sendBroadcast(Intent(getString(R.string.ui_update_gages_broadcast)))
+ }
+ if (emulatorMode) {
+ val emulatePowerIntent = Intent(getString(R.string.VHAL_emulator_broadcast)).apply {
+ putExtra(EmulatorIntentExtras.PROPERTY_ID, dataManager.CurrentPower.propertyId)
+ putExtra(EmulatorIntentExtras.TYPE, EmulatorIntentExtras.TYPE_FLOAT)
+ putExtra(EmulatorIntentExtras.VALUE, carPropertyManager.getFloatProperty(dataManager.CurrentPower.propertyId, 0))
+ }
+ sendBroadcast(emulatePowerIntent)
+ }
+ if (!dataManager.CurrentSpeed.isInitialValue && dataManager.driveState == DrivingState.DRIVE) {
+ val traveledDistanceDelta = (dataManager.currentSpeed.absoluteValue * dataManager.CurrentSpeed.timeDelta.toFloat()) / 1_000_000_000F
+ dataManager.traveledDistance += traveledDistanceDelta
+ if (dataManager == DataManagers.CURRENT_TRIP.dataManager) {
+ if (dataManager.currentSpeed.absoluteValue >= 1 && !appPreferences.doDistractionOptimization) {
+ // Drive started -> Distraction optimization
+ appPreferences.doDistractionOptimization = true
+ sendBroadcast(Intent(getString(R.string.distraction_optimization_broadcast)))
+ InAppLogger.log("Drive started")
+ } else if (dataManager.currentSpeed.absoluteValue < 1 && appPreferences.doDistractionOptimization) {
+ // Drive ended
+ appPreferences.doDistractionOptimization = false
+ sendBroadcast(Intent(getString(R.string.distraction_optimization_broadcast)))
+ InAppLogger.log("Drive ended")
+ }
+ }
+
+ dataManager.consumptionPlotDistanceDelta += traveledDistanceDelta
+
+ if (dataManager.consumptionPlotDistanceDelta >= CONSUMPTION_PLOT_UPDATE_DISTANCE) {
+ if (dataManager.driveState == DrivingState.DRIVE) {
+ addConsumptionDataPoint(
+ if(dataManager.consumptionPlotDistanceDelta > 0) dataManager.consumptionPlotEnergyDelta / (dataManager.consumptionPlotDistanceDelta / 1000) else 0F,
+ dataManager = dataManager
+ )
+ }
+ dataManager.consumptionPlotDistanceDelta = 0F
+ dataManager.consumptionPlotEnergyDelta = 0F
+ sendBroadcast(Intent(getString(R.string.ui_update_plot_broadcast)))
+ }
+ }
+ }
+
+ private fun driveStateUpdater(dataManager: DataManager) {
+ // Get real current properties to avoid old values after hibernation.
+ refreshProperty(dataManager.CurrentIgnitionState.propertyId, dataManager)
+ refreshProperty(dataManager.ChargePortConnected.propertyId, dataManager)
+ refreshProperty(dataManager.BatteryLevel.propertyId, dataManager)
+
+ val previousDrivingState = dataManager.DriveState.lastDriveState
+ if (dataManager.DriveState.hasChanged()) {
+ if (dataManager == DataManagers.values().first().dataManager) InAppLogger.log("DRIVE STATE: ${DrivingState.nameMap[previousDrivingState]} -> ${DrivingState.nameMap[dataManager.driveState]} (${dataManager.CurrentIgnitionState.value}")
+ when (dataManager.driveState) {
+ DrivingState.DRIVE -> driveState(previousDrivingState, dataManager)
+ DrivingState.CHARGE -> chargeState(previousDrivingState, dataManager)
+ DrivingState.PARKED -> parkState(previousDrivingState, dataManager)
+ }
+ writeTripDataToFile(dataManager.tripData!!, dataManager.printableName)
+ sendBroadcast(Intent(getString(R.string.ui_update_plot_broadcast)))
+ }
+ }
+
+ private fun ignitionUpdater(dataManager: DataManager) {
+ if (dataManager == DataManagers.values().first().dataManager) {
+ InAppLogger.log("Ignition switched to: ${ignitionList[dataManager.currentIgnitionState]}")
+ }
+ driveStateUpdater(dataManager)
+ }
+
+ private fun driveState(previousDrivingState: Int, dataManager: DataManager) {
+ resetAutoTrips(previousDrivingState, DrivingState.DRIVE, dataManager)
+ resumeTrip(dataManager)
+ if (previousDrivingState == DrivingState.CHARGE) stopChargingSession(dataManager)
+ if (previousDrivingState != DrivingState.UNKNOWN) dataManager.plotMarkers.endMarker(System.currentTimeMillis(), dataManager.traveledDistance)
+ if (previousDrivingState == DrivingState.UNKNOWN) {
+ if (dataManager.plotMarkers.markers.isNotEmpty()) {
+ val currentMarker = dataManager.plotMarkers.markers.maxWith(Comparator.comparingLong { it.StartTime })
+ if (currentMarker.EndTime == null && listOf(PlotMarkerType.CHARGE, PlotMarkerType.PARK).contains(currentMarker.MarkerType)) {
+ dataManager.plotMarkers.endMarker(
+ System.currentTimeMillis(),
+ currentMarker.StartDistance
+ )
+ }
+ }
+ }
+ }
+
+ private fun parkState(previousDrivingState: Int, dataManager: DataManager) {
+ resetAutoTrips(previousDrivingState, DrivingState.PARKED, dataManager)
+ if (previousDrivingState == DrivingState.DRIVE){
+ pauseTrip(dataManager)
+ dataManager.plotMarkers.addMarker(PlotMarkerType.PARK, System.currentTimeMillis(), dataManager.traveledDistance)
+ }
+ if (previousDrivingState == DrivingState.CHARGE) stopChargingSession(dataManager)
+ // Drive ended
+ appPreferences.doDistractionOptimization = false
+ sendBroadcast(Intent(getString(R.string.distraction_optimization_broadcast)))
+ InAppLogger.log("Drive ended")
+ }
+
+ private fun chargeState(previousDrivingState: Int, dataManager: DataManager) {
+ if (previousDrivingState == DrivingState.DRIVE) pauseTrip(dataManager)
+ if (previousDrivingState != DrivingState.UNKNOWN){
+ startChargingSession(dataManager)
+ dataManager.plotMarkers.addMarker(PlotMarkerType.CHARGE, System.currentTimeMillis(), dataManager.traveledDistance)
+ }
+ else {
+ dataManager.plotMarkers.apply {
+ if (markers.isNotEmpty()) {
+ if (markers.last().MarkerType != PlotMarkerType.CHARGE) {
+ startChargingSession(dataManager)
+ dataManager.plotMarkers.addMarker(PlotMarkerType.CHARGE, System.currentTimeMillis(), dataManager.traveledDistance)
+ }
+ else dataManager.ChargeTime.start()
+ }
+ }
+ }
+ }
+
+ private fun startChargingSession(dataManager: DataManager) {
+ dataManager.chargePlotLine.reset()
+ dataManager.chargeStartDate = Date()
+ dataManager.chargedEnergy = 0F
+ dataManager.ChargeTime.reset()
+ dataManager.ChargeTime.start()
+
+ addChargeDataPoint(PlotLineMarkerType.BEGIN_SESSION, dataManager = dataManager)
+ }
+
+ private fun stopChargingSession(dataManager: DataManager) {
+ if (dataManager == DataManagers.SINCE_CHARGE.dataManager) return
+ refreshProperty(dataManager.CurrentPower.propertyId, dataManager)
+ refreshProperty(dataManager.BatteryLevel.propertyId, dataManager)
+
+ dataManager.ChargeTime.stop()
+
+ addChargeDataPoint(PlotLineMarkerType.END_SESSION, dataManager = dataManager)
+
+ val chargeCurve = ChargeCurve(
+ dataManager.chargePlotLine.getDataPoints(PlotDimension.TIME),
+ dataManager.chargeTime,
+ dataManager.chargedEnergy,
+ dataManager.ambientTemperature,
+ dataManager.chargeStartDate
+ )
+ dataManager.chargeCurves.add(chargeCurve)
+
+ if (dataManager != enumValues().last().dataManager) return
+ InAppLogger.log("Added Charge Curve to SINCE_CHARGE")
+ DataManagers.SINCE_CHARGE.dataManager.chargeCurves.add(chargeCurve)
+ }
+
+ private fun pauseTrip(dataManager: DataManager) {
+ dataManager.TravelTime.stop()
+ val newPlotItem = if (dataManager.consumptionPlotDistanceDelta > 0) dataManager.consumptionPlotEnergyDelta / (dataManager.consumptionPlotDistanceDelta / 1000) else 0F
+ addConsumptionDataPoint(newPlotItem, PlotLineMarkerType.END_SESSION, dataManager)
+ }
+
+ private fun resumeTrip(dataManager: DataManager) {
+ dataManager.TravelTime.start()
+ addConsumptionDataPoint(0F, PlotLineMarkerType.BEGIN_SESSION, dataManager)
+ }
+
+ private fun addChargeDataPoint(plotLineMarkerType: PlotLineMarkerType? = null, dataManager: DataManager) {
+ dataManager.chargePlotLine.addDataPoint(
+ -dataManager.currentPower / 1_000_000,
+ System.currentTimeMillis(),
+ dataManager.CurrentPower.timestamp,
+ dataManager.traveledDistance,
+ dataManager.stateOfCharge.toFloat(),
+ plotLineMarkerType = plotLineMarkerType,
+ autoMarkerTimeDeltaThreshold = CHARGE_PLOT_MARKER_THRESHOLD_NANOS
+ )
+
+ if (dataManager.chargePlotLine.getDataPoints(PlotDimension.TIME).last().Marker == PlotLineMarkerType.BEGIN_SESSION) {
+ val timeSpan = dataManager.chargePlotLine.getDataPoints(PlotDimension.TIME).last().EpochTime - dataManager.chargePlotLine.getDataPoints(PlotDimension.TIME).first().EpochTime
+ dataManager.ChargeTime.reset()
+ dataManager.ChargeTime.restore(timeSpan)
+ dataManager.ChargeTime.start()
+ }
+ }
+
+ private fun addConsumptionDataPoint(item: Float, plotLineMarkerType: PlotLineMarkerType? = null, dataManager: DataManager) {
+ dataManager.consumptionPlotLine.addDataPoint(
+ item,
+ System.currentTimeMillis(),
+ dataManager.CurrentSpeed.timestamp,
+ dataManager.traveledDistance,
+ dataManager.stateOfCharge.toFloat(),
+ plotLineMarkerType = plotLineMarkerType
+ )
+ }
+
+ private fun registerCarPropertyCallbacks() {
+ // InAppLogger.log("DataCollector.registerCarPropertyCallbacks")
+ for (propertyId in DataManager.propertyIds) {
+ carPropertyManager.registerCallback(
+ carPropertyListener,
+ propertyId,
+ DataManager.sensorRateMap[propertyId]?: 0.0F
+ )
+ }
+ }
+
+ private fun refreshProperty(propertyId: Int, dataManager: DataManager) {
+ val property = carPropertyManager.getProperty(propertyId, 0)
+ getPropertyStatus(propertyId)
+ dataManager.update(
+ property.value,
+ property.timestamp,
+ propertyId,
+ doLog = false,
+ allowInvalidTimestamps = DataManager.allowInvalidTimestampsMap[propertyId] == true)
+ }
+
+ private fun getPropertyStatus(propertyId: Int): Int {
+ val status = carPropertyManager.getProperty(propertyId, 0).status
+ if (status != CarPropertyValue.STATUS_AVAILABLE) InAppLogger.log("PropertyStatus: $status")
+ return status
+ }
+
+ private fun createNotificationChannel() {
+ val name = "TestChannel"
+ val descriptionText = "TestChannel"
+ val importance = NotificationManager.IMPORTANCE_DEFAULT
+ val channel = NotificationChannel(CHANNEL_ID, name, importance).apply {
+ description = descriptionText
+ }
+ val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.createNotificationChannel(channel)
+ }
+
+ private fun updateStatsNotification() {
+ if (notificationsEnabled && appPreferences.notifications) {
+ with(NotificationManagerCompat.from(this)) {
+ val averageConsumption = CurrentTripDataManager.usedEnergy / (CurrentTripDataManager.traveledDistance/1000)
+
+ var averageConsumptionString = String.format("%d Wh/km", averageConsumption.toInt())
+ if (!appPreferences.consumptionUnit) {
+ averageConsumptionString = String.format(
+ "%.1f kWh/100km",
+ averageConsumption / 10)
+ }
+ if ((CurrentTripDataManager.traveledDistance <= 0)) averageConsumptionString = "N/A"
+
+ notificationCounter++
+
+ val message = String.format(
+ "P:%.1f kW, D: %.3f km, Ø: %s",
+ CurrentTripDataManager.currentPower / 1_000_000,
+ CurrentTripDataManager.traveledDistance / 1000,
+ averageConsumptionString
+ )
+
+ statsNotification.setContentText(message)
+ foregroundServiceNotification.setContentText(message)
+ notify(STATS_NOTIFICATION_ID, statsNotification.build())
+ notify(FOREGROUND_NOTIFICATION_ID, foregroundServiceNotification.build())
+ }
+ } else if (notificationsEnabled && !appPreferences.notifications) {
+ notificationsEnabled = false
+ with(NotificationManagerCompat.from(this)) {
+ cancel(STATS_NOTIFICATION_ID)
+ }
+ foregroundServiceNotification.setContentText(getString(R.string.foreground_service_info))
+ NotificationManagerCompat.from(this).notify(FOREGROUND_NOTIFICATION_ID, foregroundServiceNotification.build())
+ } else if (!notificationsEnabled && appPreferences.notifications) {
+ notificationsEnabled = true
+ }
+ }
+
+ private fun writeTripDataToFile(tripData: TripData, fileName: String) {
+ val dir = File(applicationContext.filesDir, "TripData")
+ if (!dir.exists()) {
+ dir.mkdir()
+ }
+
+ try {
+ val gpxFile = File(dir, "$fileName.json")
+ val writer = FileWriter(gpxFile)
+ writer.append(gson.toJson(tripData))
+ writer.flush()
+ writer.close()
+ InAppLogger.log("TRIP DATA: Saved $fileName.json")
+ } catch (e: java.lang.Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ private fun readTripDataFromFile(fileName: String): TripData? {
+
+ InAppLogger.log("TRIP DATA: Reading $fileName.json")
+ val startTime = System.currentTimeMillis()
+ val dir = File(applicationContext.filesDir, "TripData")
+ if (!dir.exists()) {
+ InAppLogger.log("TRIP DATA: Directory TripData does not exist!")
+ return null
+ }
+
+ val gpxFile = File(dir, "$fileName.json")
+ if (!gpxFile.exists() && gpxFile.length() > 0) {
+ InAppLogger.log("TRIP_DATA File $fileName.json does not exist!")
+ return null
+ }
+
+ return try {
+ InAppLogger.log("TRIP DATA: File size: %.1f kB".format(gpxFile.length() / 1024f))
+
+ // val fileReader = FileReader(gpxFile)
+ val tripData: TripData = Gson().fromJson(gpxFile.readText(), TripData::class.java)
+ // fileReader.close()
+
+ InAppLogger.log("TRIP DATA: Time to read: ${System.currentTimeMillis() - startTime} ms")
+
+ tripData
+
+ } catch (e: java.lang.Exception) {
+ InAppLogger.log("Error reading File: $e")
+ null
+ }
+ }
+
+ private fun resetAutoTrips(previousDrivingState: Int, newDrivingState: Int, dataManager: DataManager) {
+ // Handle resets on different dataManagers
+ if (DataManagers.CURRENT_MONTH.dataManager == dataManager &&
+ DataManagers.CURRENT_MONTH.doTrack &&
+ newDrivingState == DrivingState.DRIVE) {
+ // Reset if in different Month than start and save old month
+ if (Date().month != DataManagers.CURRENT_MONTH.dataManager.tripStartDate.month) {
+ writeTripDataToFile(
+ DataManagers.CURRENT_MONTH.dataManager.tripData!!,
+ "MonthData_${DataManagers.CURRENT_MONTH.dataManager.tripStartDate}_${DataManagers.CURRENT_MONTH.dataManager.tripStartDate.month + 1}"
+ )
+ DataManagers.CURRENT_MONTH.dataManager.reset()
+ InAppLogger.log("Resetting ${DataManagers.CURRENT_MONTH.dataManager.printableName}")
+ }
+ }
+ if (DataManagers.AUTO_DRIVE.dataManager == dataManager &&
+ DataManagers.AUTO_DRIVE.doTrack &&
+ newDrivingState == DrivingState.DRIVE) {
+ // Reset if parked for x hours
+ if (DataManagers.AUTO_DRIVE.dataManager.plotMarkers.markers.isNotEmpty()) {
+ if (
+ DataManagers.AUTO_DRIVE.dataManager.plotMarkers.markers.last().EndTime == null &&
+ DataManagers.AUTO_DRIVE.dataManager.plotMarkers.markers.last().StartTime < (Date().time - TimeUnit.HOURS.toMillis(AUTO_RESET_TIME_HOURS))
+ ){
+ DataManagers.AUTO_DRIVE.dataManager.reset()
+ InAppLogger.log("Resetting ${DataManagers.AUTO_DRIVE.dataManager.printableName}")
+ }
+ }
+ }
+ if (DataManagers.SINCE_CHARGE.dataManager == dataManager &&
+ DataManagers.SINCE_CHARGE.doTrack &&
+ previousDrivingState == DrivingState.CHARGE) {
+ DataManagers.SINCE_CHARGE.dataManager.reset()
+ InAppLogger.log("Resetting ${DataManagers.SINCE_CHARGE.dataManager.printableName}")
+ }
+ }
+
+ private val ignitionList = listOf(
+ "UNKNOWN", "Lock", "Off", "Accessories", "On", "Start"
+ )
+}
\ No newline at end of file
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/dataManager/DataManager.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/dataManager/DataManager.kt
new file mode 100644
index 00000000..fd79f6c7
--- /dev/null
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/dataManager/DataManager.kt
@@ -0,0 +1,302 @@
+package com.ixam97.carStatsViewer.dataManager
+
+import android.car.VehiclePropertyIds
+import android.car.hardware.CarPropertyValue
+import android.car.hardware.property.CarPropertyManager
+import android.util.Log
+import com.ixam97.carStatsViewer.BuildConfig
+import com.ixam97.carStatsViewer.InAppLogger
+import com.ixam97.carStatsViewer.plot.enums.PlotDimension
+import com.ixam97.carStatsViewer.plot.enums.PlotHighlightMethod
+import com.ixam97.carStatsViewer.plot.enums.PlotLabelPosition
+import com.ixam97.carStatsViewer.plot.enums.PlotLineLabelFormat
+import com.ixam97.carStatsViewer.plot.objects.*
+import java.util.Date
+import kotlin.math.absoluteValue
+
+/**
+ * The DataManager is responsible of holding and managing all data regarding driving and charging
+ * statistics of the vehicle.
+ *
+ * The start of a drive should be triggered by putting the car into drive (therefore putting the
+ * ignition state into "START". When the car is left and locked the ignition state changes to "OFF",
+ * marking the end of a drive. In between those events used energy and time may be tracked.
+ *
+ * A charging session is started by plugging in the charge cable, stopped by unplugging it. Like a
+ * drive, charging time and charged energy are tracked in between these events.
+ */
+class DataManager(val printableName: String) {
+ /** Current speed in m/s */
+ val CurrentSpeed = VehicleProperty("CurrentSpeed", VehiclePropertyIds.PERF_VEHICLE_SPEED)
+ /** Current power in mW */
+ val CurrentPower = VehicleProperty("CurrentPower", VehiclePropertyIds.EV_BATTERY_INSTANTANEOUS_CHARGE_RATE)
+ /** Current gear selection */
+ val CurrentGear = VehicleProperty("CurrentGear", VehiclePropertyIds.GEAR_SELECTION)
+ /** Connection status of the charge port */
+ val ChargePortConnected = VehicleProperty("ChargePortConnected", VehiclePropertyIds.EV_CHARGE_PORT_CONNECTED)
+ /** Battery level in Wh, only usable for calculating the SoC! */
+ val BatteryLevel = VehicleProperty("BatteryLevel", VehiclePropertyIds.EV_BATTERY_LEVEL)
+ /** Ignition state of the vehicle */
+ val CurrentIgnitionState = VehicleProperty("CurrentIgnitionState", VehiclePropertyIds.IGNITION_STATE)
+ /** Current ambientTemperature */
+ val CurrentAmbientTemperature = VehicleProperty("CurrentAmbientTemperature", VehiclePropertyIds.ENV_OUTSIDE_TEMPERATURE)
+
+ /** Travel time in milliseconds */
+ val TravelTime = TimeTracker()
+ /** Charge time in milliseconds */
+ val ChargeTime = TimeTracker()
+ /** Current DrivingState of the car */
+ val DriveState = DrivingState(ChargePortConnected, CurrentIgnitionState)
+
+ // companion object {
+ val TAG = "DataManager"
+ // Easier vehicle property access and implicit values
+ /** Current speed in m/s */
+ val currentSpeed get() = ((CurrentSpeed.value?: 0F) as Float).absoluteValue
+ /** Current power in mW */
+ val currentPower get() = com.ixam97.carStatsViewer.activities.emulatorPowerSign * (CurrentPower.value ?: 0F) as Float
+ /** Current gear selection */
+ val currentGear get() = (CurrentGear.value?: 0) as Int
+ /** Connection status of the charge port */
+ val chargePortConnected get() = (ChargePortConnected.value?: false) as Boolean
+ /** Battery level in Wh, only usable for calculating the SoC! */
+ val batteryLevel get() = (BatteryLevel.value?: 0F) as Float
+ /** Current state of charge */
+ val stateOfCharge: Int get() = ((batteryLevel / maxBatteryLevel) * 100F).toInt()
+ /** Ignition state of the vehicle */
+ val currentIgnitionState get() = (CurrentIgnitionState.value?: 0) as Int
+ /** instantaneous consumption in Wh/m */
+ val instConsumption: Float? get() = (currentPower / currentSpeed).run { if (this.isFinite()) return (this / 3_600_000) else return null }
+ /** Average consumption in Wh/m, null if distance is zero */
+ val avgConsumption: Float? get() = (usedEnergy / traveledDistance).run { if (this.isFinite()) return this else return null }
+ /** Average speed in m/s */
+ val avgSpeed: Float get() = (traveledDistance / (travelTime / 1_000)).run { if (this.isFinite()) return this else return 0F}
+ /** Travel time in milliseconds */
+ val travelTime: Long get() = TravelTime.getTime()
+ /** Charge time in milliseconds */
+ val chargeTime: Long get() = ChargeTime.getTime()
+ /** Current DrivingState of the car */
+ val driveState: Int get() = DriveState.getDriveState()
+ /** Current AmbientTemperature */
+ val ambientTemperature: Float get() = (CurrentAmbientTemperature.value?: 0f) as Float
+
+ // Vehicle statistics
+ /** Max battery level in Wh, only usable for calculating the SoC! */
+ var maxBatteryLevel: Float = 80_400F
+ /** Date on reset */
+ var tripStartDate: Date = Date()
+ /** Used energy in Wh **/
+ var usedEnergy: Float = 0F
+ /** Traveled distance in m */
+ var traveledDistance: Float = 0F
+ /** Date of cable plugging in */
+ var chargeStartDate: Date = Date()
+ /** Used energy in Wh **/
+ var chargedEnergy: Float = 0F
+ /** Contains plot markers indicating when the car is parked or charging */
+ var plotMarkers = PlotMarkers()
+ /** ArrayList of the past charging sessions during the current trip */
+ var chargeCurves: ArrayList = ArrayList()
+
+ // Plot values
+ var consumptionPlotEnergyDelta = 0F
+ var consumptionPlotDistanceDelta = 0F
+ var chargePlotTimeDelta = 0L
+
+ var consumptionPlotLine = PlotLine(
+ PlotLineConfiguration(
+ PlotRange(-300f, 900f, -300f, 900f, 100f, 0f),
+ PlotLineLabelFormat.NUMBER,
+ PlotLabelPosition.LEFT,
+ PlotHighlightMethod.AVG_BY_DISTANCE,
+ "Wh/km"
+ ),
+ )
+
+ var chargePlotLine = PlotLine(
+ PlotLineConfiguration(
+ PlotRange(0f, 20f, 0f, 160f, 20f),
+ PlotLineLabelFormat.FLOAT,
+ PlotLabelPosition.LEFT,
+ PlotHighlightMethod.AVG_BY_TIME,
+ "kW"
+ ),
+ )
+
+ // Updater return values
+ /** Value was updated */
+ val VALID = 0
+ /** Timestamp of new value is invalid (smaller than current timestamp) */
+ val INVALID_TIMESTAMP = 1
+ /** The PropertyID is not implemented in the DataManager */
+ val INVALID_PROPERTY_ID = 2
+ /** The new Value is of an invalid Type */
+ val INVALID_TYPE = 3
+ /** The new value is equal to the last value */
+ val SKIP_SAME_VALUE = 4
+
+ /** Update data manager using a VehiclePropertyValue. Returns VALID when value was changed.
+ * @param value The CarPropertyValue received by the CarPropertyManager.
+ * @param doLog Info about the updated value is printed to the console.
+ * @param valueMustChange If set true only values different from the current values will be accepted.
+ * @param allowInvalidTimestamps If set true the timestamp will be set to the startup timestamp should it be smaller than this.
+ * @return Int representing the success of the update. 0 means a valid update.
+ */
+ fun update(value: CarPropertyValue<*>, doLog: Boolean = false, valueMustChange: Boolean = false, allowInvalidTimestamps: Boolean = false): Int {
+ if (value.status != CarPropertyValue.STATUS_AVAILABLE) InAppLogger.log("PropertyStatus ${getVehiclePropertyById(value.propertyId)?.printableName}: ${value.status}")
+ return update(value.value, value.timestamp, value.propertyId, doLog, valueMustChange, allowInvalidTimestamps)
+ }
+
+ /** Update data manager using a VehiclePropertyValue. Returns VALID when value was changed.
+ * @param value The actual value of the property (Int, Float, Boolean or String).
+ * @param pTimestamp The Timestamp of the new property value in nanoseconds.
+ * @param propertyId: The PropertyID of the property to update.
+ * @param doLog Info about the updated value is printed to the console.
+ * @param valueMustChange If set true only values different from the current values will be accepted.
+ * @param allowInvalidTimestamps If set true the timestamp will be set to the startup timestamp should it be smaller than this.
+ * @return Int representing the success of the update. 0 means a valid update.
+ */
+ fun update(value: Any?, pTimestamp: Long, propertyId: Int, doLog: Boolean = false, valueMustChange: Boolean = false, allowInvalidTimestamps: Boolean = false): Int {
+ var timestamp = pTimestamp
+ if (!propertiesMap.containsKey(propertyId)) {
+ if (doLog) Log.w(TAG, "${timestamp}: Failed to update property ID ${propertyId}: Invalid property ID")
+ return INVALID_PROPERTY_ID
+ }
+ val property: VehicleProperty = propertiesMap[propertyId]!!
+ if (value !is Boolean? && value !is Float? && value !is Int? && value !is String?){
+ if (doLog) Log.w(TAG, "${timestamp}: Failed to update ${property.printableName}: Invalid data type")
+ return INVALID_TYPE
+ }
+ // if (allowInvalidTimestamps && timestamp < property.timestamp){
+ // //timestamp = property.startupTimestamp
+ // timestamp = System.nanoTime()
+ // }
+ if (!allowInvalidTimestamps && timestamp < property.timestamp) {
+ if (doLog) Log.w(TAG, "${timestamp}: Failed to update ${property.printableName}: Invalid timestamp")
+ return INVALID_TIMESTAMP
+ }
+ if (property.value == value && valueMustChange) {
+ // if (doLog) Log.i(TAG, "Skipped update for ${property.printableName}: Value not changed")
+ return SKIP_SAME_VALUE
+ }
+ property.value = value
+ property.timestamp = timestamp
+ if (doLog) Log.i(TAG, "${property.timestamp}: Updated ${property.printableName}, value=${property.value}, valueDelta=${property.valueDelta}, timeDelta=${property.timeDelta}")
+ return VALID
+ }
+
+
+ /** Get tripData for summary or saving. Set tripData to load current trip into DataManager */
+ var tripData: TripData?
+ get() = TripData(
+ appVersion = BuildConfig.VERSION_NAME,
+ dataVersion = PlotGlobalConfiguration.DataVersion,
+ tripStartDate = tripStartDate,
+ usedEnergy = usedEnergy,
+ traveledDistance = traveledDistance,
+ travelTime = travelTime,
+ chargeStartDate = chargeStartDate,
+ chargedEnergy = chargedEnergy,
+ chargeTime = chargeTime,
+ markers = plotMarkers.markers.toList(),
+ chargeCurves = chargeCurves.toList(),
+ consumptionPlotLine = consumptionPlotLine.getDataPoints(PlotDimension.DISTANCE).toList(),
+ chargePlotLine = chargePlotLine.getDataPoints(PlotDimension.TIME).toList()
+ )
+ set(value) {
+ tripStartDate = value?.tripStartDate?: Date()
+ chargeStartDate = value?.chargeStartDate?: Date()
+ traveledDistance = value?.traveledDistance?: 0F
+ usedEnergy = value?.usedEnergy?: 0F
+ TravelTime.restore(value?.travelTime?: 0L)
+ chargedEnergy = value?.chargedEnergy?: 0F
+ ChargeTime.restore(value?.chargeTime?: 0L)
+ plotMarkers.reset()
+ chargeCurves = ArrayList()
+ consumptionPlotLine.reset()
+ chargePlotLine.reset()
+
+ if (value?.dataVersion != null) {
+ if(value.chargeCurves.isNotEmpty()) {
+ for (curve in value.chargeCurves) {
+ chargeCurves.add(curve)
+ }
+ }
+ plotMarkers.addMarkers(value.markers?: emptyList())
+ consumptionPlotLine.addDataPoints(value.consumptionPlotLine?: emptyList())
+ chargePlotLine.addDataPoints(value.chargePlotLine?: emptyList())
+ } else {
+ TravelTime.reset()
+ ChargeTime.reset()
+ when (driveState) {
+ DrivingState.DRIVE -> TravelTime.start()
+ DrivingState.CHARGE -> ChargeTime.start()
+ }
+ }
+ }
+
+ /** Returns a list of all propertyIDs contained within the DataManager */
+ fun getVehiclePropertyIds(): List {
+ var idArrayList: ArrayList = arrayListOf()
+ for (property in propertiesMap) {
+ idArrayList.add(property.key)
+ }
+ return idArrayList.toList()
+ }
+
+ /**
+ * Returns the VehicleProperty from the DataManager corresponding to the given PropertyID.
+ * Null if VehicleProperty is not contained in DataManager.
+ */
+ fun getVehiclePropertyById(propertyId: Int): VehicleProperty? {
+ if (!propertiesMap.containsKey(propertyId)) return null
+ return propertiesMap[propertyId]
+ }
+
+ /** Resets the DataManager to default values */
+ fun reset() {
+ tripData = null
+ }
+
+ private val propertiesMap: Map = mapOf(
+ CurrentSpeed.propertyId to CurrentSpeed,
+ CurrentPower.propertyId to CurrentPower,
+ CurrentGear.propertyId to CurrentGear,
+ ChargePortConnected.propertyId to ChargePortConnected,
+ BatteryLevel.propertyId to BatteryLevel,
+ CurrentIgnitionState.propertyId to CurrentIgnitionState,
+ CurrentAmbientTemperature.propertyId to CurrentAmbientTemperature
+ )
+
+ companion object {
+ val propertyIds = listOf(
+ VehiclePropertyIds.PERF_VEHICLE_SPEED,
+ VehiclePropertyIds.EV_BATTERY_INSTANTANEOUS_CHARGE_RATE,
+ VehiclePropertyIds.GEAR_SELECTION,
+ VehiclePropertyIds.EV_CHARGE_PORT_CONNECTED,
+ VehiclePropertyIds.EV_BATTERY_LEVEL,
+ VehiclePropertyIds.IGNITION_STATE,
+ VehiclePropertyIds.ENV_OUTSIDE_TEMPERATURE
+ )
+
+ val sensorRateMap: Map = mapOf(
+ VehiclePropertyIds.PERF_VEHICLE_SPEED to CarPropertyManager.SENSOR_RATE_ONCHANGE,
+ VehiclePropertyIds.EV_BATTERY_INSTANTANEOUS_CHARGE_RATE to CarPropertyManager.SENSOR_RATE_ONCHANGE,
+ VehiclePropertyIds.GEAR_SELECTION to CarPropertyManager.SENSOR_RATE_ONCHANGE,
+ VehiclePropertyIds.EV_CHARGE_PORT_CONNECTED to CarPropertyManager.SENSOR_RATE_FAST,
+ VehiclePropertyIds.EV_BATTERY_LEVEL to CarPropertyManager.SENSOR_RATE_FAST,
+ VehiclePropertyIds.IGNITION_STATE to CarPropertyManager.SENSOR_RATE_FAST,
+ VehiclePropertyIds.ENV_OUTSIDE_TEMPERATURE to CarPropertyManager.SENSOR_RATE_ONCHANGE
+ )
+
+ val allowInvalidTimestampsMap: Map = mapOf(
+ VehiclePropertyIds.PERF_VEHICLE_SPEED to false,
+ VehiclePropertyIds.EV_BATTERY_INSTANTANEOUS_CHARGE_RATE to false,
+ VehiclePropertyIds.GEAR_SELECTION to true,
+ VehiclePropertyIds.EV_CHARGE_PORT_CONNECTED to true,
+ VehiclePropertyIds.EV_BATTERY_LEVEL to true,
+ VehiclePropertyIds.IGNITION_STATE to true,
+ VehiclePropertyIds.ENV_OUTSIDE_TEMPERATURE to true
+ )
+ }
+}
\ No newline at end of file
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/dataManager/DataManagers.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/dataManager/DataManagers.kt
new file mode 100644
index 00000000..2c1e53c5
--- /dev/null
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/dataManager/DataManagers.kt
@@ -0,0 +1,8 @@
+package com.ixam97.carStatsViewer.dataManager
+
+enum class DataManagers(var doTrack: Boolean = true, val dataManager: DataManager) {
+ CURRENT_TRIP(dataManager = DataManager("CurrentTripData")),
+ SINCE_CHARGE(dataManager = DataManager("SinceChargeData")),
+ AUTO_DRIVE(dataManager = DataManager("AutoTripData")),
+ CURRENT_MONTH(dataManager = DataManager("CurrentMonthData"))
+}
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/dataManager/DrivingState.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/dataManager/DrivingState.kt
new file mode 100644
index 00000000..5ff0a1f5
--- /dev/null
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/dataManager/DrivingState.kt
@@ -0,0 +1,41 @@
+package com.ixam97.carStatsViewer.dataManager
+
+import android.car.VehicleIgnitionState
+
+class DrivingState(private val ChargePortConnected: VehicleProperty, private val CurrentIgnitionState: VehicleProperty) {
+ // States
+ companion object {
+ val UNKNOWN = -1
+ val PARKED = 0
+ val DRIVE = 1
+ val CHARGE = 2
+
+ val nameMap = mapOf(
+ UNKNOWN to "UNKNOWN",
+ PARKED to "PARKED",
+ DRIVE to "DRIVE",
+ CHARGE to "CHARGE"
+ )
+ }
+
+ var lastDriveState: Int = UNKNOWN
+
+ /** Check weather the DrivingState has changed since the last time this function has been called. */
+ fun hasChanged(): Boolean {
+ if (getDriveState() != lastDriveState) {
+ lastDriveState = getDriveState()
+ return true
+ }
+ return false
+ }
+
+ /** Get the current DrivingState independent of hasChanged(). */
+ fun getDriveState(): Int {
+ val chargePortConnected = (ChargePortConnected.value ?: false) as Boolean
+ val currentIgnitionState = (CurrentIgnitionState.value ?: 0) as Int
+ return if (chargePortConnected) CHARGE
+ else if (currentIgnitionState == VehicleIgnitionState.START) DRIVE
+ else if (currentIgnitionState != VehicleIgnitionState.UNDEFINED) PARKED
+ else UNKNOWN
+ }
+}
\ No newline at end of file
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/dataManager/EmulatorIntentExtras.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/dataManager/EmulatorIntentExtras.kt
new file mode 100644
index 00000000..c035457a
--- /dev/null
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/dataManager/EmulatorIntentExtras.kt
@@ -0,0 +1,13 @@
+package com.ixam97.carStatsViewer.dataManager
+
+sealed class EmulatorIntentExtras {
+ companion object {
+ const val PROPERTY_ID = "propertyId"
+ const val TYPE = "valueType"
+ const val VALUE = "value"
+ const val TYPE_FLOAT = "Float"
+ const val TYPE_INT = "Int"
+ const val TYPE_BOOLEAN = "Boolean"
+ const val TYPE_STRING = "String"
+ }
+}
\ No newline at end of file
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/dataManager/TimeTracker.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/dataManager/TimeTracker.kt
new file mode 100644
index 00000000..afd48d3f
--- /dev/null
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/dataManager/TimeTracker.kt
@@ -0,0 +1,40 @@
+package com.ixam97.carStatsViewer.dataManager
+
+import java.util.*
+
+/**
+ * The TimeTracker is able to track a time span based on the Date-object.
+ */
+class TimeTracker(val printableName: String = "", val doLog: Boolean = false) {
+ private var startDate: Date? = null
+ private var timeSpan: Long = 0
+
+ /** Start or resume time tracking */
+ fun start() {
+ if (startDate == null) startDate = Date()
+ }
+
+ /** Stop time tracking */
+ fun stop() {
+ startDate.let {
+ if (it != null) timeSpan += (Date().time - it.time)
+ }
+ startDate = null
+ }
+
+ /** Reset tracked time to zero */
+ fun reset() {
+ startDate = null
+ timeSpan = 0L
+ }
+
+ /** Restore tracked time to defined base value */
+ fun restore(pTimeSpan: Long) {
+ timeSpan = pTimeSpan
+ }
+
+ /** returns the current tracked time in milliseconds */
+ fun getTime(): Long {
+ return timeSpan + (Date().time - (startDate?.time?: Date().time))
+ }
+}
\ No newline at end of file
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/objects/TripData.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/dataManager/TripData.kt
similarity index 51%
rename from automotive/src/main/java/com/ixam97/carStatsViewer/objects/TripData.kt
rename to automotive/src/main/java/com/ixam97/carStatsViewer/dataManager/TripData.kt
index d0217bc4..c5f3c77c 100644
--- a/automotive/src/main/java/com/ixam97/carStatsViewer/objects/TripData.kt
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/dataManager/TripData.kt
@@ -1,26 +1,21 @@
-package com.ixam97.carStatsViewer.objects
+package com.ixam97.carStatsViewer.dataManager
import com.ixam97.carStatsViewer.plot.objects.PlotLineItem
-import com.ixam97.carStatsViewer.plot.enums.PlotLineMarkerType
import com.ixam97.carStatsViewer.plot.objects.PlotMarker
import java.util.*
data class TripData(
var appVersion: String,
+ var dataVersion: Int?,
var tripStartDate: Date,
- var traveledDistance: Float,
+ var chargeStartDate: Date,
var usedEnergy: Float,
- var averageConsumption: Float,
- var travelTimeMillis: Long,
- var lastPlotDistance: Float,
- var lastPlotEnergy: Float,
- var lastPlotTime: Long,
- var lastPlotGear: Int,
- var lastPlotMarker: PlotLineMarkerType?,
- var lastChargePower:Float,
+ var traveledDistance: Float,
+ var travelTime: Long,
+ var chargedEnergy: Float,
+ var chargeTime: Long,
var consumptionPlotLine: List,
+ var chargePlotLine: List,
var chargeCurves: List,
var markers: List
-) {
-
-}
+)
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/dataManager/VehicleProperty.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/dataManager/VehicleProperty.kt
new file mode 100644
index 00000000..ad646506
--- /dev/null
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/dataManager/VehicleProperty.kt
@@ -0,0 +1,45 @@
+package com.ixam97.carStatsViewer.dataManager
+
+class VehicleProperty(val printableName: String, val propertyId: Int) {
+ internal val startupTimestamp = System.nanoTime()
+
+ /** Value of the Vehicle */
+ var value: Any? = null
+ internal set(value) {
+ lastValue = field
+ field = value
+ }
+
+ var lastValue: Any? = null
+ internal set
+
+ /** Timestamp of the last time the value was changed in nanoseconds */
+ var timestamp: Long = startupTimestamp
+ internal set(value) {
+ lastTimestamp = field
+ field = value
+ }
+
+ internal var lastTimestamp: Long = startupTimestamp
+
+ /** Time difference between value changes in nanoseconds */
+ val timeDelta: Long get() {
+ if (lastTimestamp == 0L || lastTimestamp < startupTimestamp) return 0L
+ return timestamp - lastTimestamp
+ }
+
+ /** Returns true if the value difference is null, therefore the value is the initial value */
+ val isInitialValue: Boolean get() = (valueDelta == null)
+
+ /** Value difference since last value change */
+ val valueDelta: Any?
+ get() {
+ value.let {
+ if (it == null || lastValue == null) return null
+ if (it is Float) return (it - lastValue as Float)
+ if (it is Int) return (it - lastValue as Int)
+ if (it is Boolean) return !(lastValue as Boolean)
+ }
+ return null
+ }
+}
\ No newline at end of file
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/enums/DistanceUnitEnum.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/enums/DistanceUnitEnum.kt
new file mode 100644
index 00000000..90b3176f
--- /dev/null
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/enums/DistanceUnitEnum.kt
@@ -0,0 +1,70 @@
+package com.ixam97.carStatsViewer.enums
+
+import kotlin.math.roundToLong
+
+enum class DistanceUnitEnum {
+ KM, MILES;
+
+ fun toFactor(): Float {
+ return when (this) {
+ KM -> 1.0f
+ else -> 1f / asFactor()
+ }
+ }
+
+ fun asFactor(): Float {
+ return when (this) {
+ KM -> 1.0f
+ else -> 1.60934f
+ }
+ }
+
+ fun toUnit(value: Float): Float {
+ return when (this) {
+ KM -> value
+ else -> value * this.toFactor()
+ }
+ }
+
+ fun asUnit(value: Float): Float {
+ return when (this) {
+ KM -> value
+ else -> value / this.toFactor()
+ }
+ }
+
+ fun toUnit(value: Double): Double {
+ return when (this) {
+ KM -> value
+ else -> value * this.toFactor()
+ }
+ }
+
+ fun asUnit(value: Double): Double {
+ return when (this) {
+ KM -> value
+ else -> value / this.toFactor()
+ }
+ }
+
+ fun toUnit(value: Long): Long {
+ return when (this) {
+ KM -> value
+ else -> (value * this.toFactor()).roundToLong()
+ }
+ }
+
+ fun asUnit(value: Long): Long {
+ return when (this) {
+ KM -> value
+ else -> (value / this.toFactor()).roundToLong()
+ }
+ }
+
+ fun unit(): String {
+ return when (this) {
+ KM -> "km"
+ else -> "mi"
+ }
+ }
+}
\ No newline at end of file
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/mailSender/JSSEProvider.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/mailSender/JSSEProvider.kt
new file mode 100644
index 00000000..e6e909f9
--- /dev/null
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/mailSender/JSSEProvider.kt
@@ -0,0 +1,23 @@
+package com.ixam97.carStatsViewer.mailSender
+
+import java.security.AccessController
+import java.security.PrivilegedAction
+import java.security.Provider
+
+class JSSEProvider : Provider("HarmonyJSSE", 1.0, "Harmony JSSE Provider") {
+ init {
+ AccessController.doPrivileged(PrivilegedAction {
+ put("SSLContext.TLS", "org.apache.harmony.xnet.provider.jsse.SSLContextImpl")
+ put("Alg.Alias.SSLContext.TLSv1", "TLS")
+ put(
+ "KeyManagerFactory.X509",
+ "org.apache.harmony.xnet.provider.jsse.KeyManagerFactoryImpl"
+ )
+ put(
+ "TrustManagerFactory.X509",
+ "org.apache.harmony.xnet.provider.jsse.TrustManagerFactoryImpl"
+ )
+ null
+ })
+ }
+}
\ No newline at end of file
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/mailSender/MailSender.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/mailSender/MailSender.kt
new file mode 100644
index 00000000..f0e9c847
--- /dev/null
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/mailSender/MailSender.kt
@@ -0,0 +1,109 @@
+package com.ixam97.carStatsViewer.mailSender
+
+import kotlin.jvm.Synchronized
+import kotlin.Throws
+import com.ixam97.carStatsViewer.mailSender.JSSEProvider
+import jakarta.activation.DataHandler
+import jakarta.activation.DataSource
+import jakarta.activation.FileDataSource
+import jakarta.mail.*
+import jakarta.mail.internet.InternetAddress
+import jakarta.mail.internet.MimeBodyPart
+import jakarta.mail.internet.MimeMessage
+import jakarta.mail.internet.MimeMultipart
+import java.io.*
+import java.lang.Exception
+import java.security.Security
+import java.util.*
+
+class MailSender(private val user: String, private val password: String, private val server: String) : Authenticator() {
+ private val session: Session
+ private val _multipart: Multipart = MimeMultipart()
+
+ init {
+ val props = Properties()
+ props.setProperty("mail.transport.protocol", "smtp")
+ props.setProperty("mail.host", server)
+ props["mail.smtp.auth"] = "true"
+ props["mail.smtp.port"] = "465"
+ props["mail.smtp.socketFactory.port"] = "465"
+ props["mail.smtp.socketFactory.class"] = "javax.net.ssl.SSLSocketFactory"
+ props["mail.smtp.socketFactory.fallback"] = "false"
+ props.setProperty("mail.smtp.quitwait", "false")
+ session = Session.getDefaultInstance(props, this)
+ }
+
+ override fun getPasswordAuthentication(): PasswordAuthentication {
+ return PasswordAuthentication(user, password)
+ }
+
+ @Synchronized
+ fun sendMail(subject: String?, body: String, sender: String?, recipients: String) {
+ val message = MimeMessage(session)
+ val handler = DataHandler(ByteArrayDataSource(body.toByteArray(), "text/plain"))
+ message.sender = InternetAddress(sender)
+ message.subject = subject
+ message.dataHandler = handler
+ val messageBodyPart: BodyPart = MimeBodyPart()
+ messageBodyPart.setText(body)
+ _multipart.addBodyPart(messageBodyPart)
+ message.setContent(_multipart)
+ if (recipients.indexOf(',') > 0) message.setRecipients(
+ Message.RecipientType.TO,
+ InternetAddress.parse(recipients)
+ ) else message.setRecipient(
+ Message.RecipientType.TO, InternetAddress(recipients)
+ )
+ Transport.send(message)
+ }
+
+ fun addAttachment(file: File) {
+ val messageBodyPart: BodyPart = MimeBodyPart()
+ val source: DataSource = FileDataSource(file)
+ messageBodyPart.dataHandler = DataHandler(source)
+ messageBodyPart.fileName = file.name
+ _multipart.addBodyPart(messageBodyPart)
+ }
+
+ inner class ByteArrayDataSource : DataSource {
+ private var data: ByteArray
+ private var type: String? = null
+
+ constructor(data: ByteArray, type: String?) : super() {
+ this.data = data
+ this.type = type
+ }
+
+ constructor(data: ByteArray) : super() {
+ this.data = data
+ }
+
+ fun setType(type: String?) {
+ this.type = type
+ }
+
+ override fun getContentType(): String {
+ return this.type?: "application/octet-stream"
+ }
+
+ @Throws(IOException::class)
+ override fun getInputStream(): InputStream {
+ return ByteArrayInputStream(data)
+ }
+
+ override fun getName(): String {
+ return "ByteArrayDataSource"
+ }
+
+ @Throws(IOException::class)
+ override fun getOutputStream(): OutputStream {
+ throw IOException("Not Supported")
+ }
+ }
+
+ companion object {
+ init {
+ Security.addProvider(JSSEProvider())
+ }
+ }
+}
\ No newline at end of file
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/objects/AppPreferences.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/objects/AppPreferences.kt
deleted file mode 100644
index faafaa3c..00000000
--- a/automotive/src/main/java/com/ixam97/carStatsViewer/objects/AppPreferences.kt
+++ /dev/null
@@ -1,193 +0,0 @@
-package com.ixam97.carStatsViewer.objects
-
-import android.content.Context
-import android.content.SharedPreferences
-import com.ixam97.carStatsViewer.R
-import com.ixam97.carStatsViewer.plot.enums.PlotDimension
-
-class AppPreferences(context: Context) {
-
- private var sharedPref: SharedPreferences
-
- init {
- sharedPref = context.getSharedPreferences(
- context.getString(R.string.preferences_file_key), Context.MODE_PRIVATE
- )
- }
-
- var debug: Boolean
- get() {
- return getPreference(AppPreference.DEBUG) as Boolean
- }
- set(value) {
- setPreference(AppPreference.DEBUG, value)
- }
- var notifications: Boolean
- get() {
- return getPreference(AppPreference.NOTIFICATIONS) as Boolean
- }
- set(value) {
- setPreference(AppPreference.NOTIFICATIONS, value)
- }
- var consumptionUnit: Boolean
- get() {
- return getPreference(AppPreference.CONSUMPTION_UNIT) as Boolean
- }
- set(value) {
- setPreference(AppPreference.CONSUMPTION_UNIT, value)
- }
- var experimentalLayout: Boolean
- get() {
- return getPreference(AppPreference.EXPERIMENTAL_LAYOUT) as Boolean
- }
- set(value) {
- setPreference(AppPreference.EXPERIMENTAL_LAYOUT, value)
- }
- var deepLog: Boolean
- get() {
- return getPreference(AppPreference.DEEP_LOG) as Boolean
- }
- set(value) {
- setPreference(AppPreference.DEEP_LOG, value)
- }
- var plotSpeed: Boolean
- get() {
- return getPreference(AppPreference.PLOT_SHOW_SPEED) as Boolean
- }
- set(value) {
- setPreference(AppPreference.PLOT_SHOW_SPEED, value)
- }
- var plotDistance: Int
- get() {
- return getPreference(AppPreference.PLOT_DISTANCE) as Int
- }
- set(value) {
- setPreference(AppPreference.PLOT_DISTANCE, value)
- }
- var consumptionPlotSingleMotor: Boolean
- get() {
- return getPreference(AppPreference.CONSUMPTION_PLOT_SINGLE_MOTOR) as Boolean
- }
- set(value) {
- setPreference(AppPreference.CONSUMPTION_PLOT_SINGLE_MOTOR, value)
- }
- var consumptionPlotSecondaryColor: Boolean
- get() {
- return getPreference(AppPreference.CONSUMPTION_PLOT_SECONDARY_COLOR) as Boolean
- }
- set(value) {
- setPreference(AppPreference.CONSUMPTION_PLOT_SECONDARY_COLOR, value)
- }
- var consumptionPlotVisibleGages: Boolean
- get() {
- return getPreference(AppPreference.CONSUMPTION_PLOT_VISIBLE_GAGES) as Boolean
- }
- set(value) {
- setPreference(AppPreference.CONSUMPTION_PLOT_VISIBLE_GAGES, value)
- }
- var chargePlotSecondaryColor: Boolean
- get() {
- return getPreference(AppPreference.CHARGE_PLOT_SECONDARY_COLOR) as Boolean
- }
- set(value) {
- setPreference(AppPreference.CHARGE_PLOT_SECONDARY_COLOR, value)
- }
- var chargePlotVisibleGages: Boolean
- get() {
- return getPreference(AppPreference.CHARGE_PLOT_VISIBLE_GAGES) as Boolean
- }
- set(value) {
- setPreference(AppPreference.CHARGE_PLOT_VISIBLE_GAGES, value)
- }
-
- var chargePlotDimension: PlotDimension
- get() {
- return getPreference(AppPreference.CHARGE_PLOT_DIMENSION) as PlotDimension
- }
- set(value) {
- setPreference(AppPreference.CHARGE_PLOT_DIMENSION, value)
- }
-
-
- private val keyMap = hashMapOf(
- AppPreference.DEBUG to context.getString(R.string.preferences_debug_key),
- AppPreference.NOTIFICATIONS to context.getString(R.string.preferences_notifications_key),
- AppPreference.CONSUMPTION_UNIT to context.getString(R.string.preferences_consumption_unit_key),
- AppPreference.EXPERIMENTAL_LAYOUT to context.getString(R.string.preferences_experimental_layout_key),
- AppPreference.DEEP_LOG to context.getString(R.string.preferences_deep_log_key),
- AppPreference.PLOT_SHOW_SPEED to context.getString(R.string.preferences_plot_speed_key),
- AppPreference.PLOT_DISTANCE to context.getString(R.string.preferences_plot_distance_key),
- AppPreference.CONSUMPTION_PLOT_SINGLE_MOTOR to context.getString(R.string.preferences_consumption_plot_single_motor_key),
- AppPreference.CONSUMPTION_PLOT_SECONDARY_COLOR to context.getString(R.string.preference_consumption_plot_secondary_color_key),
- AppPreference.CONSUMPTION_PLOT_VISIBLE_GAGES to context.getString(R.string.preference_consumption_plot_visible_gages_key),
- AppPreference.CHARGE_PLOT_SECONDARY_COLOR to context.getString(R.string.preference_charge_plot_secondary_color_key),
- AppPreference.CHARGE_PLOT_VISIBLE_GAGES to context.getString(R.string.preference_charge_plot_visible_gages_key),
- AppPreference.CHARGE_PLOT_DIMENSION to context.getString(R.string.preference_charge_plot_dimension_key)
- )
-
- private var typeMap = mapOf( // Also contains default values
- AppPreference.DEBUG to false,
- AppPreference.NOTIFICATIONS to false,
- AppPreference.CONSUMPTION_UNIT to false,
- AppPreference.EXPERIMENTAL_LAYOUT to false,
- AppPreference.DEEP_LOG to false,
- AppPreference.PLOT_SHOW_SPEED to false,
- AppPreference.PLOT_DISTANCE to 1,
- AppPreference.CONSUMPTION_PLOT_SINGLE_MOTOR to false,
- AppPreference.CONSUMPTION_PLOT_SECONDARY_COLOR to false,
- AppPreference.CONSUMPTION_PLOT_VISIBLE_GAGES to true,
- AppPreference.CHARGE_PLOT_SECONDARY_COLOR to false,
- AppPreference.CHARGE_PLOT_VISIBLE_GAGES to true,
- AppPreference.CHARGE_PLOT_DIMENSION to PlotDimension.TIME
- )
-
- private fun getPreference(appPreference: AppPreference): Any {
- if (typeMap.containsKey(appPreference)) {
- if (typeMap[appPreference] is Boolean) {
- return sharedPref.getBoolean(keyMap[appPreference], typeMap[appPreference] as Boolean)
- }
- if (typeMap[appPreference] is Int) {
- return sharedPref.getInt(keyMap[appPreference], typeMap[appPreference] as Int)
- }
- if (typeMap[appPreference] is PlotDimension) {
- return PlotDimension.valueOf(sharedPref.getString(keyMap[appPreference], (typeMap[appPreference] as PlotDimension).name) ?: PlotDimension.TIME.name)
- }
- }
- throw java.lang.Exception("AppPreferences.setPreference: Unknown Preference!")
- }
-
- private fun setPreference(appPreference: AppPreference, value: Any) {
- if (typeMap.containsKey(appPreference)) {
- if (typeMap[appPreference] is Boolean && value is Boolean) {
- sharedPref.edit().putBoolean(keyMap[appPreference], value).apply()
- return
- }
- if (typeMap[appPreference] is Int && value is Int) {
- sharedPref.edit().putInt(keyMap[appPreference], value).apply()
- return
- }
- if (typeMap[appPreference] is PlotDimension && value is PlotDimension) {
- sharedPref.edit().putString(keyMap[appPreference], value.name).apply()
- return
- }
- throw java.lang.Exception("AppPreferences.setPreference: Unsupported type!")
- }
- throw java.lang.Exception("AppPreferences.setPreference: Unknown Preference!")
- }
-}
-
-enum class AppPreference {
- DEBUG,
- NOTIFICATIONS,
- CONSUMPTION_UNIT,
- EXPERIMENTAL_LAYOUT,
- DEEP_LOG,
- PLOT_SHOW_SPEED,
- PLOT_DISTANCE,
- CONSUMPTION_PLOT_SINGLE_MOTOR,
- CONSUMPTION_PLOT_SECONDARY_COLOR,
- CONSUMPTION_PLOT_VISIBLE_GAGES,
- CHARGE_PLOT_SECONDARY_COLOR,
- CHARGE_PLOT_VISIBLE_GAGES,
- CHARGE_PLOT_DIMENSION
-}
\ No newline at end of file
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/objects/ChargeCurve.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/objects/ChargeCurve.kt
deleted file mode 100644
index d799cf94..00000000
--- a/automotive/src/main/java/com/ixam97/carStatsViewer/objects/ChargeCurve.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.ixam97.carStatsViewer.objects
-
-import com.ixam97.carStatsViewer.plot.objects.PlotLineItem
-
-data class ChargeCurve(
- var chargePlotLine: List,
- var stateOfChargePlotLine: List?,
- var chargeTime: Long,
- var chargedEnergyWh: Float,
- var maxChargeRatemW: Float,
- var avgChargeRatemW: Float
-) {}
\ No newline at end of file
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/objects/DataHolder.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/objects/DataHolder.kt
deleted file mode 100644
index ed99d948..00000000
--- a/automotive/src/main/java/com/ixam97/carStatsViewer/objects/DataHolder.kt
+++ /dev/null
@@ -1,205 +0,0 @@
-package com.ixam97.carStatsViewer.objects
-
-import android.car.VehicleGear
-import com.ixam97.carStatsViewer.BuildConfig
-import com.ixam97.carStatsViewer.InAppLogger
-import com.ixam97.carStatsViewer.plot.enums.*
-import com.ixam97.carStatsViewer.plot.objects.*
-import java.util.*
-import kotlin.collections.ArrayList
-
-object DataHolder {
-
- private const val maxSmoothSize = 20
- private const val FTC = 10f
-
- var currentGear: Int = VehicleGear.GEAR_PARK
-
- var currentPowermW = 0F
- set(value) {
- lastPowermW = field
- field = value
-
- currentPowerSmooth += ((1f / FTC) * (value - currentPowerSmooth))
- // currentPowerSmoothArray.add(value)
- // if (currentPowerSmoothArray.size > maxSmoothSize) currentPowerSmoothArray.removeAt(0)
- }
-
- private var currentPowerSmoothArray = arrayListOf()
- var currentPowerSmooth = 0f
- //get() {
- // return currentPowerSmooth // Array.average().toFloat()
- //}
- private set
-
- var lastPowermW = 0F
- private set
-
- var currentSpeed = 0F
- set(value) {
- lastSpeed = field
- field = value
-
- currentSpeedSmooth += ((1f / FTC) * (value - currentSpeedSmooth))
- // currentSpeedSmoothArray.add(value)
- // if (currentSpeedSmoothArray.size > maxSmoothSize) currentSpeedSmoothArray.removeAt(0)
- }
- private var currentSpeedSmoothArray = arrayListOf()
- var currentSpeedSmooth = 0f
- //get() {
- // return currentSpeedSmooth // Array.average().toFloat()
- //}
- private set
-
- var avgSpeed = 0F
-
- var lastSpeed = 0F
- private set
-
- var currentBatteryCapacity = 0f
- set(value) {
- lastBatteryCapacity = field
- field = value
- }
-
- var lastBatteryCapacity = 0f
- private set
-
- var maxBatteryCapacity = 0f
-
- var tripStartDate = Date()
- var traveledDistance = 0F
- var usedEnergy = 0F
- var chargedEnergy = 0F
- var averageConsumption = 0F
- var chargePortConnected = false
- var travelTimeMillis = 0L
- var chargeTimeMillis = 0L
-
- var lastPlotDistance = 0F
- var lastPlotEnergy = 0F
- var lastPlotTime = 0L
- var lastPlotGear = VehicleGear.GEAR_PARK
- var lastPlotMarker : PlotLineMarkerType? = null
- var lastChargePower = 0f
-
- var plotMarkers = PlotMarkers()
-
- var consumptionPlotLine = PlotLine(
- PlotLineConfiguration(
- PlotRange(-300f, 900f, -300f, 900f, 100f, 0f),
- PlotLineLabelFormat.NUMBER,
- PlotLabelPosition.LEFT,
- PlotHighlightMethod.AVG_BY_DISTANCE,
- "Wh/km"
- ),
- )
-
- var chargePlotLine = PlotLine(
- PlotLineConfiguration(
- PlotRange(0f, 20f, 0f, 160f, 20f),
- PlotLineLabelFormat.NUMBER,
- PlotLabelPosition.LEFT,
- PlotHighlightMethod.AVG_BY_TIME,
- "kW"
- ),
- )
-
- var chargeCurves: ArrayList = ArrayList()
-
- fun stateOfCharge(): Float {
- return 100f / maxBatteryCapacity * currentBatteryCapacity
- }
-
- fun applyTripData(tripData: TripData) {
- if (tripData.appVersion != BuildConfig.VERSION_NAME) {
- InAppLogger.log("File saved with older app version, trying to convert ...")
- }
-
- traveledDistance = tripData.traveledDistance ?: 0f
- tripStartDate = tripData.tripStartDate ?: Date()
- usedEnergy = tripData.usedEnergy ?: 0f
- averageConsumption = tripData.averageConsumption ?: 0f
- travelTimeMillis = tripData.travelTimeMillis ?: 0L
- lastPlotDistance = tripData.lastPlotDistance ?: 0F
- lastPlotEnergy = tripData.lastPlotEnergy ?: 0F
- lastPlotTime = tripData.lastPlotTime ?: 0L
- lastPlotGear = tripData.lastPlotGear ?: VehicleGear.GEAR_PARK
- lastPlotMarker = tripData.lastPlotMarker
- lastChargePower = tripData.lastChargePower ?: 0F
- consumptionPlotLine.reset()
- chargePlotLine.reset()
-
- if (tripData.consumptionPlotLine?.isNotEmpty() == true) {
- consumptionPlotLine.addDataPoints(tripData.consumptionPlotLine)
- }
-
- chargeCurves = ArrayList()
- if (tripData.chargeCurves?.isNotEmpty() == true) {
- // move StateOfCharge PlotLine to charge PlotLine STateOfCharge Value
- for (curve in tripData.chargeCurves) {
- if (curve.stateOfChargePlotLine?.isNotEmpty() == true) {
- var lastStateOfCharge = curve.stateOfChargePlotLine!!.first().Value
- for (index in curve.stateOfChargePlotLine!!.indices) {
- val stateOfCharge = curve.stateOfChargePlotLine!![index].Value
- curve.chargePlotLine[index].StateOfCharge = stateOfCharge
- curve.chargePlotLine[index].StateOfChargeDelta = lastStateOfCharge - stateOfCharge
- lastStateOfCharge = stateOfCharge
- }
- curve.stateOfChargePlotLine = null
- }
- chargeCurves.add(curve)
- }
-
- chargePlotLine.addDataPoints(tripData.chargeCurves.last().chargePlotLine)
- }
-
- if (tripData.markers?.isNotEmpty() == true){
- plotMarkers.addMarkers(tripData.markers)
- }
- }
-
- fun getTripData(fileName: String): TripData {
- return getTripData()
- }
-
- fun getTripData(): TripData {
- return TripData(
- BuildConfig.VERSION_NAME,
- tripStartDate,
- traveledDistance,
- usedEnergy,
- averageConsumption,
- travelTimeMillis,
- lastPlotDistance,
- lastPlotEnergy,
- lastPlotTime,
- lastPlotGear,
- lastPlotMarker,
- lastChargePower,
- consumptionPlotLine.getDataPoints(PlotDimension.DISTANCE),
- chargeCurves.toList(),
- plotMarkers.markers.toList()
- )
- }
-
- fun resetDataHolder() {
- traveledDistance = 0f
- tripStartDate = Date()
- usedEnergy = 0f
- chargedEnergy = 0f
- chargeTimeMillis = 0L
- averageConsumption = 0f
- travelTimeMillis = 0L
- lastPlotDistance = 0F
- lastPlotEnergy = 0F
- lastPlotTime = 0L
- lastPlotGear = VehicleGear.GEAR_PARK
- lastPlotMarker = null
- lastChargePower = 0F
- consumptionPlotLine.reset()
- chargePlotLine.reset()
- chargeCurves = ArrayList()
- plotMarkers = PlotMarkers()
- }
-}
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/plot/enums/PlotDimension.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/plot/enums/PlotDimension.kt
index a0238ce4..8e58b34d 100644
--- a/automotive/src/main/java/com/ixam97/carStatsViewer/plot/enums/PlotDimension.kt
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/plot/enums/PlotDimension.kt
@@ -1,5 +1,12 @@
package com.ixam97.carStatsViewer.plot.enums
enum class PlotDimension {
- INDEX, DISTANCE, TIME, STATE_OF_CHARGE
+ INDEX, DISTANCE, TIME, STATE_OF_CHARGE;
+
+ fun toPlotDirection(): PlotDirection {
+ return when (this) {
+ TIME, STATE_OF_CHARGE -> PlotDirection.LEFT_TO_RIGHT
+ else -> PlotDirection.RIGHT_TO_LEFT
+ }
+ }
}
\ No newline at end of file
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/plot/enums/PlotDirection.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/plot/enums/PlotDirection.kt
new file mode 100644
index 00000000..79a0d1d2
--- /dev/null
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/plot/enums/PlotDirection.kt
@@ -0,0 +1,5 @@
+package com.ixam97.carStatsViewer.plot.enums
+
+enum class PlotDirection {
+ LEFT_TO_RIGHT, RIGHT_TO_LEFT
+}
\ No newline at end of file
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/plot/enums/PlotHighlightMethod.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/plot/enums/PlotHighlightMethod.kt
index 81322b20..f3db1187 100644
--- a/automotive/src/main/java/com/ixam97/carStatsViewer/plot/enums/PlotHighlightMethod.kt
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/plot/enums/PlotHighlightMethod.kt
@@ -1,5 +1,5 @@
package com.ixam97.carStatsViewer.plot.enums
enum class PlotHighlightMethod {
- MIN, MAX, FIRST, LAST, AVG_BY_INDEX, AVG_BY_DISTANCE, AVG_BY_TIME, AVG_BY_STATE_OF_CHARGE, NONE
+ MIN, MAX, FIRST, LAST, AVG_BY_DIMENSION, AVG_BY_INDEX, AVG_BY_DISTANCE, AVG_BY_TIME, AVG_BY_STATE_OF_CHARGE, RAW, NONE
}
\ No newline at end of file
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/plot/enums/PlotSessionGapRendering.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/plot/enums/PlotSessionGapRendering.kt
index 47e3747c..3459bf8f 100644
--- a/automotive/src/main/java/com/ixam97/carStatsViewer/plot/enums/PlotSessionGapRendering.kt
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/plot/enums/PlotSessionGapRendering.kt
@@ -1,5 +1,5 @@
package com.ixam97.carStatsViewer.plot.enums
enum class PlotSessionGapRendering {
- NONE, JOIN, CIRCLE
+ NONE, JOIN, CIRCLE, GAP
}
\ No newline at end of file
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/plot/graphics/PlotLinePaint.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/plot/graphics/PlotLinePaint.kt
new file mode 100644
index 00000000..8e699406
--- /dev/null
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/plot/graphics/PlotLinePaint.kt
@@ -0,0 +1,20 @@
+package com.ixam97.carStatsViewer.plot.graphics
+
+import com.ixam97.carStatsViewer.plot.enums.PlotSecondaryDimension
+
+class PlotLinePaint(
+ private val primary : PlotPaint,
+ private val secondaryNormal : PlotPaint,
+ private val secondaryAlternative : PlotPaint,
+ private var useSecondaryAlternative: () -> Boolean
+) {
+ fun bySecondaryDimension(secondaryDimension: PlotSecondaryDimension?) : PlotPaint {
+ return when {
+ secondaryDimension != null -> when {
+ useSecondaryAlternative.invoke() -> secondaryAlternative
+ else -> secondaryNormal
+ }
+ else -> primary
+ }
+ }
+}
\ No newline at end of file
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/plot/graphics/PlotPaint.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/plot/graphics/PlotPaint.kt
index 2eedb674..0dcfeba2 100644
--- a/automotive/src/main/java/com/ixam97/carStatsViewer/plot/graphics/PlotPaint.kt
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/plot/graphics/PlotPaint.kt
@@ -6,9 +6,16 @@ import android.graphics.Paint
class PlotPaint(
val Plot: Paint,
- val PlotSecondary: Paint,
+ val PlotGap: Paint,
val PlotBackground: Paint,
+
+ val PlotSecondary: Paint,
+ val PlotGapSecondary: Paint,
val PlotBackgroundSecondary: Paint,
+
+ val Color: Int,
+ val TransparentColor: Int,
+
val HighlightLabel: Paint,
val HighlightLabelLine: Paint
) {
@@ -26,17 +33,24 @@ class PlotPaint(
plotPaint.color = color
plotPaint.strokeWidth = 3f
- val plotSecondaryPaint = Paint(plotPaint)
- plotSecondaryPaint.color = Color.argb(160, Color.red(color), Color.green(color), Color.blue(color))
- plotSecondaryPaint.strokeWidth = 2f
+ val plotGapPaint = Paint(plotPaint)
+ plotGapPaint.color = Color.argb(128, Color.red(color), Color.green(color), Color.blue(color))
+ plotGapPaint.pathEffect = DashPathEffect(floatArrayOf(2f, 10f), 0f)
val plotBackgroundPaint = Paint(plotPaint)
- plotBackgroundPaint.color = Color.argb(32, Color.red(color), Color.green(color), Color.blue(color))
+ plotBackgroundPaint.color = Color.argb(160, Color.red(color), Color.green(color), Color.blue(color))
plotBackgroundPaint.style = Paint.Style.FILL
- val plotBackgroundSecondaryPaint = Paint(plotBackgroundPaint)
- plotBackgroundSecondaryPaint.color = Color.argb(32, Color.red(color), Color.green(color), Color.blue(color))
- plotBackgroundSecondaryPaint.strokeWidth = 2f
+// val plotSecondaryPaint = Paint(plotPaint)
+// plotSecondaryPaint.color = Color.argb(160, Color.red(color), Color.green(color), Color.blue(color))
+// plotSecondaryPaint.strokeWidth = 2f
+//
+// val plotGapSecondaryPaint = Paint(plotSecondaryPaint)
+// plotGapSecondaryPaint.pathEffect = DashPathEffect(floatArrayOf(5f, 10f), 0f)
+//
+// val plotBackgroundSecondaryPaint = Paint(plotBackgroundPaint)
+// plotBackgroundSecondaryPaint.color = Color.argb(32, Color.red(color), Color.green(color), Color.blue(color))
+// plotBackgroundSecondaryPaint.strokeWidth = 2f
val highlightLabelPaint = Paint(basePaint)
highlightLabelPaint.color = color
@@ -46,7 +60,18 @@ class PlotPaint(
highlightLabelLinePaint.strokeWidth = 3f
highlightLabelLinePaint.pathEffect = DashPathEffect(floatArrayOf(5f, 10f), 0f)
- val paint = PlotPaint(plotPaint, plotSecondaryPaint, plotBackgroundPaint, plotBackgroundSecondaryPaint, highlightLabelPaint, highlightLabelLinePaint)
+ val paint = PlotPaint(
+ plotPaint,
+ plotGapPaint,
+ plotBackgroundPaint,
+ plotPaint, // use same as primary for now
+ plotGapPaint,
+ plotBackgroundPaint,
+ color, // use same as primary for now
+ Color.argb(0, Color.red(color), Color.green(color), Color.blue(color)),
+ highlightLabelPaint,
+ highlightLabelLinePaint
+ )
if (paintCache[color] == null) paintCache[color] = HashMap()
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/plot/objects/PlotGlobalConfiguration.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/plot/objects/PlotGlobalConfiguration.kt
index 236952e1..3490f92a 100644
--- a/automotive/src/main/java/com/ixam97/carStatsViewer/plot/objects/PlotGlobalConfiguration.kt
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/plot/objects/PlotGlobalConfiguration.kt
@@ -1,8 +1,11 @@
package com.ixam97.carStatsViewer.plot.objects
+import com.ixam97.carStatsViewer.enums.DistanceUnitEnum
import com.ixam97.carStatsViewer.plot.enums.*
object PlotGlobalConfiguration {
+ val DataVersion : Int? = 20230206
+
val SecondaryDimensionConfiguration: HashMap =
hashMapOf(
PlotSecondaryDimension.SPEED to PlotLineConfiguration(
@@ -30,8 +33,20 @@ object PlotGlobalConfiguration {
PlotRange(0f, 100f, backgroundZero = 0f),
PlotLineLabelFormat.PERCENTAGE,
PlotLabelPosition.RIGHT,
- PlotHighlightMethod.MAX,
+ PlotHighlightMethod.LAST,
"% SoC"
)
)
+
+ fun updateDistanceUnit(distanceUnit: DistanceUnitEnum) {
+
+ SecondaryDimensionConfiguration[PlotSecondaryDimension.SPEED]?.UnitFactor = distanceUnit.asFactor()
+ SecondaryDimensionConfiguration[PlotSecondaryDimension.SPEED]?.Divider = distanceUnit.asFactor()
+ SecondaryDimensionConfiguration[PlotSecondaryDimension.SPEED]?.Unit = "%s/h".format(distanceUnit.unit())
+
+
+ SecondaryDimensionConfiguration[PlotSecondaryDimension.DISTANCE]?.UnitFactor = distanceUnit.toFactor()
+ SecondaryDimensionConfiguration[PlotSecondaryDimension.DISTANCE]?.Divider = distanceUnit.asFactor()
+ SecondaryDimensionConfiguration[PlotSecondaryDimension.DISTANCE]?.Unit = "%s".format(distanceUnit.unit())
+ }
}
\ No newline at end of file
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/plot/objects/PlotLine.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/plot/objects/PlotLine.kt
index 93f92f4b..22fd359c 100644
--- a/automotive/src/main/java/com/ixam97/carStatsViewer/plot/objects/PlotLine.kt
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/plot/objects/PlotLine.kt
@@ -3,7 +3,7 @@ package com.ixam97.carStatsViewer.plot.objects
import com.ixam97.carStatsViewer.plot.graphics.*
import com.ixam97.carStatsViewer.plot.enums.*
import java.util.concurrent.ConcurrentHashMap
-import kotlin.math.abs
+import kotlin.math.absoluteValue
class PlotLine(
val Configuration: PlotLineConfiguration,
@@ -13,47 +13,78 @@ class PlotLine(
var baseLineAt: ArrayList = ArrayList()
- var plotPaint: PlotPaint? = null
- var secondaryPlotPaint: PlotPaint? = null
-
var alignZero: Boolean = false
var zeroAt: Float? = null
- fun addDataPoint(item: Float, time: Long, distance: Float, stateOfCharge: Float, timeDelta: Long? = null, distanceDelta: Float? = null, stateOfChargeDelta: Float? = null, plotLineMarkerType: PlotLineMarkerType? = null) {
- val prev = dataPoints[dataPoints.size - 1]
+ fun addDataPoint(item: Float, epochTime: Long, nanoTime: Long, distance: Float, stateOfCharge: Float, timeDelta: Long? = null, distanceDelta: Float? = null, stateOfChargeDelta: Float? = null, plotLineMarkerType: PlotLineMarkerType? = null, autoMarkerTimeDeltaThreshold: Long? = null) {
+ val prev = when (dataPoints[dataPoints.size - 1]?.Marker ?: PlotLineMarkerType.BEGIN_SESSION) {
+ PlotLineMarkerType.BEGIN_SESSION -> dataPoints[dataPoints.size - 1]
+ else -> null
+ }
- addDataPoint(
- PlotLineItem(
+ addDataPoint(PlotLineItem(
item,
- time,
+ epochTime,
+ nanoTime,
distance,
stateOfCharge,
- timeDelta?:(time - (prev?.Time ?: time)),
+ timeDelta?:(nanoTime - (prev?.NanoTime ?: nanoTime)),
distanceDelta?:(distance - (prev?.Distance ?: distance)),
- stateOfChargeDelta?:(stateOfCharge - (prev?.StateOfCharge ?: distance)),
+ stateOfChargeDelta?:(stateOfCharge - (prev?.StateOfCharge ?: stateOfCharge)),
plotLineMarkerType
- )
- )
+ ), autoMarkerTimeDeltaThreshold)
}
- fun addDataPoint(dataPoint: PlotLineItem) {
+ fun addDataPoint(dataPoint: PlotLineItem, autoMarkerTimeDeltaThreshold: Long? = null) {
+ val prev = dataPoints[dataPoints.size - 1]
+
+ if (dataPoint.Marker == PlotLineMarkerType.BEGIN_SESSION && prev?.Marker == null) {
+ prev?.Marker = PlotLineMarkerType.END_SESSION
+ }
+
+ if (dataPoint.Marker == null && (prev == null || prev?.Marker == PlotLineMarkerType.END_SESSION)) {
+ dataPoint.Marker = PlotLineMarkerType.BEGIN_SESSION
+ }
+
+ if ((autoMarkerTimeDeltaThreshold ?: dataPoint.TimeDelta ?: 0L) < (dataPoint.TimeDelta ?: 0L)) {
+ prev?.Marker = PlotLineMarkerType.END_SESSION
+ dataPoint.Marker = PlotLineMarkerType.BEGIN_SESSION
+ } else if (prev?.Marker == PlotLineMarkerType.BEGIN_SESSION) {
+ if ((prev.StateOfCharge - dataPoint.StateOfCharge).absoluteValue > 1) {
+ // Car gives an old value for SoC at the end of hibernation. just override that. Bit hacky though...
+ prev.StateOfCharge = dataPoint.StateOfCharge
+ dataPoint.StateOfChargeDelta = 0f
+ }
+ if (prev.Value < dataPoint.Value - 1_000) {
+ prev.Value = dataPoint.Value
+ }
+ }
+
+ if (dataPoint.Marker == PlotLineMarkerType.BEGIN_SESSION) {
+ dataPoint.TimeDelta = 0L
+ dataPoint.DistanceDelta = 0f
+ dataPoint.StateOfChargeDelta = 0f
+ }
+
when {
dataPoint.Value.isFinite() -> {
dataPoints[dataPoints.size] = dataPoint
}
- dataPoint.Marker == PlotLineMarkerType.END_SESSION -> {
- val lastPoint = dataPoints[dataPoints.size - 1]
- when {
- lastPoint != null && lastPoint.Marker == null -> {
- lastPoint.Marker = dataPoint.Marker
- }
- }
- }
}
}
fun addDataPoints(dataPoints: List) {
+ if (dataPoints.isEmpty()) return
+
+ val last = dataPoints.last()
+
+ // make sure to close the last marker on restore, next item will then be a BEGIN
+ last.Marker = when (last.Marker) {
+ PlotLineMarkerType.BEGIN_SESSION -> PlotLineMarkerType.SINGLE_SESSION
+ else -> PlotLineMarkerType.END_SESSION
+ }
+
for (dataPoint in dataPoints) {
addDataPoint(dataPoint)
}
@@ -66,88 +97,99 @@ class PlotLine(
fun getDataPoints(dimension: PlotDimension, dimensionRestriction: Long? = null, dimensionShift: Long? = null): List {
return when {
dataPoints.isEmpty() || dimensionRestriction == null -> dataPoints.map { it.value }
- else -> when (dimension) {
- PlotDimension.INDEX -> {
- val max = dataPoints.size - 1 - (dimensionShift ?: 0L)
- val min = max - dimensionRestriction
-
- dataPoints.filter { it.key in min..max }.map { it.value }
- }
- PlotDimension.DISTANCE -> {
- val max = dataPoints[dataPoints.size - 1]!!.Distance - (dimensionShift ?: 0L)
- val min = max - dimensionRestriction
+ else -> {
+ val min = minDimension(dimension, dimensionRestriction, dimensionShift)
+ val max = maxDimension(dimension, dimensionRestriction, dimensionShift)
- dataPoints.filter { it.value.Distance in min..max }.map { it.value }
- }
- PlotDimension.TIME -> {
- val min = dataPoints[0]!!.Time + (dimensionShift ?: 0L)
- val max = min + dimensionRestriction
-
- dataPoints.filter { it.value.Time in min..max }.map { it.value }
- }
- PlotDimension.STATE_OF_CHARGE -> {
- val max = dataPoints[dataPoints.size - 1]!!.StateOfCharge - (dimensionShift ?: 0L)
- val min = max - dimensionRestriction
-
- dataPoints.filter { it.value.StateOfCharge in min..max }.map { it.value }
- }
+ getDataPoints (dimension, min, max)
}
}
}
- internal fun minDimension(dataPoints: List, dimension: PlotDimension, dimensionRestriction: Long?): Any {
- return when (dimension) {
- PlotDimension.INDEX -> 0f
- PlotDimension.DISTANCE -> when {
- dataPoints.isEmpty() -> 0f
- else -> (maxDimension(dataPoints, dimension, dimensionRestriction) as Float - (dimensionRestriction ?: 0L))
- .coerceAtMost(dataPoints.minOf { it.Distance })
- }
- PlotDimension.TIME -> when {
- dataPoints.isEmpty() -> 0L
- else -> dataPoints.minOf { it.Time }
+ private fun getDataPoints(dimension: PlotDimension, min: Any?, max: Any?): List {
+ return when {
+ dataPoints.isEmpty() || min == null || max == null -> dataPoints.map { it.value }
+ else -> when (dimension) {
+ PlotDimension.INDEX -> dataPoints.filter { it.key in min as Int..max as Int }.map { it.value }
+ PlotDimension.DISTANCE -> dataPoints.filter { it.value.Distance in min as Float..max as Float }.map { it.value }
+ PlotDimension.TIME -> dataPoints.filter { it.value.EpochTime in min as Long..max as Long }.map { it.value }
+ PlotDimension.STATE_OF_CHARGE -> dataPoints.filter { it.value.StateOfCharge in min as Float..max as Float }.map { it.value }
}
- PlotDimension.STATE_OF_CHARGE -> 0f
}
}
- internal fun maxDimension(dataPoints: List, dimension: PlotDimension, dimensionRestriction: Long?): Any {
- return when (dimension) {
- PlotDimension.INDEX -> (dataPoints.size - 1).toFloat()
- PlotDimension.DISTANCE -> when {
- dataPoints.isEmpty() -> 0f
- else -> dataPoints.maxOf { it.Distance }
+ internal fun minDimension(dimension: PlotDimension, dimensionRestriction: Long? = null, dimensionShift: Long? = null): Any? {
+ return when {
+ dataPoints.isEmpty() -> null
+ else -> when (dimension) {
+ PlotDimension.INDEX -> when(dimensionRestriction) {
+ null -> dataPoints.minOf { it.key }
+ else -> maxDimension(dimension, dimensionRestriction, dimensionShift) as Int - dimensionRestriction
+ }
+ PlotDimension.DISTANCE -> when(dimensionRestriction) {
+ null -> dataPoints.minOf { it.value.Distance }
+ else -> maxDimension(dimension, dimensionRestriction, dimensionShift) as Float - dimensionRestriction
+ }
+ PlotDimension.TIME -> when(dimensionRestriction) {
+ null -> dataPoints.minOf { it.value.EpochTime }
+ else -> dataPoints.minOf { it.value.EpochTime } + (dimensionShift ?: 0L)
+ }
+ PlotDimension.STATE_OF_CHARGE -> 0f
}
- PlotDimension.TIME -> when {
- dataPoints.isEmpty() -> 0L
- else -> (minDimension(dataPoints, dimension, dimensionRestriction) as Long + (dimensionRestriction ?: 0L))
- .coerceAtLeast(dataPoints.maxOf { it.Time })
+ }
+ }
+
+ internal fun maxDimension(dimension: PlotDimension, dimensionRestriction: Long? = null, dimensionShift: Long? = null): Any? {
+ return when {
+ dataPoints.isEmpty() -> null
+ else -> when (dimension) {
+ PlotDimension.INDEX -> when(dimensionRestriction) {
+ null -> dataPoints.maxOf { it.key }
+ else -> dataPoints.maxOf { it.key } - (dimensionShift ?: 0L)
+ }
+ PlotDimension.DISTANCE -> when(dimensionRestriction) {
+ null -> dataPoints.maxOf { it.value.Distance }
+ else -> dataPoints.maxOf { it.value.Distance } - (dimensionShift ?: 0L)
+ }
+ PlotDimension.TIME -> when(dimensionRestriction) {
+ null -> dataPoints.maxOf { it.value.EpochTime }
+ else -> minDimension(dimension, dimensionRestriction, dimensionShift) as Long + dimensionRestriction
+ }
+ PlotDimension.STATE_OF_CHARGE -> 100f
}
- PlotDimension.STATE_OF_CHARGE -> 100f
}
}
- fun distanceDimension(dimension: PlotDimension, dimensionRestriction: Long? = null): Float {
- return distanceDimension(getDataPoints(dimension), dimension, dimensionRestriction)
+ fun distanceDimension(dimension: PlotDimension, dimensionRestriction: Long? = null, dimensionShift: Long? = null): Float? {
+ return distanceDimensionMinMax(
+ dimension,
+ minDimension(dimension, dimensionRestriction, dimensionShift),
+ maxDimension(dimension, dimensionRestriction, dimensionShift)
+ )
}
- fun distanceDimension(dataPoints: List, dimension: PlotDimension, dimensionRestriction: Long?): Float {
+ fun distanceDimensionMinMax(dimension: PlotDimension, min: Any?, max: Any?): Float? {
+ if (min == null || max == null) return null
+
return when (dimension) {
- PlotDimension.TIME -> (maxDimension(dataPoints, dimension, dimensionRestriction) as Long - minDimension(dataPoints, dimension, dimensionRestriction) as Long).toFloat()
- else -> maxDimension(dataPoints, dimension, dimensionRestriction) as Float - minDimension(dataPoints, dimension, dimensionRestriction) as Float
+ PlotDimension.TIME -> (max as Long - min as Long).toFloat()
+ else -> max as Float - min as Float
}
}
fun maxValue(dataPoints: List, secondaryDimension: PlotSecondaryDimension? = null, applyRange: Boolean = true): Float? {
val baseConfiguration = PlotGlobalConfiguration.SecondaryDimensionConfiguration[secondaryDimension] ?: Configuration
val max : Float? = when {
- dataPoints.isEmpty() -> baseConfiguration.Range.minPositive ?: 0f
+ dataPoints.isEmpty() -> when {
+ baseConfiguration.Range.minPositive == null || !applyRange -> null
+ else -> baseConfiguration.Range.minPositive
+ }
else -> {
- var maxByData = dataPoints.maxOf { it.bySecondaryDimension(secondaryDimension) }
+ var maxByData = dataPoints.mapNotNull { it.bySecondaryDimension(secondaryDimension) }.maxOfOrNull { it }
if (applyRange) {
- if (baseConfiguration.Range.minPositive != null) maxByData = maxByData.coerceAtLeast(baseConfiguration.Range.minPositive)
- if (baseConfiguration.Range.maxPositive != null) maxByData = maxByData.coerceAtMost(baseConfiguration.Range.maxPositive)
+ if (baseConfiguration.Range.minPositive != null) maxByData = (maxByData?:0f).coerceAtLeast(baseConfiguration.Range.minPositive)
+ if (baseConfiguration.Range.maxPositive != null) maxByData = (maxByData?:0f).coerceAtMost(baseConfiguration.Range.maxPositive)
}
maxByData
@@ -158,9 +200,9 @@ class PlotLine(
return when {
max == null -> null
- baseConfiguration.Range.smoothAxis != null -> when (max % baseConfiguration.Range.smoothAxis) {
+ baseConfiguration.Range.smoothAxis != null -> when (max % (baseConfiguration.Range.smoothAxis * baseConfiguration.UnitFactor)) {
0f -> max
- else -> max + (baseConfiguration.Range.smoothAxis - max % baseConfiguration.Range.smoothAxis)
+ else -> max + ((baseConfiguration.Range.smoothAxis * baseConfiguration.UnitFactor) - max % (baseConfiguration.Range.smoothAxis * baseConfiguration.UnitFactor))
}
else -> max
}
@@ -169,13 +211,16 @@ class PlotLine(
fun minValue(dataPoints: List, secondaryDimension: PlotSecondaryDimension? = null, applyRange: Boolean = true): Float? {
val baseConfiguration = PlotGlobalConfiguration.SecondaryDimensionConfiguration[secondaryDimension] ?: Configuration
val min : Float? = when {
- dataPoints.isEmpty() -> baseConfiguration.Range.minNegative ?: 0f
+ dataPoints.isEmpty() -> when {
+ baseConfiguration.Range.minNegative == null || !applyRange -> null
+ else -> baseConfiguration.Range.minNegative
+ }
else -> {
- var minByData = dataPoints.minOf { it.bySecondaryDimension(secondaryDimension) }
+ var minByData = dataPoints.mapNotNull { it.bySecondaryDimension(secondaryDimension) }.minOfOrNull { it }
if (applyRange) {
- if (baseConfiguration.Range.minNegative != null) minByData = minByData.coerceAtMost(baseConfiguration.Range.minNegative)
- if (baseConfiguration.Range.maxNegative != null) minByData = minByData.coerceAtLeast(baseConfiguration.Range.maxNegative)
+ if (baseConfiguration.Range.minNegative != null) minByData = (minByData?:0f).coerceAtMost(baseConfiguration.Range.minNegative)
+ if (baseConfiguration.Range.maxNegative != null) minByData = (minByData?:0f).coerceAtLeast(baseConfiguration.Range.maxNegative)
}
minByData
@@ -186,9 +231,9 @@ class PlotLine(
val minSmooth = when {
min == null -> null
- baseConfiguration.Range.smoothAxis != null -> when (min % baseConfiguration.Range.smoothAxis) {
+ baseConfiguration.Range.smoothAxis != null -> when (min % (baseConfiguration.Range.smoothAxis * baseConfiguration.UnitFactor)) {
0f -> min
- else -> min - (min % baseConfiguration.Range.smoothAxis) - baseConfiguration.Range.smoothAxis
+ else -> min - (min % (baseConfiguration.Range.smoothAxis * baseConfiguration.UnitFactor)) - (baseConfiguration.Range.smoothAxis * baseConfiguration.UnitFactor)
}
else -> min
}
@@ -216,44 +261,53 @@ class PlotLine(
if (dataPoints.isEmpty()) return null
if (dataPoints.size == 1) return dataPoints.first().bySecondaryDimension(secondaryDimension)
- return when (averageMethod) {
- PlotHighlightMethod.AVG_BY_INDEX -> dataPoints.map { it.bySecondaryDimension(secondaryDimension) }.average().toFloat()
+ val averageValue = when (averageMethod) {
+ PlotHighlightMethod.AVG_BY_INDEX -> dataPoints.mapNotNull { it.bySecondaryDimension(secondaryDimension) }.average().toFloat()
PlotHighlightMethod.AVG_BY_DISTANCE -> {
- val value = dataPoints.filter { it.DistanceDelta != null }.map { (it.DistanceDelta?:0f) * it.bySecondaryDimension(secondaryDimension) }.sum()
- val distance = dataPoints.filter { it.DistanceDelta != null }.map { (it.DistanceDelta?:0f) }.sum()
+ val value = dataPoints.map { (it.DistanceDelta?:0f) * (it.bySecondaryDimension(secondaryDimension)?:0f) }.sum()
+ val distance = dataPoints.map { (it.DistanceDelta?:0f) }.sum()
- return when {
+ when {
distance != 0f -> value / distance
- else -> 0f
+ else -> null
}
}
PlotHighlightMethod.AVG_BY_TIME -> {
- val value = dataPoints.filter { it.TimeDelta != null }.map { (it.TimeDelta?:0L) * it.bySecondaryDimension(secondaryDimension) }.sum()
- val distance = dataPoints.filter { it.TimeDelta != null }.map { (it.TimeDelta?:0L) }.sum()
+ val value = dataPoints.map { (it.TimeDelta?:0L) * (it.bySecondaryDimension(secondaryDimension)?:0f) }.sum()
+ val distance = dataPoints.sumOf { (it.TimeDelta?:0L) }
- return when {
+ when {
distance != 0L -> value / distance
- else -> 0f
+ else -> null
}
}
PlotHighlightMethod.AVG_BY_STATE_OF_CHARGE -> {
- val value = dataPoints.filter { it.StateOfChargeDelta != null }.map { (it.StateOfChargeDelta?:0f) * it.bySecondaryDimension(secondaryDimension) }.sum()
- val distance = dataPoints.filter { it.StateOfChargeDelta != null }.map { (it.StateOfChargeDelta?:0f) }.sum()
+ val value = dataPoints.map { (it.StateOfChargeDelta?:0f) * (it.bySecondaryDimension(secondaryDimension)?:0f) }.sum()
+ val distance = dataPoints.map { (it.StateOfChargeDelta?:0f) }.sum()
- return when {
+ when {
distance != 0f -> value / distance
- else -> 0f
+ else -> null
}
}
else -> null
}
+
+ if (averageValue != null) return averageValue
+
+ val nonNull = dataPoints.mapNotNull { it.bySecondaryDimension(secondaryDimension) }
+
+ return when {
+ nonNull.isEmpty() -> null
+ else -> nonNull.sum() / nonNull.size
+ }
}
fun isEmpty(): Boolean {
return dataPoints.isEmpty()
}
- fun byHighlightMethod(dataPoints: List, secondaryDimension: PlotSecondaryDimension? = null): Float? {
+ fun byHighlightMethod(dataPoints: List, dimension: PlotDimension, secondaryDimension: PlotSecondaryDimension? = null): Float? {
if (dataPoints.isEmpty()) return null
val configuration = when {
@@ -261,7 +315,17 @@ class PlotLine(
else -> Configuration
} ?: return null
- return when (configuration.HighlightMethod) {
+ val highlightMethod = when (configuration.HighlightMethod) {
+ PlotHighlightMethod.AVG_BY_DIMENSION -> when (dimension) {
+ PlotDimension.INDEX -> PlotHighlightMethod.AVG_BY_INDEX
+ PlotDimension.DISTANCE -> PlotHighlightMethod.AVG_BY_DISTANCE
+ PlotDimension.TIME -> PlotHighlightMethod.AVG_BY_TIME
+ PlotDimension.STATE_OF_CHARGE -> PlotHighlightMethod.AVG_BY_STATE_OF_CHARGE
+ }
+ else -> configuration.HighlightMethod
+ }
+
+ return when (highlightMethod) {
PlotHighlightMethod.MIN -> minValue(dataPoints, secondaryDimension, false)
PlotHighlightMethod.MAX -> maxValue(dataPoints, secondaryDimension, false)
PlotHighlightMethod.FIRST -> dataPoints.first().bySecondaryDimension(secondaryDimension)
@@ -269,45 +333,12 @@ class PlotLine(
PlotHighlightMethod.AVG_BY_INDEX,
PlotHighlightMethod.AVG_BY_DISTANCE,
PlotHighlightMethod.AVG_BY_TIME,
- PlotHighlightMethod.AVG_BY_STATE_OF_CHARGE -> averageValue(dataPoints, configuration.HighlightMethod, secondaryDimension)
- else -> null
- }
- }
-
- fun x(dataPoints: List, value: Long?, valueDimension: PlotDimension, targetDimension: PlotDimension, min: Any, max: Any) : Float? {
- if (dataPoints.isEmpty() || value == null) return null
- return when (targetDimension) {
- PlotDimension.DISTANCE -> when (valueDimension) {
- PlotDimension.TIME -> {
- if (value !in dataPoints.first().Time .. dataPoints.last().Time) return null
-
- val closePoint = dataPoints.minBy { abs(it.Time - value) }
- when (closePoint.Marker) {
- PlotLineMarkerType.BEGIN_SESSION -> x(closePoint.Distance - (closePoint.DistanceDelta ?: 0f), min, max)
- else -> x(closePoint.Distance, min, max)
- }
- }
- PlotDimension.DISTANCE -> x(value.toFloat(), min, max)
- else -> null
- }
- PlotDimension.TIME -> when (valueDimension) {
- PlotDimension.TIME -> x(value.toFloat(), min, max)
- PlotDimension.DISTANCE -> {
- if (value.toFloat() !in dataPoints.first().Distance .. dataPoints.last().Distance) return null
-
- val closePoint = dataPoints.minBy { abs(it.Distance - value) }
- when (closePoint.Marker) {
- PlotLineMarkerType.BEGIN_SESSION -> x(closePoint.Time - (closePoint.TimeDelta ?: 0L), min, max)
- else -> x(closePoint.Distance, min, max)
- }
- }
- else -> null
- }
+ PlotHighlightMethod.AVG_BY_STATE_OF_CHARGE -> averageValue(dataPoints, highlightMethod, secondaryDimension)
else -> null
}
}
- fun x(index: Float, min: Any, max: Any) : Float {
+ private fun x(index: Float, min: Any, max: Any) : Float {
return PlotLineItem.cord(
index,
min as Float,
@@ -315,7 +346,7 @@ class PlotLine(
)
}
- fun x(index: Long, min: Any, max: Any) : Float {
+ private fun x(index: Long, min: Any, max: Any) : Float {
return PlotLineItem.cord(
index,
min as Long,
@@ -323,19 +354,19 @@ class PlotLine(
)
}
- fun toPlotLineItemPointCollection(dataPoints: List, dimension: PlotDimension, dimensionSmoothing: Float?, min: Any, max: Any): ArrayList> {
- val result = ArrayList>()
- var group = ArrayList()
+ fun toPlotLineItemPointCollection(dataPoints: List, dimension: PlotDimension, dimensionSmoothing: Float?, min: Any, max: Any): ArrayList> {
+ val result = ArrayList>()
+ var group = ArrayList()
for (index in dataPoints.indices) {
val item = dataPoints[index]
group.add(
- PlotLineItemPoint(
+ PlotPoint(
when (dimension) {
PlotDimension.INDEX -> x(index.toFloat(), min, max)
PlotDimension.DISTANCE -> x(item.Distance, min, max)
- PlotDimension.TIME -> x(item.Time, min, max)
+ PlotDimension.TIME -> x(item.EpochTime, min, max)
PlotDimension.STATE_OF_CHARGE -> x(item.StateOfCharge, min, max)
},
item,
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/plot/objects/PlotLineConfiguration.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/plot/objects/PlotLineConfiguration.kt
index f79f3683..7d9101fd 100644
--- a/automotive/src/main/java/com/ixam97/carStatsViewer/plot/objects/PlotLineConfiguration.kt
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/plot/objects/PlotLineConfiguration.kt
@@ -8,5 +8,6 @@ class PlotLineConfiguration(
var LabelPosition: PlotLabelPosition,
var HighlightMethod: PlotHighlightMethod,
var Unit: String,
- var Divider: Float = 1f
+ var Divider: Float = 1f,
+ var UnitFactor: Float = 1f
)
\ No newline at end of file
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/plot/objects/PlotLineItem.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/plot/objects/PlotLineItem.kt
index 824e720e..81708c8b 100644
--- a/automotive/src/main/java/com/ixam97/carStatsViewer/plot/objects/PlotLineItem.kt
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/plot/objects/PlotLineItem.kt
@@ -1,42 +1,54 @@
package com.ixam97.carStatsViewer.plot.objects
import com.ixam97.carStatsViewer.plot.enums.*
+import com.ixam97.carStatsViewer.utils.Exclude
+import java.time.LocalDate
+import java.util.*
import kotlin.math.roundToInt
class PlotLineItem (
- val Value: Float,
-
- val Time: Long,
+ var Value: Float,
+ val EpochTime: Long,
+ @Exclude
+ val NanoTime: Long?,
val Distance: Float,
var StateOfCharge: Float,
-
- val TimeDelta: Long?,
- val DistanceDelta: Float?,
+ var TimeDelta: Long?,
+ var DistanceDelta: Float?,
var StateOfChargeDelta: Float?,
var Marker: PlotLineMarkerType?
){
- fun group(index: Int, dimension: PlotDimension, dimensionSmoothing: Float?): Float {
+ fun group(index: Int, dimension: PlotDimension, dimensionSmoothing: Float?): Any {
val value = when(dimension) {
- PlotDimension.INDEX -> index.toFloat()
+ PlotDimension.INDEX -> index
PlotDimension.DISTANCE -> Distance
- PlotDimension.TIME -> Time.toFloat()
+ PlotDimension.TIME -> EpochTime
PlotDimension.STATE_OF_CHARGE -> StateOfCharge
}
return when (dimensionSmoothing) {
null -> value
0f -> value
- else -> (value / dimensionSmoothing).roundToInt().toFloat()
+ else -> when(dimension) {
+ PlotDimension.INDEX -> (value as Int / dimensionSmoothing).roundToInt()
+ PlotDimension.DISTANCE, PlotDimension.STATE_OF_CHARGE -> (value as Float / dimensionSmoothing).roundToInt()
+ PlotDimension.TIME -> (value as Long / dimensionSmoothing).roundToInt()
+ }
}
}
- fun bySecondaryDimension(secondaryDimension: PlotSecondaryDimension? = null): Float {
+ fun bySecondaryDimension(secondaryDimension: PlotSecondaryDimension? = null): Float? {
return when (secondaryDimension) {
- PlotSecondaryDimension.SPEED -> (DistanceDelta ?: 0f) / ((TimeDelta ?: 1L) / 1_000_000_000f) * 3.6f
+ PlotSecondaryDimension.SPEED -> {
+ when {
+ (TimeDelta?:0L) <= 0 -> null
+ else -> (DistanceDelta ?: 0f) / ((TimeDelta ?: 1L) / 1_000_000_000f) * 3.6f
+ }
+ }
PlotSecondaryDimension.DISTANCE -> Distance
- PlotSecondaryDimension.TIME -> Time.toFloat()
- PlotSecondaryDimension.STATE_OF_CHARGE -> StateOfCharge ?: 0f
+ PlotSecondaryDimension.TIME -> EpochTime.toFloat()
+ PlotSecondaryDimension.STATE_OF_CHARGE -> StateOfCharge?:0f
else -> Value
}
}
@@ -53,6 +65,13 @@ class PlotLineItem (
return 1f / (max - min) * (index - min)
}
+ fun cord(index: Long?, min: Long, max: Long) : Float? {
+ return when (index) {
+ null -> null
+ else -> cord(index, min, max)
+ }
+ }
+
fun cord(index: Long, min: Long, max: Long) : Float {
return 1f / (max - min) * (index - min)
}
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/plot/objects/PlotMarker.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/plot/objects/PlotMarker.kt
index f3aefde7..a69031c8 100644
--- a/automotive/src/main/java/com/ixam97/carStatsViewer/plot/objects/PlotMarker.kt
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/plot/objects/PlotMarker.kt
@@ -1,26 +1,38 @@
package com.ixam97.carStatsViewer.plot.objects
+import com.ixam97.carStatsViewer.plot.enums.PlotDimension
import com.ixam97.carStatsViewer.plot.enums.PlotMarkerType
+import kotlin.math.roundToInt
class PlotMarkers {
val markers : ArrayList = ArrayList()
- fun addMarker(plotMarkerType: PlotMarkerType, time: Long) {
+ fun reset() {
+ markers.clear()
+ }
+
+ fun addMarker(plotMarkerType: PlotMarkerType, time: Long, distance: Float) {
when {
markers.isNotEmpty() -> {
- val last = markers.last()
+ val last = markers.maxByOrNull { it.StartTime }!!
when {
last.MarkerType == plotMarkerType && last.EndTime == null -> return
last.MarkerType == plotMarkerType && last.EndTime == time -> {
last.EndTime = null
+ last.EndDistance = null
return
}
- else -> endMarker(time)
+ last.MarkerType == plotMarkerType && last.EndDistance == distance -> {
+ last.EndTime = null
+ last.EndDistance = null
+ return
+ }
+ else -> endMarker(time, distance)
}
}
}
- markers.add(PlotMarker(plotMarkerType, time))
+ markers.add(PlotMarker(plotMarkerType, StartTime = time, StartDistance = distance, MarkerVersion = PlotGlobalConfiguration.DataVersion))
}
fun addMarkers(markers: List) {
@@ -30,21 +42,86 @@ class PlotMarkers {
for (marker in markers.filter { it.EndTime == null }) {
marker.EndTime = marker.StartTime
}
+ for (marker in markers.filter { it.EndDistance == null }) {
+ marker.EndDistance = marker.StartDistance
+ }
+ }
+
+ // Version Migrations
+ val provided : ArrayList = ArrayList()
+ for (marker in markers) {
+ // old version of markers before switch to System.currentTimeMillis()
+ if (marker.MarkerVersion == null) continue
+
+ provided.add(marker)
}
this.markers.clear()
- this.markers.addAll(current.union(markers).sortedBy { it.StartTime })
+ this.markers.addAll(current.union(provided).sortedBy { it.StartTime })
}
- fun endMarker(time: Long) {
- if (markers.isNotEmpty() && markers.last().EndTime == null) {
- markers.last().EndTime = time
+ fun endMarker(time: Long, distance: Float) {
+ if (markers.isEmpty()) return
+
+ val last = markers.last()
+
+ if (last.EndTime == null) {
+ last.EndTime = when (last.MarkerType) {
+ PlotMarkerType.PARK, PlotMarkerType.CHARGE -> time
+ else -> last.StartTime
+ }
+ }
+
+ if (last.EndDistance == null) {
+ last.EndDistance = when (last.MarkerType) {
+ PlotMarkerType.PARK, PlotMarkerType.CHARGE -> last.StartDistance
+ else -> distance
+ }
}
}
}
class PlotMarker (
val MarkerType: PlotMarkerType,
+ val MarkerVersion: Int? = null,
val StartTime: Long,
var EndTime: Long? = null,
-)
+ val StartDistance: Float,
+ var EndDistance: Float? = null
+) {
+ fun group(dimension: PlotDimension, dimensionSmoothing: Float? = null): Any? {
+ val value = when(dimension) {
+ PlotDimension.DISTANCE -> StartDistance
+ PlotDimension.TIME -> StartTime
+ else -> null
+ } ?: return null
+
+ return when (dimensionSmoothing) {
+ null -> value
+ 0f -> value
+ else -> when(dimension) {
+ PlotDimension.DISTANCE -> (value as Float / dimensionSmoothing).roundToInt()
+ PlotDimension.TIME -> (value as Long / dimensionSmoothing).roundToInt()
+ else -> value
+ }
+ }
+ }
+
+ fun startByDimension(dimension: PlotDimension) : Any? {
+ return when (dimension) {
+ PlotDimension.TIME -> StartTime
+ PlotDimension.DISTANCE -> StartDistance
+ else -> null
+ }
+ }
+
+ fun endByDimension(dimension: PlotDimension) : Any? {
+ return when (dimension) {
+ PlotDimension.TIME -> EndTime
+ PlotDimension.DISTANCE -> EndDistance
+ else -> null
+ }
+ }
+
+
+}
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/plot/objects/PlotPoint.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/plot/objects/PlotPoint.kt
index 86b74010..3eba0b90 100644
--- a/automotive/src/main/java/com/ixam97/carStatsViewer/plot/objects/PlotPoint.kt
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/plot/objects/PlotPoint.kt
@@ -1,10 +1,6 @@
package com.ixam97.carStatsViewer.plot.objects
class PlotPoint(
- val x: Float,
- val y: Float)
-
-class PlotLineItemPoint(
val x: Float,
val y: PlotLineItem,
- val group: Float)
\ No newline at end of file
+ val group: Any)
\ No newline at end of file
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/services/DataCollector.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/services/DataCollector.kt
deleted file mode 100644
index 83b6f218..00000000
--- a/automotive/src/main/java/com/ixam97/carStatsViewer/services/DataCollector.kt
+++ /dev/null
@@ -1,638 +0,0 @@
-package com.ixam97.carStatsViewer.services
-
-
-import com.ixam97.carStatsViewer.objects.*
-import com.ixam97.carStatsViewer.*
-import android.app.*
-import android.car.Car
-import android.car.VehicleGear
-import android.car.VehiclePropertyIds
-import android.car.hardware.CarPropertyValue
-import android.car.hardware.property.CarPropertyManager
-import android.content.*
-import android.graphics.BitmapFactory
-import android.os.*
-import android.util.Log
-import android.widget.Toast
-import androidx.core.app.NotificationManagerCompat
-import com.google.gson.Gson
-import com.ixam97.carStatsViewer.activities.emulatorMode
-import com.ixam97.carStatsViewer.activities.emulatorPowerSign
-import com.ixam97.carStatsViewer.plot.enums.*
-import kotlinx.coroutines.*
-import java.io.File
-import java.io.FileWriter
-import java.lang.Runnable
-import kotlin.collections.HashMap
-import kotlin.math.absoluteValue
-
-lateinit var mainActivityPendingIntent: PendingIntent
-
-class DataCollector : Service() {
- companion object {
- private const val CHANNEL_ID = "TestChannel"
- private const val STATS_NOTIFICATION_ID = 1
- private const val FOREGROUND_NOTIFICATION_ID = 2
- private const val NOTIFICATION_TIMER_HANDLER_DELAY_MILLIS = 1_000L
- private const val SAVE_TRIP_DATA_TIMER_HANDLER_DELAY_MILLIS = 30_000L
- private const val CHARGE_CURVE_UPDATE_INTERVAL_MILLIS = 10_000L
- }
-
- private var startupTimestamp: Long = 0L
- private var lastPowerValueTimestamp: Long = 0L
- private var lastSpeedValueTimestamp: Long = 0L
-
- private var consumptionPlotTracking = false
- private var lastNotificationTimeMillis = 0L
-
- private var chargeStartTimeNanos = 0L
-
- private var notificationCounter = 0
-
- private lateinit var appPreferences: AppPreferences
-
- private val mBinder: LocalBinder = LocalBinder()
-
- private var notificationsEnabled = true
-
- private lateinit var car: Car
- private lateinit var carPropertyManager: CarPropertyManager
-
- private lateinit var notificationTitleString: String
-
- private lateinit var notificationTimerHandler: Handler
- private lateinit var saveTripDataTimerHandler: Handler
-
-
- init {
- startupTimestamp = System.nanoTime()
- }
-
- private val broadcastReceiver = object : BroadcastReceiver() {
- override fun onReceive(context: Context, intent: Intent) {
- when (intent.action) {
- getString(R.string.save_trip_data_broadcast) -> {
- val tripDataToSave = DataHolder.getTripData()
- writeTripDataToFile(tripDataToSave, getString(R.string.file_name_current_trip_data))
- }
- else -> {}
- }
- }
- }
-
- private val saveTripDataTask = object : Runnable {
- override fun run() {
- // sendBroadcast(Intent(getString(R.string.save_trip_data_broadcast)))
- saveTripDataTimerHandler.postDelayed(this, SAVE_TRIP_DATA_TIMER_HANDLER_DELAY_MILLIS)
- }
- }
-
- private val updateStatsNotificationTask = object : Runnable {
- override fun run() {
- updateStatsNotification()
- InAppLogger.logNotificationUpdate()
-
- val currentNotificationTimeMillis = System.currentTimeMillis()
- if (DataHolder.currentGear != VehicleGear.GEAR_PARK && lastNotificationTimeMillis > 0) DataHolder.travelTimeMillis += currentNotificationTimeMillis - lastNotificationTimeMillis
- if (DataHolder.chargePortConnected && lastNotificationTimeMillis > 0) DataHolder.chargeTimeMillis += currentNotificationTimeMillis - lastNotificationTimeMillis
- lastNotificationTimeMillis = currentNotificationTimeMillis
-
- // val ignitionState = carPropertyManager.getIntProperty(VehiclePropertyIds.IGNITION_STATE, 0)
- // val ignitionString = when (ignitionState) {
- // VehicleIgnitionState.LOCK -> "LOCK"
- // VehicleIgnitionState.OFF -> "OFF"
- // VehicleIgnitionState.ACC -> "ACC"
- // VehicleIgnitionState.ON -> "ON"
- // VehicleIgnitionState.START -> "START"
- // else -> "UNDEFINED"
- // }
- // InAppLogger.log(String.format("IAmAlive - Ignition state: %s, current power: %f W", ignitionString, DataHolder.currentPowermW / 1_000))
-
- notificationTimerHandler.postDelayed(this, NOTIFICATION_TIMER_HANDLER_DELAY_MILLIS)
- }
- }
-
- private lateinit var statsNotification: Notification.Builder
- private lateinit var foregroundServiceNotification: Notification.Builder
-
- inner class LocalBinder : Binder() {
- fun getService(): DataCollector = this@DataCollector
- }
-
- override fun onBind(intent: Intent): IBinder? {
- return mBinder
- }
-
- override fun onCreate() {
- super.onCreate()
-
- var tripRestoreComplete = false
- CoroutineScope(Dispatchers.IO).launch {
- val mPrevTripData = readTripDataFromFile(getString(R.string.file_name_current_trip_data))
- runBlocking {
- if (mPrevTripData != null) {
- DataHolder.applyTripData(mPrevTripData)
- sendBroadcast(Intent(getString(R.string.ui_update_plot_broadcast)))
- } else {
- InAppLogger.log("No trip file read!")
- // Toast.makeText(applicationContext ,R.string.toast_file_read_error, Toast.LENGTH_LONG).show()
- }
- tripRestoreComplete = true
- }
- }
-
- while (!tripRestoreComplete) {
- // Wait for completed restore before doing anything
- }
-
- createNotificationChannel()
-
- foregroundServiceNotification = Notification.Builder(applicationContext, CHANNEL_ID)
- .setContentTitle(getString(R.string.app_name))
- .setContentText(getString(R.string.foreground_service_info))
- .setSmallIcon(R.drawable.ic_launcher_foreground)
- .setOngoing(true)
-
- statsNotification = Notification.Builder(this, CHANNEL_ID)
- .setContentTitle("Title")
- .setContentText("Test Notification from Car Stats Viewer")
- .setSmallIcon(R.drawable.ic_launcher_foreground)
- .setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.ic_launcher_foreground))
- .setStyle(Notification.MediaStyle())
- .setCategory(Notification.CATEGORY_TRANSPORT)
- .setOngoing(true)
-
- InAppLogger.log(String.format(
- "DataCollector.onCreate in Thread: %s",
- Thread.currentThread().name))
-
- // sharedPref = this.getSharedPreferences(
- // getString(R.string.preferences_file_key),
- // Context.MODE_PRIVATE)
-
- appPreferences = AppPreferences(applicationContext)
-
- notificationsEnabled = appPreferences.notifications
-
- car = Car.createCar(this)
- carPropertyManager = car.getCarManager(Car.PROPERTY_SERVICE) as CarPropertyManager
-
- DataHolder.maxBatteryCapacity = carPropertyManager.getFloatProperty(VehiclePropertyIds.INFO_EV_BATTERY_CAPACITY, 0)
- DataHolder.currentBatteryCapacity = carPropertyManager.getFloatProperty(VehiclePropertyIds.EV_BATTERY_LEVEL, 0)
- DataHolder.currentGear = carPropertyManager.getIntProperty(VehiclePropertyIds.GEAR_SELECTION, 0)
-
- // if (DataHolder.resetTimestamp == 0L) DataHolder.resetTimestamp = System.nanoTime()
- // if (DataHolder.currentGear == VehicleGear.GEAR_PARK) DataHolder.parkTimestamp = DataHolder.resetTimestamp
-
- /** Get vehicle name to enable dev mode in emulator */
- val carName = carPropertyManager.getProperty(VehiclePropertyIds.INFO_MODEL, 0).value.toString()
- if (carName == "Speedy Model") {
- Toast.makeText(this, "Emulator Mode", Toast.LENGTH_LONG).show()
- emulatorMode = true
- DataHolder.currentGear = VehicleGear.GEAR_PARK
- }
-
- notificationTitleString = resources.getString(R.string.notification_title)
- statsNotification.setContentTitle(notificationTitleString).setContentIntent(mainActivityPendingIntent)
-
- if (notificationsEnabled) {
- with(NotificationManagerCompat.from(this)) {
- notify(STATS_NOTIFICATION_ID, statsNotification.build())
- }
- }
-
- DataHolder.consumptionPlotLine.baseLineAt.add(0f)
-
- registerCarPropertyCallbacks()
-
- notificationTimerHandler = Handler(Looper.getMainLooper())
- notificationTimerHandler.post(updateStatsNotificationTask)
- saveTripDataTimerHandler = Handler(Looper.getMainLooper())
- saveTripDataTimerHandler.postDelayed(saveTripDataTask, SAVE_TRIP_DATA_TIMER_HANDLER_DELAY_MILLIS)
-
- registerReceiver(broadcastReceiver, IntentFilter(getString(R.string.save_trip_data_broadcast)))
- }
-
- override fun onDestroy() {
- super.onDestroy()
- InAppLogger.log("DataCollector.onDestroy")
- sendBroadcast(Intent(getString(R.string.save_trip_data_broadcast)))
- unregisterReceiver(broadcastReceiver)
- car.disconnect()
- }
-
- override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
- InAppLogger.log("DataCollector.onStartCommand")
- startForeground(FOREGROUND_NOTIFICATION_ID, foregroundServiceNotification.build())
- return START_STICKY
- }
-
- private fun registerCarPropertyCallbacks() {
-
- InAppLogger.log("DataCollector.registerCarPropertyCallbacks")
-
- carPropertyManager.registerCallback(
- carPropertyPowerListener,
- VehiclePropertyIds.EV_BATTERY_INSTANTANEOUS_CHARGE_RATE,
- CarPropertyManager.SENSOR_RATE_FASTEST
- )
-
- carPropertyManager.registerCallback(
- carPropertySpeedListener,
- VehiclePropertyIds.PERF_VEHICLE_SPEED,
- CarPropertyManager.SENSOR_RATE_FASTEST
- )
-
- carPropertyManager.registerCallback(
- carPropertyGenericListener,
- VehiclePropertyIds.EV_BATTERY_LEVEL,
- CarPropertyManager.SENSOR_RATE_ONCHANGE
- )
-
- carPropertyManager.registerCallback(
- carPropertyGenericListener,
- VehiclePropertyIds.EV_CHARGE_PORT_CONNECTED,
- CarPropertyManager.SENSOR_RATE_ONCHANGE
- )
-
- carPropertyManager.registerCallback(
- carPropertyGenericListener,
- VehiclePropertyIds.GEAR_SELECTION,
- CarPropertyManager.SENSOR_RATE_ONCHANGE
- )
- }
-
- private val timeDifferenceStore: HashMap = HashMap()
-
- private fun timeDifference(value: CarPropertyValue<*>, maxDifferenceInMilliseconds: Int, timestamp: Long) : Float? {
- var timeDifference : Long? = null
-
- if (timeDifferenceStore.containsKey(value.propertyId)) {
- timeDifference = timestamp - timeDifferenceStore[value.propertyId]!!
- }
-
- timeDifferenceStore[value.propertyId] = timestamp
-
- return when {
- timeDifference == null || timeDifference > (maxDifferenceInMilliseconds * 1_000_000) -> null
- else -> timeDifference.toFloat() / 1_000_000
- }
- }
-
- private fun timeDifference(value: CarPropertyValue<*>, maxDifferenceInMilliseconds: Int) : Float? {
- return timeDifference(value, maxDifferenceInMilliseconds, value.timestamp)
- }
-
- private val timeTriggerStore: HashMap = HashMap()
-
- private fun timerTriggered(value: CarPropertyValue<*>, timerInMilliseconds: Float, timestamp: Long): Boolean {
- var timeTriggered = true
-
- if (timeTriggerStore.containsKey(value.propertyId)) {
- timeTriggered = timeTriggerStore[value.propertyId]?.plus(timerInMilliseconds * 1_000_000)!! <= timestamp
- }
-
- if (timeTriggered) {
- timeTriggerStore[value.propertyId] = timestamp
- }
- return timeTriggered
- }
-
- private fun timerTriggered(value: CarPropertyValue<*>, timerInMilliseconds: Float) : Boolean {
- return timerTriggered(value, timerInMilliseconds, value.timestamp)
- }
-
- private var carPropertyPowerListener = object : CarPropertyManager.CarPropertyEventCallback {
- override fun onChangeEvent(value: CarPropertyValue<*>) {
- //InAppLogger.deepLog("DataCollector.carPropertyPowerListener", appPreferences.deepLog)
-
- if (!emulatorMode) powerUpdater(value)
- }
- override fun onErrorEvent(propId: Int, zone: Int) {
- Log.w("carPropertyPowerListener",
- "Received error car property event, propId=$propId")
- }
- }
-
- private var carPropertySpeedListener = object : CarPropertyManager.CarPropertyEventCallback {
- override fun onChangeEvent(value: CarPropertyValue) {
- if (value.timestamp < startupTimestamp) return
- InAppLogger.logVHALCallback()
-
- speedUpdater(value)
-
- if (emulatorMode) {
- // Also get power in emulator
- var powerValue = carPropertyManager.getProperty(VehiclePropertyIds.EV_BATTERY_INSTANTANEOUS_CHARGE_RATE,0)
- lastSpeedValueTimestamp = value.timestamp
- powerUpdater(powerValue, value.timestamp)
-
- }
-/*
- val consumptionPlotLineJSON = Gson().toJson(DataHolder.consumptionPlotLine)
- val speedPlotLineJSON = Gson().toJson(DataHolder.speedPlotLine)
-
- sharedPref.edit()
- .putString(getString(R.string.userdata_consumption_plot_key), consumptionPlotLineJSON)
- .putString(getString(R.string.userdata_speed_plot_key), speedPlotLineJSON)
- .apply()
-*/
- }
- override fun onErrorEvent(propId: Int, zone: Int) {
- Log.w("carPropertySpeedListener",
- "Received error car property event, propId=$propId")
- }
- }
-
- private var carPropertyGenericListener = object : CarPropertyManager.CarPropertyEventCallback {
- override fun onChangeEvent(value: CarPropertyValue<*>) {
- // InAppLogger.deepLog("DataCollector.carPropertyGenericListener", appPreferences.deepLog)
-
- when (value.propertyId) {
- VehiclePropertyIds.EV_BATTERY_LEVEL -> DataHolder.currentBatteryCapacity = (value.value as Float)
- VehiclePropertyIds.GEAR_SELECTION -> gearUpdater(value.value as Int, value.timestamp)
- VehiclePropertyIds.EV_CHARGE_PORT_CONNECTED -> portUpdater(value.value as Boolean)
- }
- }
- override fun onErrorEvent(propId: Int, zone: Int) {
- Log.w("carPropertyGenericListener","Received error car property event, propId=$propId")
- }
- }
-
- private fun gearUpdater(gear: Int, timestamp: Long) {
- if (DataHolder.currentGear == gear) return
- DataHolder.currentGear = gear
-
- when (gear) {
- VehicleGear.GEAR_PARK -> DataHolder.plotMarkers.addMarker(PlotMarkerType.PARK, timestamp)
- else -> DataHolder.plotMarkers.endMarker(timestamp)
- }
-
- sendBroadcast(Intent(getString(R.string.gear_update_broadcast)))
- if (DataHolder.currentGear == VehicleGear.GEAR_PARK) sendBroadcast(Intent(getString(R.string.save_trip_data_broadcast)))
- }
-
- private fun portUpdater(connected: Boolean) {
- if (connected != DataHolder.chargePortConnected) {
- DataHolder.chargePortConnected = connected
-
- if (lastPowerValueTimestamp < startupTimestamp) lastPowerValueTimestamp = startupTimestamp
-
- if (connected) {
- DataHolder.chargePlotLine.reset()
- DataHolder.chargedEnergy = 0F
- DataHolder.chargeTimeMillis = 0L
- chargeStartTimeNanos = System.nanoTime()
- addChargePlotLine(lastPowerValueTimestamp, PlotLineMarkerType.BEGIN_SESSION)
- DataHolder.plotMarkers.addMarker(PlotMarkerType.CHARGE, lastPowerValueTimestamp)
- sendBroadcast(Intent(getString(R.string.ui_update_plot_broadcast)))
- }
-
- if (!connected) { // && chargeStartTimeNanos > 0 && DataHolder.chargedEnergy > 0) {
- if (DataHolder.chargePlotLine.getDataPoints(PlotDimension.TIME).last().Marker != PlotLineMarkerType.END_SESSION){
- addChargePlotLine(lastPowerValueTimestamp, PlotLineMarkerType.END_SESSION)
- DataHolder.plotMarkers.addMarker(PlotMarkerType.PARK, lastPowerValueTimestamp)
- sendBroadcast(Intent(getString(R.string.ui_update_plot_broadcast)))
- }
- DataHolder.chargeCurves.add(
- ChargeCurve(
- DataHolder.chargePlotLine.getDataPoints(PlotDimension.TIME, null),
- null,
- DataHolder.chargeTimeMillis,
- DataHolder.chargedEnergy,
- 0f, 0f
- )
- )
- sendBroadcast(Intent(getString(R.string.save_trip_data_broadcast)))
- }
- }
- // } else if (!connected && DataHolder.chargeCurves.isNotEmpty()) {
- // DataHolder.chargePlotLine.reset()
- // DataHolder.stateOfChargePlotLine.reset()
- // DataHolder.chargePlotLine.addDataPoints(DataHolder.chargeCurves.last().chargePlotLine)
- // DataHolder.stateOfChargePlotLine.addDataPoints(DataHolder.chargeCurves.last().stateOfChargePlotLine)
- // }
-
- }
-
- private fun powerUpdater(value: CarPropertyValue<*>, timestamp: Long) {
- lastPowerValueTimestamp = timestamp
- DataHolder.currentPowermW = when (emulatorMode) {
- true -> (value.value as Float) * emulatorPowerSign
- else -> - (value.value as Float)
- }
-
- val timeDifference = timeDifference(value, 10_000, timestamp)
- if (timeDifference != null) {
- val energy = (DataHolder.lastPowermW / 1_000) * (timeDifference.toFloat() / (1_000 * 60 * 60))
- if (DataHolder.currentGear != VehicleGear.GEAR_PARK && !DataHolder.chargePortConnected) {
- DataHolder.usedEnergy += energy
- DataHolder.averageConsumption = when {
- DataHolder.traveledDistance <= 0 -> 0F
- else -> DataHolder.usedEnergy / (DataHolder.traveledDistance / 1_000)
- }
- } else if (DataHolder.chargePortConnected) {
- DataHolder.chargedEnergy += -energy
- }
-
- if (emulatorMode) {
- DataHolder.currentBatteryCapacity = DataHolder.currentBatteryCapacity - ((DataHolder.lastPowermW / 1_000) * (timeDifference.toFloat() / (1_000 * 60 * 60)))
- }
- }
-
- if (timerTriggered(value, CHARGE_CURVE_UPDATE_INTERVAL_MILLIS.toFloat(), timestamp) && DataHolder.chargePortConnected && DataHolder.currentGear == VehicleGear.GEAR_PARK) {
- addChargePlotLine(timestamp)
- sendBroadcast(Intent(getString(R.string.ui_update_plot_broadcast)))
- DataHolder.lastChargePower = DataHolder.currentPowermW
- }
- }
-
- private fun addChargePlotLine(timestamp: Long, marker: PlotLineMarkerType? = null) {
- DataHolder.chargePlotLine.addDataPoint(
- -(DataHolder.currentPowermW / 1_000_000f),
- timestamp,
- DataHolder.traveledDistance,
- DataHolder.stateOfCharge(),
- plotLineMarkerType = marker
- )
- }
-
- private fun powerUpdater(value: CarPropertyValue<*>) {
- powerUpdater(value, value.timestamp)
- }
-
- private fun speedUpdater(value: CarPropertyValue<*>) {
- // speed in park = 0 (overrule emulator)
- DataHolder.currentSpeed = when (DataHolder.currentGear) {
- VehicleGear.GEAR_PARK -> 0f
- else -> (value.value as Float).absoluteValue
- }
-
- // after reset
- if (DataHolder.traveledDistance == 0f) {
- consumptionPlotTracking = false
- resetPlotVar(value.timestamp)
- }
-
- val timeDifference = timeDifference(value, 1_000)
- if (timeDifference != null) {
- DataHolder.traveledDistance += DataHolder.lastSpeed * (timeDifference.toFloat() / 1000)
- DataHolder.averageConsumption = when {
- DataHolder.traveledDistance <= 0 -> 0F
- else -> DataHolder.usedEnergy / (DataHolder.traveledDistance / 1000)
- }
-
- if (!consumptionPlotTracking) {
- consumptionPlotTracking = DataHolder.currentGear != VehicleGear.GEAR_PARK
- resetPlotVar(value.timestamp)
- }
-
- val consumptionPlotTrigger = when {
- consumptionPlotTracking -> when {
- DataHolder.traveledDistance >= DataHolder.lastPlotDistance + 100 -> true
- DataHolder.currentGear == VehicleGear.GEAR_PARK -> (DataHolder.lastPlotMarker?: PlotLineMarkerType.BEGIN_SESSION) == PlotLineMarkerType.BEGIN_SESSION || DataHolder.traveledDistance != DataHolder.lastPlotDistance
- else -> false
- }
- else -> false
- }
-
- if (consumptionPlotTrigger) {
- consumptionPlotTracking = DataHolder.currentGear != VehicleGear.GEAR_PARK
-
- val distanceDifference = DataHolder.traveledDistance - DataHolder.lastPlotDistance
- val timeDifference = value.timestamp - DataHolder.lastPlotTime
- val powerDifference = DataHolder.usedEnergy - DataHolder.lastPlotEnergy
-
- val newConsumptionPlotValue = if (distanceDifference > 0) powerDifference / (distanceDifference / 1000) else 0f
-
- val plotMarker = when(DataHolder.lastPlotGear) {
- VehicleGear.GEAR_PARK -> when (DataHolder.currentGear) {
- VehicleGear.GEAR_PARK -> PlotLineMarkerType.SINGLE_SESSION
- else -> PlotLineMarkerType.BEGIN_SESSION
- }
- else -> when (DataHolder.currentGear) {
- VehicleGear.GEAR_PARK -> PlotLineMarkerType.END_SESSION
- else -> null
- }
- }
-
- DataHolder.lastPlotMarker = plotMarker
- DataHolder.lastPlotGear = DataHolder.currentGear
-
- resetPlotVar(value.timestamp)
-
- DataHolder.consumptionPlotLine.addDataPoint(newConsumptionPlotValue, value.timestamp, DataHolder.traveledDistance, DataHolder.stateOfCharge(), timeDifference, distanceDifference, null, plotMarker)
-
- sendBroadcast(Intent(getString(R.string.ui_update_plot_broadcast)))
- }
- }
- sendBroadcast(Intent(getString(R.string.ui_update_gages_broadcast)))
- }
-
- private fun resetPlotVar(currentPlotTimestampMilliseconds: Long) {
- DataHolder.lastPlotDistance = DataHolder.traveledDistance
- DataHolder.lastPlotTime = currentPlotTimestampMilliseconds
- DataHolder.lastPlotEnergy = DataHolder.usedEnergy
- }
-
- private fun createNotificationChannel() {
- val name = "TestChannel"
- val descriptionText = "TestChannel"
- val importance = NotificationManager.IMPORTANCE_DEFAULT
- val channel = NotificationChannel(CHANNEL_ID, name, importance).apply {
- description = descriptionText
- }
- val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- notificationManager.createNotificationChannel(channel)
- }
-
- private fun updateStatsNotification() {
- if (notificationsEnabled && appPreferences.notifications) {
- with(NotificationManagerCompat.from(this)) {
- val averageConsumption = DataHolder.usedEnergy / (DataHolder.traveledDistance/1000)
-
- var averageConsumptionString = String.format("%d Wh/km", averageConsumption.toInt())
- if (!appPreferences.consumptionUnit) {
- averageConsumptionString = String.format(
- "%.1f kWh/100km",
- averageConsumption / 10)
- }
- if ((DataHolder.traveledDistance <= 0)) averageConsumptionString = "N/A"
-
- notificationCounter++
-
- val message = String.format(
- "P:%.1f kW, D: %.3f km, Ø: %s",
- DataHolder.currentPowermW / 1_000_000,
- DataHolder.traveledDistance / 1000,
- averageConsumptionString
- )
-
- statsNotification.setContentText(message)
- foregroundServiceNotification.setContentText(message)
- notify(STATS_NOTIFICATION_ID, statsNotification.build())
- notify(FOREGROUND_NOTIFICATION_ID, foregroundServiceNotification.build())
- }
- } else if (notificationsEnabled && !appPreferences.notifications) {
- notificationsEnabled = false
- with(NotificationManagerCompat.from(this)) {
- cancel(STATS_NOTIFICATION_ID)
- }
- foregroundServiceNotification.setContentText(getString(R.string.foreground_service_info))
- NotificationManagerCompat.from(this).notify(FOREGROUND_NOTIFICATION_ID, foregroundServiceNotification.build())
- } else if (!notificationsEnabled && appPreferences.notifications) {
- notificationsEnabled = true
- }
- }
-
- private fun writeTripDataToFile(tripData: TripData, fileName: String) {
- val dir = File(applicationContext.filesDir, "TripData")
- if (!dir.exists()) {
- dir.mkdir()
- }
-
- try {
- val gpxFile = File(dir, "$fileName.json")
- val writer = FileWriter(gpxFile)
- writer.append(Gson().toJson(tripData))
- writer.flush()
- writer.close()
- InAppLogger.log("TRIP DATA: Saved $fileName.json in Thread ${Thread.currentThread().name}")
- } catch (e: java.lang.Exception) {
- e.printStackTrace()
- }
- }
-
- private fun readTripDataFromFile(fileName: String): TripData? {
-
- InAppLogger.log("TRIP DATA: Reading $fileName.json in Thread ${Thread.currentThread().name}")
- val startTime = System.currentTimeMillis()
- val dir = File(applicationContext.filesDir, "TripData")
- if (!dir.exists()) {
- InAppLogger.log("TRIP DATA: Directory TripData does not exist!")
- return null
- }
-
- val gpxFile = File(dir, "$fileName.json")
- if (!gpxFile.exists() && gpxFile.length() > 0) {
- InAppLogger.log("TRIP_DATA File $fileName.json does not exist!")
- return null
- }
-
- return try {
- InAppLogger.log("TRIP DATA: File size: %.1f kB".format(gpxFile.length() / 1024f))
-
- // val fileReader = FileReader(gpxFile)
- val tripData: TripData = Gson().fromJson(gpxFile.readText(), TripData::class.java)
- // fileReader.close()
-
- InAppLogger.log("TRIP DATA: Time to read: ${System.currentTimeMillis() - startTime} ms")
-
- tripData
-
- } catch (e: java.lang.Exception) {
- InAppLogger.log(e.toString())
- null
- }
- }
-}
\ No newline at end of file
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/services/LocCollector.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/services/LocCollector.kt
new file mode 100644
index 00000000..ba75459f
--- /dev/null
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/services/LocCollector.kt
@@ -0,0 +1,88 @@
+package com.ixam97.carStatsViewer.services
+
+import com.ixam97.carStatsViewer.*
+import android.app.*
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.*
+import com.google.android.gms.location.*
+
+
+class LocCollector : Service() {
+ companion object {
+ }
+
+ private lateinit var fusedLocationClient: FusedLocationProviderClient
+
+ override fun onCreate() {
+ super.onCreate()
+
+ fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
+ getLocationUpdates()
+ startLocationUpdates()
+ InAppLogger.log("GPS started")
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ stopLocationUpdates()
+ }
+
+ private lateinit var locationRequest: LocationRequest
+
+ private lateinit var locationCallback: LocationCallback
+
+ override fun onBind(intent: Intent): IBinder? {
+ return null
+ }
+
+
+ private fun getLocationUpdates() {
+ InAppLogger.log("getLocationUpdate")
+ fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
+ locationRequest = LocationRequest()
+ locationRequest.interval = 10_000
+ locationRequest.fastestInterval = 10_000
+ locationRequest.smallestDisplacement = 170f // 170 m = 0.1 mile
+ locationRequest.priority =
+ LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY
+ locationCallback = object : LocationCallback() {
+ override fun onLocationResult(locationResult: LocationResult?) {
+ locationResult ?: return
+
+ if (locationResult.locations.isNotEmpty()) {
+ // get latest location
+ val location = locationResult.lastLocation
+ val altitude = location.altitude
+ InAppLogger.log("LOCATION-altitude: " + altitude)
+ }
+ }
+ }
+ }
+
+ //start location updates
+ private fun startLocationUpdates() {
+ if (checkLocPermission()) {
+ fusedLocationClient.requestLocationUpdates(
+ locationRequest,
+ locationCallback,
+ null /* Looper */
+ )
+ }
+ }
+
+ // stop location updates
+ private fun stopLocationUpdates() {
+ fusedLocationClient.removeLocationUpdates(locationCallback)
+ }
+
+ //TO-DO: build solid permission check and request for location
+ private fun checkLocPermission(): Boolean {
+ if (checkSelfPermission(android.Manifest.permission.ACCESS_BACKGROUND_LOCATION) != PackageManager.PERMISSION_GRANTED){
+ InAppLogger.log("Location Permission missing!")
+ return false
+ }
+
+ return true
+ }
+}
\ No newline at end of file
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/utils/Exclude.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/utils/Exclude.kt
new file mode 100644
index 00000000..5308d377
--- /dev/null
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/utils/Exclude.kt
@@ -0,0 +1,8 @@
+package com.ixam97.carStatsViewer.utils
+
+import java.lang.annotation.Retention
+import java.lang.annotation.RetentionPolicy
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(AnnotationTarget.FIELD)
+annotation class Exclude
\ No newline at end of file
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/utils/StringFormatters.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/utils/StringFormatters.kt
new file mode 100644
index 00000000..673da664
--- /dev/null
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/utils/StringFormatters.kt
@@ -0,0 +1,85 @@
+package com.ixam97.carStatsViewer.utils
+
+import com.ixam97.carStatsViewer.appPreferences.AppPreferences
+import com.ixam97.carStatsViewer.enums.DistanceUnitEnum
+import java.util.*
+import java.util.concurrent.TimeUnit
+
+object StringFormatters {
+
+ private lateinit var dateFormat: java.text.DateFormat
+ private lateinit var timeFormat: java.text.DateFormat
+ private var consumptionUnit: Boolean = false
+ private lateinit var distanceUnit : DistanceUnitEnum
+
+ fun initFormatter(
+ dateFormat: java.text.DateFormat,
+ timeFormat: java.text.DateFormat,
+ consumptionUnit: Boolean,
+ distanceUnit : DistanceUnitEnum
+ ) {
+ this.dateFormat = dateFormat
+ this.timeFormat = timeFormat
+ this.consumptionUnit = consumptionUnit
+ this.distanceUnit = distanceUnit
+ }
+
+ /** Divides a Float by 1000 and rounds it up to one decimal point to be on par with board computer */
+ private fun kiloRounder(number: Float): Float {
+ return (number.toInt() / 100).toFloat() / 10
+ }
+
+ fun getDateString(date: Date?): String {
+ if (date == null) return "-/-"
+ return "${dateFormat.format(date)}, ${timeFormat.format(date)}"
+ }
+
+ fun getDateString(tripStartDate: Calendar): String {
+ return "${dateFormat.format(tripStartDate.time)}, ${timeFormat.format(tripStartDate.time)}"
+ }
+
+ fun getEnergyString(usedEnergy: Float): String {
+ if (!consumptionUnit) {
+ return "%.1f kWh".format(
+ Locale.ENGLISH,
+ kiloRounder(usedEnergy))
+ }
+ return "${usedEnergy.toInt()} Wh"
+ }
+
+ fun getTraveledDistanceString(traveledDistance: Float): String {
+ return "%.1f %s".format(Locale.ENGLISH, kiloRounder(distanceUnit.toUnit(traveledDistance)), distanceUnit.unit())
+ }
+
+ fun getAvgConsumptionString(usedEnergy: Float, traveledDistance: Float): String {
+ val avgConsumption = distanceUnit.asUnit(usedEnergy / (traveledDistance / 1000))
+ val unitString = when {
+ consumptionUnit -> "Wh/%s".format(distanceUnit.unit())
+ else -> "kWh/100%s".format(distanceUnit.unit())
+ }
+
+ if (traveledDistance <= 0) {
+ return "-/- $unitString"
+ }
+ if (!consumptionUnit) {
+ return "%.1f %s".format(
+ Locale.ENGLISH,
+ (avgConsumption) / 10,
+ unitString)
+ }
+ return "${(avgConsumption).toInt()} $unitString"
+ }
+
+ fun getElapsedTimeString(elapsedTime: Long): String {
+ return String.format("%02d:%02d:%02d",
+ TimeUnit.MILLISECONDS.toHours(elapsedTime),
+ TimeUnit.MILLISECONDS.toMinutes(elapsedTime) % TimeUnit.HOURS.toMinutes(1),
+ TimeUnit.MILLISECONDS.toSeconds(elapsedTime) % TimeUnit.MINUTES.toSeconds(1))
+ }
+
+ fun getTemperatureString(temperature: Float?): String {
+ if (temperature == null) return "-/-"
+ val unitString = "°C"
+ return "%d %s".format(temperature.toInt(), unitString)
+ }
+}
\ No newline at end of file
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/views/GageView.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/views/GageView.kt
index b58ab8ab..9f823b1e 100644
--- a/automotive/src/main/java/com/ixam97/carStatsViewer/views/GageView.kt
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/views/GageView.kt
@@ -10,6 +10,10 @@ import kotlin.math.roundToInt
class GageView(context: Context, attrs: AttributeSet) : View(context, attrs) {
+ companion object {
+ var descriptionTextSize = 30f
+ var valueTextSize = 100f
+ }
var gageName : String = "gageName"
set(value) {
@@ -67,13 +71,13 @@ class GageView(context: Context, attrs: AttributeSet) : View(context, attrs) {
private val namePaint = Paint().apply {
color = Color.GRAY
- textSize = dpToPx(30f)
+ textSize = descriptionTextSize
isAntiAlias = true
}
private val unitPaint = Paint().apply {
color = getPrimaryColor()
- textSize = dpToPx(30f)
+ textSize = descriptionTextSize
isAntiAlias = true
}
@@ -100,13 +104,13 @@ class GageView(context: Context, attrs: AttributeSet) : View(context, attrs) {
private val valuePaint = Paint().apply {
color = Color.WHITE
- textSize = dpToPx(100f)
+ textSize = valueTextSize
isAntiAlias = true
}
private val xTextMargin = dpToPx(15f)
private val yTextMargin = dpToPx(10f)
- private val gageWidth = dpToPx(60f)
+ private val gageWidth = 2 * descriptionTextSize
private val nameYPos = namePaint.textSize * 0.76f
private val valueYPos = nameYPos + valuePaint.textSize * 0.9f
diff --git a/automotive/src/main/java/com/ixam97/carStatsViewer/views/PlotView.kt b/automotive/src/main/java/com/ixam97/carStatsViewer/views/PlotView.kt
index 7fc7d24e..65557e3b 100644
--- a/automotive/src/main/java/com/ixam97/carStatsViewer/views/PlotView.kt
+++ b/automotive/src/main/java/com/ixam97/carStatsViewer/views/PlotView.kt
@@ -14,25 +14,29 @@ import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getColor
import com.ixam97.carStatsViewer.R
+import com.ixam97.carStatsViewer.appPreferences.AppPreferences
import com.ixam97.carStatsViewer.plot.enums.*
import com.ixam97.carStatsViewer.plot.graphics.*
import com.ixam97.carStatsViewer.plot.objects.*
import java.util.concurrent.TimeUnit
+import kotlin.math.absoluteValue
import kotlin.math.roundToInt
-class PlotView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
+class PlotView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
companion object {
- const val textSize = 26f
+ var textSize = 26f
+ var xMargin = 0
+ var yMargin = 0
}
- var xMargin: Int = 100
+ /*var xMargin: Int = 100
set(value) {
val diff = value != field
if (value > 0) {
field = value
if (diff) invalidate()
}
- }
+ }*/
var xLineCount: Int = 6
set(value) {
@@ -43,14 +47,14 @@ class PlotView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
}
}
- var yMargin: Int = 60
+ /*var yMargin: Int = 60
set(value) {
val diff = value != field
if (value > 0) {
field = value
if (diff) invalidate()
}
- }
+ }*/
var yLineCount: Int = 5
set(value) {
@@ -75,7 +79,7 @@ class PlotView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
if (diff) invalidate()
}
- var dimensionRestrictionTouchInterval: Long? = null
+ var dimensionRestrictionMin: Long? = null
var dimensionRestriction: Long? = null
set(value) {
val diff = value != field
@@ -83,7 +87,6 @@ class PlotView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
if (diff) invalidate()
}
- var dimensionShiftTouchInterval: Long? = null
var dimensionShift: Long? = null
set(value) {
val diff = value != field
@@ -105,6 +108,29 @@ class PlotView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
if (diff) invalidate()
}
+ private var dimensionHighlightAt : Float? = null
+ set(value) {
+ val coerce = value
+ ?.coerceAtLeast(xMargin.toFloat())
+ ?.coerceAtMost((width - xMargin).toFloat())
+
+ val diff = coerce != field
+ field = coerce
+ if (diff) {
+ dimensionHighlightAtPercentage = when (coerce) {
+ null -> null
+ else -> (1f / (width.toFloat() - 2 * xMargin) * (coerce - xMargin))
+ .coerceAtLeast(0f)
+ .coerceAtMost(1f)
+ }
+ invalidate()
+ }
+ }
+
+ private var dimensionHighlightAtPercentage : Float? = null
+
+ private var dimensionHighlightValue : HashMap> = HashMap()
+
var visibleMarkerTypes: HashSet = HashSet()
var sessionGapRendering : PlotSessionGapRendering = PlotSessionGapRendering.JOIN
@@ -114,22 +140,24 @@ class PlotView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
if (diff) invalidate()
}
- private val plotLines = ArrayList()
+ private val plotLines = ArrayList>()
private var plotMarkers : PlotMarkers? = null
- private val plotPaint = ArrayList()
-
private lateinit var labelPaint: Paint
private lateinit var labelLinePaint: Paint
private lateinit var borderLinePaint: Paint
private lateinit var baseLinePaint: Paint
private lateinit var backgroundPaint: Paint
+ private lateinit var dimensionHighlightLinePaint: Paint
private var markerPaint = HashMap()
private var markerIcon = HashMap()
+ private val appPreferences : AppPreferences
+
init {
setupPaint()
+ appPreferences = AppPreferences(context)
}
// Setup paint with color and stroke styles
@@ -155,11 +183,9 @@ class PlotView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
backgroundPaint.color = Color.BLACK
backgroundPaint.style = Paint.Style.FILL
- val plotColors = listOf(null, Color.CYAN, Color.BLUE, Color.RED)
-
- for (color in plotColors) {
- plotPaint.add(PlotPaint.byColor(color ?: typedValue.data, textSize))
- }
+ dimensionHighlightLinePaint = Paint(borderLinePaint)
+ dimensionHighlightLinePaint.strokeWidth = 3f
+ dimensionHighlightLinePaint.pathEffect = DashPathEffect(floatArrayOf(5f, 10f), 0f)
for (type in PlotMarkerType.values()) {
when (type) {
@@ -208,7 +234,7 @@ class PlotView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
fun reset() {
for (item in plotLines) {
- item.reset()
+ item.first.reset()
}
plotMarkers?.markers?.clear()
@@ -216,21 +242,13 @@ class PlotView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
invalidate()
}
- fun addPlotLine(plotLine: PlotLine) {
- if (plotLine.plotPaint == null) {
- plotLine.plotPaint = plotPaint[plotLines.size]
- }
-
- if (plotLine.secondaryPlotPaint == null) {
- plotLine.secondaryPlotPaint = PlotPaint.byColor(Color.GREEN, textSize)
- }
-
- plotLines.add(plotLine)
+ fun addPlotLine(plotLine: PlotLine, plotLinePaint: PlotLinePaint) {
+ plotLines.add(Pair(plotLine, plotLinePaint))
invalidate()
}
fun removePlotLine(plotLine: PlotLine?) {
- plotLines.remove(plotLine)
+ plotLines.removeAll { it.first == plotLine }
invalidate()
}
@@ -244,6 +262,22 @@ class PlotView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
invalidate()
}
+ private fun x(index: Any?, min: Any, max: Any, maxX: Float): Float? {
+ if (min is Float) {
+ return x(index as Float?, min, max as Float, maxX)
+ }
+
+ if (min is Long) {
+ return x(index as Long?, min, max as Long, maxX)
+ }
+
+ return null
+ }
+
+ private fun x(index: Long?, min: Long, max: Long, maxX: Float): Float? {
+ return x(PlotLineItem.cord(index, min, max), maxX)
+ }
+
private fun x(index: Float?, min: Float, max: Float, maxX: Float): Float? {
return x(PlotLineItem.cord(index, min, max), maxX)
}
@@ -281,72 +315,103 @@ class PlotView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
override fun onScale(scaleGestureDetector: ScaleGestureDetector): Boolean {
val spanX: Float = scaleGestureDetector.currentSpanX
+ val restrictionMin = dimensionRestrictionMin ?: return true
- val restrictionInterval = dimensionRestrictionTouchInterval
- if (restrictionInterval != null && lastRestriction != null && !(lastSpanX/spanX).isInfinite()) {
- dimensionRestriction = (((lastRestriction!!.toFloat() * (lastSpanX / spanX)).toLong()/restrictionInterval) * restrictionInterval)
- .coerceAtMost(touchDimensionMax + (restrictionInterval - touchDimensionMax % restrictionInterval))
- .coerceAtLeast(restrictionInterval)
- dimensionShiftTouchInterval = dimensionRestriction!! / dimensionRestrictionTouchInterval!! * 1_000L
- }
+ if (lastRestriction == null || (lastSpanX/spanX).isInfinite()) return true
+
+ val targetDimensionRestriction = ((lastRestriction!!.toFloat() * (lastSpanX / spanX)).toLong())
+ .coerceAtMost(touchDimensionMax)
+ .coerceAtLeast(restrictionMin)
+
+ dimensionRestriction = targetDimensionRestriction
+
+ val shift = dimensionShift ?: return true
+
+ dimensionShift = shift
+ .coerceAtMost(touchDimensionMax - targetDimensionRestriction)
+ .coerceAtLeast(0L)
return true
}
}
- private val mGestureListener = object : GestureDetector.SimpleOnGestureListener() {
+ private val mScrollGestureListener = object : GestureDetector.SimpleOnGestureListener() {
override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
- val shiftInterval = dimensionShiftTouchInterval
- if (shiftInterval != null) {
- touchDimensionShiftDistance += - distanceX
- dimensionShift = (touchDimensionShift + (touchDimensionShiftDistance / touchActionDistance.coerceAtLeast(1L)).toLong() * shiftInterval)
- .coerceAtMost(touchDimensionMax + (shiftInterval - touchDimensionMax % shiftInterval) - (dimensionRestriction!! - 1))
- .coerceAtLeast(0L)
- }
+ touchDimensionShiftDistance += distanceX * touchDistanceMultiplier
+
+ dimensionShift = (touchDimensionShift + touchDimensionShiftDistance * touchDimensionShiftByPixel).toLong()
+ .coerceAtMost(touchDimensionMax - (dimensionRestriction ?: 0L))
+ .coerceAtLeast(0L)
return true
}
}
- private val mScrollDetector = GestureDetector(context, mGestureListener)
- private val mScaleDetector = ScaleGestureDetector(context!!, mScaleGestureDetector)
-
- private var touchDimensionShift : Long = 0L
- private var touchDimensionShiftDistance : Float = 0f
+ private val mTapGestureListener = object : GestureDetector.SimpleOnGestureListener() {
+ override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
+ if (dimensionHighlightAt != null) {
+ dimensionHighlightAt = e.x
+ }
+ return super.onSingleTapConfirmed(e)
+ }
- private var touchDimensionRestriction : Long = 0L
- private var touchDimensionRestrictionDistance : Float = 0f
+ override fun onDoubleTap(e: MotionEvent): Boolean {
+ dimensionHighlightAt = when (dimensionHighlightAt) {
+ null -> e.x
+ else -> null
+ }
+ return super.onDoubleTap(e)
+ }
+ }
- private var touchActionDistance : Long = 1L
+ private val mScrollDetector = GestureDetector(context, mScrollGestureListener)
+ private val mTapDetector = GestureDetector(context, mTapGestureListener)
+ private val mScaleDetector = ScaleGestureDetector(context, mScaleGestureDetector)
+ // invert direction
+ private var touchDistanceMultiplier : Float = -1f
+ private var touchDimensionShift : Long = 0L
+ private var touchDimensionShiftDistance : Float = 0f
+ private var touchDimensionShiftByPixel : Float = 0f
private var touchDimensionMax : Long = 0L
+ private var touchGesture : Boolean = false
override fun onTouchEvent(ev: MotionEvent): Boolean {
- if (dimensionRestriction == null) return true
- if (dimensionShiftTouchInterval == null && dimensionRestrictionTouchInterval == null) return true
+ val restriction = dimensionRestriction ?: return true
+ val min = dimensionRestrictionMin ?: return true
when (ev.action) {
- MotionEvent.ACTION_DOWN, MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
+ MotionEvent.ACTION_DOWN -> {
+ touchDistanceMultiplier = when (dimension.toPlotDirection()) {
+ PlotDirection.LEFT_TO_RIGHT -> 1f
+ PlotDirection.RIGHT_TO_LEFT -> -1f
+ }
+ touchDimensionShift = dimensionShift ?: 0L
touchDimensionShiftDistance = 0f
- touchDimensionRestrictionDistance = 0f
+ touchDimensionShiftByPixel = restriction.toFloat() / width.toFloat()
- touchDimensionShift = dimensionShift ?: 0L
- touchDimensionRestriction = dimensionRestriction ?: 0L
+ val dimensionMax = plotLines.mapNotNull { it.first.distanceDimension(dimension) }.maxOfOrNull { it }?.toLong() ?: return true
+ touchDimensionMax = dimensionMax + (min - dimensionMax % min)
- touchDimensionMax = (plotLines.mapNotNull { it.distanceDimension(dimension) }.max() ?: 0f).toLong()
+ touchGesture = true
+ }
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
+ touchGesture = false
}
}
- mScrollDetector.onTouchEvent(ev)
- mScaleDetector.onTouchEvent(ev)
+ mTapDetector.onTouchEvent(ev)
+
+ if (touchGesture) {
+ mScrollDetector.onTouchEvent(ev)
+ mScaleDetector.onTouchEvent(ev)
+ }
+
return true
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
-
- touchActionDistance = (((width - 2 * xMargin).toFloat() / xLineCount.toFloat()) * 0.75f).toLong()
-
alignZero()
drawBackground(canvas)
drawXLines(canvas)
@@ -355,19 +420,13 @@ class PlotView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
drawYLines(canvas)
}
- private fun drawBackground(canvas: Canvas) {
- val maxX = width.toFloat()
- val maxY = height.toFloat()
-
- canvas.drawRect(xMargin.toFloat(), yMargin.toFloat(), maxX - xMargin, maxY - yMargin, backgroundPaint)
- }
-
private fun alignZero() {
- if (plotLines.none { it.alignZero }) return
+ if (plotLines.none { it.first.alignZero }) return
var zeroAt : Float? = null
for (index in plotLines.indices) {
- val line = plotLines[index]
+ val pair = plotLines[index]
+ val line = pair?.first ?: continue
if (index == 0) {
if (line.isEmpty() || !line.Visible) return
@@ -388,145 +447,266 @@ class PlotView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
}
}
+ private fun drawBackground(canvas: Canvas) {
+ val maxX = width.toFloat()
+ val maxY = height.toFloat()
+
+ canvas.drawRect(xMargin.toFloat(), yMargin.toFloat(), maxX - xMargin, maxY - yMargin, backgroundPaint)
+ }
+
+ private fun drawXLines(canvas: Canvas) {
+ val maxX = width.toFloat()
+ val maxY = height.toFloat()
+
+ val distanceDimension = when {
+ dimensionRestriction != null -> dimensionRestriction!!.toFloat()
+ else -> plotLines.mapNotNull { it.first.distanceDimension(dimension, dimensionRestriction, dimensionShift) }.maxOfOrNull { it }
+ } ?: return
+
+ val sectionLength = distanceDimension / (xLineCount - 1)
+ val baseShift = dimensionShift ?: 0L
+ val sectionShift = baseShift % sectionLength
+
+ for (i in -1 until xLineCount + 1) {
+ val between = i in 0 until xLineCount
+
+ val relativeShift = when {
+ between -> sectionShift % sectionLength
+ else -> 0f
+ }
+
+ val leftToRight = (sectionLength * i.coerceAtLeast(0)) + baseShift - relativeShift
+ val rightToLeft = distanceDimension - leftToRight + (2 * (baseShift - relativeShift))
+
+ val corXFactor = when (dimension.toPlotDirection()) {
+ PlotDirection.LEFT_TO_RIGHT -> -1
+ PlotDirection.RIGHT_TO_LEFT -> 1
+ }
+
+ val xPos = (sectionLength * i) + (relativeShift * corXFactor)
+ if (xPos < 0f || xPos > distanceDimension) continue
+
+ val cordX = x(xPos, 0f, distanceDimension, maxX)!!
+ val cordY = maxY - yMargin
+
+ drawXLine(canvas, cordX, maxY, labelLinePaint)
+
+ val label = when (dimension) {
+ PlotDimension.INDEX -> label(leftToRight, PlotLineLabelFormat.NUMBER)
+ PlotDimension.DISTANCE -> label(rightToLeft, PlotLineLabelFormat.DISTANCE)
+ PlotDimension.TIME -> label(leftToRight, PlotLineLabelFormat.TIME)
+ PlotDimension.STATE_OF_CHARGE -> label(leftToRight, PlotLineLabelFormat.NUMBER)
+ }
+
+ val bounds = Rect()
+ labelPaint.getTextBounds(label, 0, label.length, bounds)
+
+ if (between) {
+ canvas.drawText(
+ label,
+ cordX - bounds.width() / 2,
+ cordY + yMargin / 2 + bounds.height() / 2,
+ labelPaint
+ )
+ }
+ }
+
+ drawXLine(canvas, x(0f, 0f, 1f, maxX), maxY, borderLinePaint)
+ drawXLine(canvas, x(1f, 0f, 1f, maxX), maxY, borderLinePaint)
+
+ val dimensionHighlight = dimensionHighlightAt
+ if (dimensionHighlight != null) {
+ drawXLine(canvas, dimensionHighlight, maxY, dimensionHighlightLinePaint)
+ }
+ }
+
+ private fun drawXLine(canvas: Canvas, cord: Float?, maxY: Float, paint: Paint?) {
+ if (cord == null) return
+ val path = Path()
+ path.moveTo(cord, yMargin.toFloat())
+ path.lineTo(cord, maxY - yMargin)
+ canvas.drawPath(path, paint ?: labelLinePaint)
+ }
+
+ private fun drawYBaseLines(canvas: Canvas) {
+ val maxX = width.toFloat()
+ val maxY = height.toFloat()
+
+ for (i in 0 until yLineCount) {
+ val cordY = y(i.toFloat(), 0f, yLineCount.toFloat() - 1, maxY)!!
+
+ if (i in 1 until yLineCount - 1) {
+ drawYLine(canvas, cordY, maxX, labelLinePaint)
+ } else {
+ drawYLine(canvas, cordY, maxX, borderLinePaint)
+ }
+ }
+ }
+
private fun drawPlot(canvas: Canvas) {
val maxX = width.toFloat()
val maxY = height.toFloat()
- var index = 0
- for (line in plotLines.filter { it.Visible }) {
- for (secondaryDimension in arrayListOf(null, secondaryDimension).distinct()) {
- if (line.isEmpty()) continue
+ for (pair in plotLines.filter { it.first.Visible }) {
+ val line = pair.first
+ val plotPaint = pair.second ?: continue
- val dataPoints = line.getDataPoints(dimension, dimensionRestriction, dimensionShift)
- if (dataPoints.isEmpty()) continue
+ if (!dimensionHighlightValue.containsKey(line)) dimensionHighlightValue[line] = HashMap()
- val configuration = when {
- secondaryDimension != null -> PlotGlobalConfiguration.SecondaryDimensionConfiguration[secondaryDimension]
- else -> line.Configuration
- } ?: continue
+ for (drawBackground in listOf(true, false)) {
+ for (secondaryDimension in arrayListOf(null, secondaryDimension).distinct().reversed()) {
+ dimensionHighlightValue[line]?.set(secondaryDimension, null)
- val paint = when {
- secondaryDimension != null -> line.secondaryPlotPaint ?: line.plotPaint
- else -> line.plotPaint
- } ?: continue
+ if (line.isEmpty()) continue
- val minValue = line.minValue(dataPoints, secondaryDimension)!!
- val maxValue = line.maxValue(dataPoints, secondaryDimension)!!
+ val configuration = when {
+ secondaryDimension != null -> PlotGlobalConfiguration.SecondaryDimensionConfiguration[secondaryDimension]
+ else -> line.Configuration
+ } ?: continue
- val minDimension = line.minDimension(dataPoints, dimension, dimensionRestriction)
- val maxDimension = line.maxDimension(dataPoints, dimension, dimensionRestriction)
+ if (drawBackground && configuration.Range.backgroundZero == null) continue
- val smoothing = when {
- dimensionSmoothing != null -> dimensionSmoothing
- dimensionSmoothingPercentage != null -> line.distanceDimension(dataPoints, dimension, dimensionRestriction) * dimensionSmoothingPercentage!!
- else -> null
- }
+ val dataPoints = line.getDataPoints(dimension, dimensionRestriction, dimensionShift)
+ if (dataPoints.isEmpty()) continue
- val zeroCord = y(configuration.Range.backgroundZero, minValue, maxValue, maxY)
+ val minDimension = line.minDimension(dimension, dimensionRestriction, dimensionShift) ?: continue
+ val maxDimension = line.maxDimension(dimension, dimensionRestriction, dimensionShift) ?: continue
- val plotPointCollection = toPlotPointCollection(line, secondaryDimension, minValue, maxValue, minDimension, maxDimension, maxX, maxY, smoothing)
+ val paint = plotPaint.bySecondaryDimension(secondaryDimension) ?: continue
- drawPlotPointCollection(canvas, plotPointCollection, maxX, maxY, paint, zeroCord)
+ val minValue = line.minValue(dataPoints, secondaryDimension)!!
+ val maxValue = line.maxValue(dataPoints, secondaryDimension)!!
- if (index == 0 && plotMarkers?.markers?.isNotEmpty() ?: false) {
- drawMarker(canvas, dataPoints, line, minDimension, maxDimension, maxX)
- }
+ val smoothing = when {
+ dimensionSmoothing != null -> dimensionSmoothing
+ dimensionSmoothingPercentage != null -> (line.distanceDimensionMinMax(dimension, minDimension, maxDimension) ?: 0f) * dimensionSmoothingPercentage!!
+ else -> null
+ }
+
+ val zeroCord = y(configuration.Range.backgroundZero, minValue, maxValue, maxY)
+
+ val plotPointCollection = toPlotPointCollection(line, secondaryDimension, minValue, maxValue, minDimension, maxDimension, maxX, maxY, smoothing)
+
+ drawPlotPointCollection(canvas, plotPointCollection, maxX, maxY, paint, drawBackground, zeroCord)
- index++
+ if (!drawBackground && secondaryDimension == null && plotMarkers?.markers?.isNotEmpty() == true) {
+ drawMarker(canvas, minDimension, maxDimension, maxX, maxY)
+ }
+ }
}
}
}
- private fun toPlotPointCollection(line: PlotLine, secondaryDimension: PlotSecondaryDimension?, minValue: Float, maxValue: Float, minDimension: Any, maxDimension: Any, maxX: Float, maxY: Float, smoothing: Float?): ArrayList> {
+ private fun toPlotPointCollection(line: PlotLine, secondaryDimension: PlotSecondaryDimension?, minValue: Float, maxValue: Float, minDimension: Any, maxDimension: Any, maxX: Float, maxY: Float, smoothing: Float?): ArrayList> {
val dataPointsUnrestricted = line.getDataPoints(dimension)
val plotLineItemPointCollection = line.toPlotLineItemPointCollection(dataPointsUnrestricted, dimension, smoothing, minDimension, maxDimension)
- val plotPointCollection = ArrayList>()
+ val plotPointCollection = ArrayList>()
for (collection in plotLineItemPointCollection) {
if (collection.isEmpty()) continue
- val plotPoints = ArrayList()
+ val dimensionHighlight = dimensionHighlightAtPercentage
+ if (dimensionHighlight != null && dimensionHighlight in collection.minOf { it.x } .. collection.maxOf { it.x }) {
+ val point = collection.minByOrNull { (it.x - dimensionHighlight).absoluteValue }
+ if (point != null) {
+ val points = collection.filter { (it.x - point.x).absoluteValue <= (dimensionSmoothingPercentage ?: 0f) }
+ val value = line.averageValue(points.map { it.y }, dimension, secondaryDimension)
+
+ dimensionHighlightValue[line]?.set(secondaryDimension, value)
+ }
+ }
+
+ val plotPoints = ArrayList()
for (group in collection.groupBy { it.group }) {
val plotPoint = when {
group.value.size <= 1 -> {
val point = group.value.first()
- PlotPoint(
- x(point.x, 0f, 1f, maxX)!!,
- y(point.y.bySecondaryDimension(secondaryDimension), minValue, maxValue, maxY)!!
- )
+
+ val value = point.y.bySecondaryDimension(secondaryDimension)
+ val x = x(point.x, 0f, 1f, maxX) ?: continue
+ val y = y(value, minValue, maxValue, maxY) ?: continue
+
+ PointF(x, y)
}
else -> {
- val x = when (plotPoints.size) {
- 0 -> group.value.minBy { it.x }.x
- else -> group.value.maxBy { it.x }.x
- }
+ val xGroup = when (plotPoints.size) {
+ 0 -> group.value.minOfOrNull { it.x }
+ else -> group.value.maxOfOrNull { it.x }
+ } ?: continue
- PlotPoint(
- x(x, 0f, 1f, maxX)!!,
- y(line.averageValue(group.value.map { it.y }, dimension, secondaryDimension), minValue, maxValue, maxY)!!
- )
+ val value = line.averageValue(group.value.map { it.y }, dimension, secondaryDimension)
+ val x = x(xGroup, 0f, 1f, maxX) ?: continue
+ val y = y(value, minValue, maxValue, maxY) ?: continue
+
+ PointF(x, y)
}
}
plotPoints.add(plotPoint)
}
- plotPointCollection.add(plotPoints)
+ if (plotPoints.isNotEmpty()) plotPointCollection.add(plotPoints)
}
return plotPointCollection
}
- private fun drawPlotPointCollection(canvas: Canvas, plotPointCollection: ArrayList>, maxX: Float, maxY: Float, paint: PlotPaint, zeroCord: Float?) {
- // restrict canvas drawing region
- canvas.save()
- canvas.clipOutRect(0f, 0f, maxX, yMargin.toFloat()) // TOP
- canvas.clipOutRect(0f, maxY - yMargin, maxX, maxY) // BOTTOM
- canvas.clipOutRect(0f, 0f, xMargin.toFloat(), maxY) // LEFT
- canvas.clipOutRect(maxX - xMargin, 0f, maxX, maxY) // RIGHT
+ private fun drawPlotPointCollection(canvas: Canvas, plotPointCollection: ArrayList>, maxX: Float, maxY: Float, paint: PlotPaint, drawBackground: Boolean, zeroCord: Float?) {
+
+ restrictCanvas(canvas, maxX, maxY)
- val joinedPlotPoints = ArrayList()
+ val joinedPlotPoints = ArrayList()
for (plotPointIndex in plotPointCollection.indices) {
val plotPoints = plotPointCollection[plotPointIndex]
when (sessionGapRendering) {
PlotSessionGapRendering.JOIN -> joinedPlotPoints.addAll(plotPoints)
- else -> drawPlotPoints(canvas, plotPoints, paint, zeroCord, plotPointIndex == plotPointCollection.size - 1)
+ else -> drawPlotPoints(canvas, plotPoints, paint, drawBackground, zeroCord, plotPointIndex == plotPointCollection.size - 1)
}
}
if (joinedPlotPoints.isNotEmpty()) {
- drawPlotPoints(canvas, joinedPlotPoints, paint, zeroCord)
+ drawPlotPoints(canvas, joinedPlotPoints, paint, drawBackground, zeroCord)
+ }
+
+ if (sessionGapRendering == PlotSessionGapRendering.GAP) {
+ var last: PointF? = null
+ for (collection in plotPointCollection) {
+ if (last != null) {
+ val first = collection.first()
+ drawLine(canvas, last.x, last.y, first.x, first.y, paint.PlotGapSecondary)
+ }
+ last = collection.last()
+ }
}
canvas.restore()
}
- private fun drawPlotPoints(canvas: Canvas, plotPoints : ArrayList, plotPaint: PlotPaint, zeroCord: Float?, lastPlot: Boolean = true) {
- if (zeroCord != null) {
- val backgroundPaint = when (lastPlot) {
+ private fun drawPlotPoints(canvas: Canvas, plotPoints : ArrayList, plotPaint: PlotPaint, drawBackground: Boolean, zeroCord: Float?, lastPlot: Boolean = true) {
+ val linePaint = when (lastPlot) {
+ true -> when (drawBackground) {
true -> plotPaint.PlotBackground
- else -> plotPaint.PlotBackgroundSecondary
+ else -> plotPaint.Plot
+ }
+ else -> when (drawBackground) {
+ true -> plotPaint.PlotBackgroundSecondary
+ else -> plotPaint.PlotSecondary
}
- drawPlotLine(canvas, backgroundPaint, plotPoints, zeroCord, true)
- }
-
- val plotPaint = when (lastPlot) {
- true -> plotPaint.Plot
- else -> plotPaint.PlotSecondary
}
- drawPlotLine(canvas, plotPaint, plotPoints, zeroCord)
+ drawPlotLine(canvas, linePaint, plotPaint.TransparentColor, plotPoints, drawBackground, zeroCord)
}
- private fun drawPlotLine(canvas : Canvas, paint : Paint, plotPoints : ArrayList, zeroCord: Float?, background: Boolean = false) {
+ private fun drawPlotLine(canvas : Canvas, paint : Paint, transparentColor: Int, plotPoints : ArrayList, drawBackground: Boolean, zeroCord: Float?) {
if (plotPoints.isEmpty()) return
+ if (drawBackground && zeroCord == null) return
val path = Path()
- var firstPoint: PlotPoint? = null
- var prevPoint: PlotPoint? = null
+ var firstPoint: PointF? = null
+ var prevPoint: PointF? = null
for (i in plotPoints.indices) {
val point = plotPoints[i]
@@ -553,12 +733,14 @@ class PlotView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
path.lineTo(prevPoint!!.x, prevPoint.y)
- if (background && zeroCord != null) {
+ if (drawBackground && zeroCord != null) {
path.lineTo(prevPoint.x, zeroCord)
path.lineTo(firstPoint!!.x, zeroCord)
+
+ paint.shader = LinearGradient(0f, 0f, 0f, height.toFloat(), paint.color, transparentColor, Shader.TileMode.MIRROR)
}
- if (!background && sessionGapRendering == PlotSessionGapRendering.CIRCLE){
+ if (!drawBackground && sessionGapRendering == PlotSessionGapRendering.CIRCLE){
drawPlotLineMarker(canvas, firstPoint, paint)
drawPlotLineMarker(canvas, prevPoint, paint)
}
@@ -566,81 +748,102 @@ class PlotView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
canvas.drawPath(path, paint)
}
- private fun drawPlotLineMarker(canvas: Canvas, point: PlotPoint?, paint: Paint) {
+ private fun drawPlotLineMarker(canvas: Canvas, point: PointF?, paint: Paint) {
if (point == null) return
canvas.drawCircle(point.x, point.y, 3f, paint)
}
- private fun drawMarker(canvas: Canvas, dataPoints: List, line: PlotLine, minDimension: Any, maxDimension: Any, maxX: Float) {
- val markers = plotMarkers!!.markers.filter { visibleMarkerTypes.contains(it.MarkerType) }
+ private fun drawMarker(canvas: Canvas, minDimension: Any, maxDimension: Any, maxX: Float, maxY: Float) {
+
+ val markers = plotMarkers?.markers?.filter {
+ val isNotOnEdge = when (dimension) {
+ PlotDimension.DISTANCE -> (minDimension as Float) < it.StartDistance && it.StartDistance < (maxDimension as Float)
+ PlotDimension.TIME -> false // (minDimension as Long) < it.StartTime && it.StartTime < (maxDimension as Long)
+ else -> false
+ }
+ visibleMarkerTypes.contains(it.MarkerType) && isNotOnEdge
+ } ?: return
var markerXLimit = 0f
val markerTimes = HashMap()
- for (markerGroup in markers.groupBy { line.x(dataPoints, it.StartTime, PlotDimension.TIME, dimension, minDimension, maxDimension) }) {
+ restrictCanvas(canvas, maxX, maxY, yArea = false)
+
+ for (markerGroup in markers.groupBy { it.group(dimension, dimensionSmoothing) }) {
if (markerGroup.key == null) continue
- val markerSorted = markerGroup.value.sortedBy { it.MarkerType }
- val markerTypeGroup = markerSorted.groupBy { it.MarkerType }
- val markerType = markerSorted.first().MarkerType
+ val markerType = markerGroup.value.minOfOrNull { it.MarkerType } ?: continue
- val markers = markerTypeGroup[markerType]!!
+ val startX = markerGroup.value.mapNotNull { x(it.startByDimension(dimension), minDimension, maxDimension, maxX) }.minOfOrNull { it } ?: continue
+ val endX = markerGroup.value.mapNotNull { x(it.endByDimension(dimension), minDimension, maxDimension, maxX) }.maxOfOrNull { it }
- val x1 = x(markerGroup.key, 0f, 1f, maxX)
- val x2 = when (dimension) {
- PlotDimension.DISTANCE -> x1
- else -> x(line.x( dataPoints, markers.last().EndTime, PlotDimension.TIME, dimension, minDimension, maxDimension), 0f, 1f, maxX)
- }
+ if (startX < xMargin) continue
- if (x1 != null && markers.none { it.EndTime == null }) {
- if (markerXLimit == 0f) {
- markerXLimit = x1
- }
+ if (markerXLimit == 0f) {
+ markerXLimit = startX
+ }
- markerXLimit = drawMarkerLabel(canvas, markerTimes, markerXLimit, x1)
+ markerXLimit = drawMarkerLabel(canvas, markerTimes, startX, markerXLimit)
- drawMarkerLine(canvas, markerType, x1, -1)
- drawMarkerLine(canvas, markerType, x2, 1)
+ drawMarkerLine(canvas, markerType, startX, -1)
+ drawMarkerLine(canvas, markerType, endX, 1)
- val diff = markers.sumOf { (it.EndTime ?: it.StartTime) - it.StartTime }
+ for (markerDimensionGroup in markerGroup.value.groupBy { it.group(dimension) }) {
+ val markerDimensionType = markerDimensionGroup.value.minOfOrNull { it.MarkerType } ?: continue
- markerTimes[markerType] = (markerTimes[markerType] ?: 0L) + diff
+ for (marker in markerDimensionGroup.value) {
+ markerTimes[markerDimensionType] = (markerTimes[markerDimensionType] ?: 0L) + ((marker.EndTime ?: marker.StartTime) - marker.StartTime)
+ }
}
}
- drawMarkerLabel(canvas, markerTimes, markerXLimit, maxX)
+ drawMarkerLabel(canvas, markerTimes, Float.MAX_VALUE, markerXLimit)
+
+ canvas.restore()
}
- private fun drawMarkerLabel(canvas: Canvas, markerTimes: HashMap, xMin: Float, xCurrent: Float): Float {
- if (markerTimes.isNotEmpty() && xCurrent > xMin + markerTimes.map { 36 + labelPaint.measureText(timeLabel(it.value)) }.sum()) {
- var shift = 0
- for (item in markerTimes) {
- val label = timeLabel(item.value)
- val labelPaint = markerPaint[item.key]!!.Label
+ private fun drawMarkerLabel(canvas: Canvas, markerTimes: HashMap, xCurrent: Float, xLimit: Float): Float {
+ if (markerTimes.isEmpty()) return xLimit
- canvas.drawBitmap(
- markerIcon[item.key]!!,
- xMin - (textSize / 2f) + shift,
- (yMargin / 3f),
- labelPaint
- )
+ val marker = markerTimes.minByOrNull { it.key } ?: return xLimit
+ val icon = markerIcon[marker.key] ?: return xLimit
+ val paint = markerPaint[marker.key]?.Label ?: return xLimit
- canvas.drawText(
- label,
- xMin + labelPaint.textSize + shift,
- yMargin - labelPaint.textSize + 2f,
- labelPaint
- )
+ val labels = markerTimes.map { it }.sortedBy { it.key }.associateBy({ it.key }, { timeLabel(it.value) })
- shift += 36 + labelPaint.measureText(timeLabel(item.value)).roundToInt()
- }
+ val padding = 8f
+ val spaceNeeded = icon.width + labels.maxOf { paint.measureText(it.value).roundToInt() } + 2 * padding
- markerTimes.clear()
+ if (xCurrent <= xLimit + spaceNeeded) return xLimit
- return xCurrent
+ canvas.drawBitmap(
+ icon,
+ xLimit - (labelPaint.textSize / 2f),
+ (yMargin / 3f),
+ paint
+ )
+
+ var yStart = when (labels.size) {
+ 1 -> yMargin - labelPaint.textSize + padding
+ else -> labelPaint.textSize + padding / 2
+ }
+
+ for (label in labels) {
+ val labelPaint = markerPaint[label.key]?.Label ?: continue
+
+ canvas.drawText(
+ label.value,
+ xLimit + (labelPaint.textSize / 2f) + padding,
+ yStart,
+ labelPaint
+ )
+
+ yStart += labelPaint.textSize
}
- return xMin
+ markerTimes.clear()
+
+ return xCurrent
}
private fun drawMarkerLine(canvas: Canvas, markerType: PlotMarkerType, x: Float?, multiplier: Int) {
@@ -649,10 +852,7 @@ class PlotView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
val top = yMargin.toFloat() + 1
val bottom = canvas.height - yMargin.toFloat() - 1
- val linePath = Path()
- linePath.moveTo(x, top)
- linePath.lineTo(x, bottom)
- canvas.drawPath(linePath, markerPaint[markerType]!!.Line)
+ drawLine(canvas, x, top, x, bottom, markerPaint[markerType]!!.Line)
val trianglePath = Path()
trianglePath.moveTo(x, top)
@@ -661,70 +861,7 @@ class PlotView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
trianglePath.lineTo(x, top)
canvas.drawPath(trianglePath, markerPaint[markerType]!!.Mark)
}
-
- private fun drawXLines(canvas: Canvas) {
- val maxX = width.toFloat()
- val maxY = height.toFloat()
-
- val distanceDimension = when {
- dimensionRestriction != null -> dimensionRestriction!!.toFloat()
- else -> plotLines.mapNotNull { it.distanceDimension(dimension, dimensionRestriction) }.max()?:0f
- }
-
- for (i in 0 until xLineCount) {
- val cordX = x(i.toFloat(), 0f, xLineCount.toFloat() - 1, maxX)!!
- val cordY = maxY - yMargin
-
- val leftZero = (distanceDimension / (xLineCount - 1) * i) + (dimensionShift ?: 0L)
- val rightZero = distanceDimension - leftZero + (2 * (dimensionShift ?: 0L))
-
- if (i in 1 until xLineCount - 1) {
- drawXLine(canvas, cordX, maxY, labelLinePaint)
- } else {
- drawXLine(canvas, cordX, maxY, borderLinePaint)
- }
-
- val label = when (dimension) {
- PlotDimension.INDEX -> label(leftZero, PlotLineLabelFormat.NUMBER)
- PlotDimension.DISTANCE -> label(rightZero, PlotLineLabelFormat.DISTANCE)
- PlotDimension.TIME -> label(leftZero, PlotLineLabelFormat.TIME)
- PlotDimension.STATE_OF_CHARGE -> label(leftZero, PlotLineLabelFormat.NUMBER)
- }
-
- val bounds = Rect()
- labelPaint.getTextBounds(label, 0, label.length, bounds)
-
- canvas.drawText(
- label,
- cordX - bounds.width() / 2,
- cordY + yMargin / 2 + bounds.height() / 2,
- labelPaint
- )
- }
- }
-
- private fun drawXLine(canvas: Canvas, cord: Float?, maxY: Float, paint: Paint?) {
- if (cord == null) return
- val path = Path()
- path.moveTo(cord, yMargin.toFloat())
- path.lineTo(cord, maxY - yMargin)
- canvas.drawPath(path, paint ?: labelLinePaint)
- }
-
- private fun drawYBaseLines(canvas: Canvas) {
- val maxX = width.toFloat()
- val maxY = height.toFloat()
-
- for (i in 0 until yLineCount) {
- val cordY = y(i.toFloat(), 0f, yLineCount.toFloat() - 1, maxY)!!
-
- if (i in 1 until yLineCount - 1) {
- drawYLine(canvas, cordY, maxX, labelLinePaint)
- } else {
- drawYLine(canvas, cordY, maxX, borderLinePaint)
- }
- }
- }
+
private fun drawYLines(canvas: Canvas) {
val maxX = width.toFloat()
val maxY = height.toFloat()
@@ -734,7 +871,10 @@ class PlotView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
for (drawHighlightLabelOnly in listOf(false, true)) {
var index = 0
- for (line in plotLines.filter { it.Visible }) {
+ for (pair in plotLines.filter { it.first.Visible }) {
+ val line = pair.first
+ val plotPaint = pair.second
+
for (secondaryDimension in arrayListOf(null, secondaryDimension).distinct()) {
// only draw one secondary
if (secondaryDimension != null && index++ > 0) continue
@@ -745,14 +885,20 @@ class PlotView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
else -> line.Configuration
} ?: continue
- val paint = when {
- secondaryDimension != null -> line.secondaryPlotPaint ?: line.plotPaint
- else -> line.plotPaint
- } ?: continue
+ val paint = plotPaint.bySecondaryDimension(secondaryDimension) ?: continue
val minValue = line.minValue(dataPoints, secondaryDimension)!!
val maxValue = line.maxValue(dataPoints, secondaryDimension)!!
- val highlight = line.byHighlightMethod(dataPoints, secondaryDimension)
+
+ val highlight = when (dimensionHighlightAt) {
+ null -> line.byHighlightMethod(dataPoints, dimension, secondaryDimension)
+ else -> dimensionHighlightValue[line]?.get(secondaryDimension)
+ }
+
+ val highlightMethod = when (dimensionHighlightAt) {
+ null -> configuration.HighlightMethod
+ else -> PlotHighlightMethod.RAW
+ }
val labelShiftY = (bounds.height() / 2).toFloat()
val valueShiftY = (maxValue - minValue) / (yLineCount - 1)
@@ -768,7 +914,7 @@ class PlotView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
}
val labelCordX = when (configuration.LabelPosition) {
- PlotLabelPosition.LEFT -> textSize
+ PlotLabelPosition.LEFT -> 0f // textSize
PlotLabelPosition.RIGHT -> maxX - xMargin + textSize / 2
else -> null
}
@@ -790,12 +936,17 @@ class PlotView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
}
}
- when (configuration.HighlightMethod) {
- PlotHighlightMethod.AVG_BY_INDEX -> drawYLine(canvas, highlightCordY, maxX, paint.HighlightLabelLine)
- PlotHighlightMethod.AVG_BY_DISTANCE -> drawYLine(canvas, highlightCordY, maxX, paint.HighlightLabelLine)
- PlotHighlightMethod.AVG_BY_TIME -> drawYLine(canvas, highlightCordY, maxX, paint.HighlightLabelLine)
- else -> {
- // Don't draw
+ if (highlightCordY != null && highlightCordY in yMargin.toFloat() .. maxY - yMargin) {
+ when (highlightMethod) {
+ PlotHighlightMethod.AVG_BY_DIMENSION,
+ PlotHighlightMethod.AVG_BY_INDEX,
+ PlotHighlightMethod.AVG_BY_DISTANCE,
+ PlotHighlightMethod.AVG_BY_STATE_OF_CHARGE,
+ PlotHighlightMethod.AVG_BY_TIME,
+ PlotHighlightMethod.RAW -> drawYLine(canvas, highlightCordY, maxX, paint.HighlightLabelLine)
+ else -> {
+ // Don't draw
+ }
}
}
@@ -804,29 +955,33 @@ class PlotView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
}
} else {
if (labelCordX != null && highlightCordY != null) {
- val label = label((highlight!! - valueCorrectionY) / configuration.Divider, configuration.LabelFormat, configuration.HighlightMethod)
+ val highlightCordYLimited = highlightCordY
+ .coerceAtLeast(yMargin.toFloat())
+ .coerceAtMost(maxY - yMargin)
+
+ val label = label((highlight!! - valueCorrectionY) / configuration.Divider, configuration.LabelFormat, highlightMethod)
paint.HighlightLabel.textSize = 35f
val labelWidth = paint.HighlightLabel.measureText(label)
val labelHeight = paint.HighlightLabel.textSize
val textBoxMargin = paint.HighlightLabel.textSize / 3.5f
canvas.drawRect(
- labelCordX + labelUnitXOffset - textBoxMargin,
- highlightCordY - labelHeight + labelShiftY,
- labelCordX + labelUnitXOffset + labelWidth + textBoxMargin,
- highlightCordY + labelShiftY + textBoxMargin,
+ labelCordX + paint.Plot.strokeWidth * 4 + labelUnitXOffset - textBoxMargin,
+ highlightCordYLimited - labelHeight + labelShiftY,
+ labelCordX + paint.Plot.strokeWidth * 4 + labelUnitXOffset + labelWidth + textBoxMargin,
+ highlightCordYLimited + labelShiftY + textBoxMargin,
backgroundPaint
)
canvas.drawRect(
- labelCordX + labelUnitXOffset - textBoxMargin,
- highlightCordY - labelHeight + labelShiftY,
- labelCordX + labelUnitXOffset + labelWidth + textBoxMargin,
- highlightCordY + labelShiftY + textBoxMargin,
+ labelCordX + paint.Plot.strokeWidth * 4 + labelUnitXOffset - textBoxMargin,
+ highlightCordYLimited - labelHeight + labelShiftY,
+ labelCordX + paint.Plot.strokeWidth * 4 + labelUnitXOffset + labelWidth + textBoxMargin,
+ highlightCordYLimited + labelShiftY + textBoxMargin,
paint.Plot
)
- canvas.drawText(label, labelCordX + labelUnitXOffset, highlightCordY + labelShiftY, paint.HighlightLabel)
+ canvas.drawText(label, labelCordX + paint.Plot.strokeWidth * 4 + labelUnitXOffset, highlightCordYLimited + labelShiftY, paint.HighlightLabel)
}
}
}
@@ -836,27 +991,50 @@ class PlotView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
private fun drawYLine(canvas: Canvas, cord: Float?, maxX: Float, paint: Paint?) {
if (cord == null) return
+ drawLine(canvas, xMargin.toFloat(), cord, maxX - xMargin, cord, paint ?: labelLinePaint)
+ }
+
+ private fun drawLine(canvas: Canvas, x1: Float, y1: Float, x2: Float, y2: Float, paint: Paint) {
val path = Path()
- path.moveTo(xMargin.toFloat(), cord)
- path.lineTo(maxX - xMargin, cord)
- canvas.drawPath(path, paint ?: labelLinePaint)
+ path.moveTo(x1, y1)
+ path.lineTo(x2, y2)
+ canvas.drawPath(path, paint)
}
- private fun timeLabel(time: Long): String {
- return when {
- TimeUnit.HOURS.convert(time, TimeUnit.NANOSECONDS) > 12 -> String.format("%02d:%02d", TimeUnit.DAYS.convert(time, TimeUnit.NANOSECONDS), TimeUnit.HOURS.convert(time, TimeUnit.NANOSECONDS) % 24)
- TimeUnit.MINUTES.convert(time, TimeUnit.NANOSECONDS) > 30 -> String.format("%02d:%02d'", TimeUnit.HOURS.convert(time, TimeUnit.NANOSECONDS), TimeUnit.MINUTES.convert(time, TimeUnit.NANOSECONDS) % 60)
- else -> String.format("%02d'%02d''", TimeUnit.MINUTES.convert(time, TimeUnit.NANOSECONDS), TimeUnit.SECONDS.convert(time, TimeUnit.NANOSECONDS) % 60)
+ private fun restrictCanvas(canvas: Canvas, maxX: Float, maxY: Float, xArea: Boolean = true, yArea: Boolean = true) {
+ // restrict canvas drawing region
+ canvas.save()
+ if (yArea) {
+ canvas.clipOutRect(0f, 0f, maxX, yMargin.toFloat()) // TOP
+ canvas.clipOutRect(0f, maxY - yMargin, maxX, maxY) // BOTTOM
+ }
+
+ if (xArea) {
+ canvas.clipOutRect(0f, 0f, xMargin.toFloat(), maxY) // LEFT
+ canvas.clipOutRect(maxX - xMargin, 0f, maxX, maxY) // RIGHT
}
}
+ private fun timeLabel(time: Long): String {
+ // return when {
+ // TimeUnit.HOURS.convert(time, TimeUnit.NANOSECONDS) > 12 -> String.format("%02d:%02d", TimeUnit.DAYS.convert(time, TimeUnit.NANOSECONDS), TimeUnit.HOURS.convert(time, TimeUnit.NANOSECONDS) % 24)
+ // TimeUnit.MINUTES.convert(time, TimeUnit.NANOSECONDS) > 30 -> String.format("%02d:%02d'", TimeUnit.HOURS.convert(time, TimeUnit.NANOSECONDS), TimeUnit.MINUTES.convert(time, TimeUnit.NANOSECONDS) % 60)
+ // else -> String.format("%02d'%02d''", TimeUnit.MINUTES.convert(time, TimeUnit.NANOSECONDS), TimeUnit.SECONDS.convert(time, TimeUnit.NANOSECONDS) % 60)
+ // }
+ return String.format("%02d:%02d",
+ TimeUnit.MILLISECONDS.toHours(time),
+ (TimeUnit.MILLISECONDS.toSeconds(time) / 60f).roundToInt() % TimeUnit.HOURS.toMinutes(1))
+ }
+
private fun label(value: Float, plotLineLabelFormat: PlotLineLabelFormat, plotHighlightMethod: PlotHighlightMethod? = null): String {
if (plotHighlightMethod == PlotHighlightMethod.NONE) return ""
- val suffix = when (plotLineLabelFormat) {
- PlotLineLabelFormat.PERCENTAGE -> " %"
- else -> ""
- }
+ // val suffix = when (plotLineLabelFormat) {
+ // PlotLineLabelFormat.PERCENTAGE -> " %"
+ // else -> ""
+ // }
+
+ val suffix = ""
return when (plotLineLabelFormat) {
PlotLineLabelFormat.NUMBER, PlotLineLabelFormat.PERCENTAGE -> when (plotHighlightMethod) {
@@ -869,8 +1047,8 @@ class PlotView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
}
PlotLineLabelFormat.TIME -> timeLabel(value.toLong())
PlotLineLabelFormat.DISTANCE -> when {
- value < 1000 -> String.format("%d m", (value - (value % 100)).toInt())
- else -> String.format("%d km", (value / 1000).toInt())
+ appPreferences.distanceUnit.toUnit(value) % 1000 > 100 -> String.format("%.1f %s", appPreferences.distanceUnit.toUnit(value) / 1000, appPreferences.distanceUnit.unit())
+ else -> String.format("%d %s", (appPreferences.distanceUnit.toUnit(value) / 1000).roundToInt(), appPreferences.distanceUnit.unit())
}
}
}
diff --git a/automotive/src/main/res/drawable/ic_arrow_back.xml b/automotive/src/main/res/drawable/ic_arrow_back.xml
index 3899dd31..751289db 100644
--- a/automotive/src/main/res/drawable/ic_arrow_back.xml
+++ b/automotive/src/main/res/drawable/ic_arrow_back.xml
@@ -1,6 +1,6 @@
diff --git a/automotive/src/main/res/drawable/ic_avg_consumption.xml b/automotive/src/main/res/drawable/ic_avg_consumption.xml
index 52fefe78..f56b35c9 100644
--- a/automotive/src/main/res/drawable/ic_avg_consumption.xml
+++ b/automotive/src/main/res/drawable/ic_avg_consumption.xml
@@ -1,8 +1,8 @@
diff --git a/automotive/src/main/res/drawable/ic_charger.xml b/automotive/src/main/res/drawable/ic_charger.xml
index fa0cff03..6ea203b3 100644
--- a/automotive/src/main/res/drawable/ic_charger.xml
+++ b/automotive/src/main/res/drawable/ic_charger.xml
@@ -1,6 +1,6 @@
+
+
+
\ No newline at end of file
diff --git a/automotive/src/main/res/drawable/ic_diagram.xml b/automotive/src/main/res/drawable/ic_diagram.xml
index 36f4a7f7..38105637 100644
--- a/automotive/src/main/res/drawable/ic_diagram.xml
+++ b/automotive/src/main/res/drawable/ic_diagram.xml
@@ -1,6 +1,6 @@
diff --git a/automotive/src/main/res/drawable/ic_distance.xml b/automotive/src/main/res/drawable/ic_distance.xml
index 596db6b8..f3b3fc8e 100644
--- a/automotive/src/main/res/drawable/ic_distance.xml
+++ b/automotive/src/main/res/drawable/ic_distance.xml
@@ -1,4 +1,4 @@
-
+
diff --git a/automotive/src/main/res/drawable/ic_info.xml b/automotive/src/main/res/drawable/ic_info.xml
new file mode 100644
index 00000000..02bda4fb
--- /dev/null
+++ b/automotive/src/main/res/drawable/ic_info.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/automotive/src/main/res/drawable/ic_kill.xml b/automotive/src/main/res/drawable/ic_kill.xml
index 49be9c43..9c6bc67d 100644
--- a/automotive/src/main/res/drawable/ic_kill.xml
+++ b/automotive/src/main/res/drawable/ic_kill.xml
@@ -1,6 +1,6 @@
+
+
diff --git a/automotive/src/main/res/drawable/ic_login.xml b/automotive/src/main/res/drawable/ic_login.xml
new file mode 100644
index 00000000..0864b3b4
--- /dev/null
+++ b/automotive/src/main/res/drawable/ic_login.xml
@@ -0,0 +1,11 @@
+
+
+
+
\ No newline at end of file
diff --git a/automotive/src/main/res/drawable/ic_performance.xml b/automotive/src/main/res/drawable/ic_performance.xml
index dd1bde7a..f22ce31d 100644
--- a/automotive/src/main/res/drawable/ic_performance.xml
+++ b/automotive/src/main/res/drawable/ic_performance.xml
@@ -1,6 +1,6 @@
+
diff --git a/automotive/src/main/res/drawable/ic_remaining_range.xml b/automotive/src/main/res/drawable/ic_remaining_range.xml
index 5ac72e4b..1e58432f 100644
--- a/automotive/src/main/res/drawable/ic_remaining_range.xml
+++ b/automotive/src/main/res/drawable/ic_remaining_range.xml
@@ -1,5 +1,5 @@
-
+
+
+
diff --git a/automotive/src/main/res/drawable/ic_send.xml b/automotive/src/main/res/drawable/ic_send.xml
new file mode 100644
index 00000000..1cc68aba
--- /dev/null
+++ b/automotive/src/main/res/drawable/ic_send.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/automotive/src/main/res/drawable/ic_settings.xml b/automotive/src/main/res/drawable/ic_settings.xml
index 8b57e452..7279b175 100644
--- a/automotive/src/main/res/drawable/ic_settings.xml
+++ b/automotive/src/main/res/drawable/ic_settings.xml
@@ -1,6 +1,6 @@
+
+
\ No newline at end of file
diff --git a/automotive/src/main/res/drawable/ic_time.xml b/automotive/src/main/res/drawable/ic_time.xml
index 2e11d36a..d0b67f82 100644
--- a/automotive/src/main/res/drawable/ic_time.xml
+++ b/automotive/src/main/res/drawable/ic_time.xml
@@ -1,4 +1,4 @@
-
+
diff --git a/automotive/src/main/res/layout/activity_about.xml b/automotive/src/main/res/layout/activity_about.xml
new file mode 100644
index 00000000..0e47edc1
--- /dev/null
+++ b/automotive/src/main/res/layout/activity_about.xml
@@ -0,0 +1,266 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/automotive/src/main/res/layout/activity_libs.xml b/automotive/src/main/res/layout/activity_libs.xml
new file mode 100644
index 00000000..46d6753c
--- /dev/null
+++ b/automotive/src/main/res/layout/activity_libs.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/automotive/src/main/res/layout/activity_log.xml b/automotive/src/main/res/layout/activity_log.xml
index cf5c4671..bb4a4190 100644
--- a/automotive/src/main/res/layout/activity_log.xml
+++ b/automotive/src/main/res/layout/activity_log.xml
@@ -21,9 +21,18 @@
+
+
-
-
+ android:layout_height="wrap_content">
+
+
+
+
+
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:orientation="vertical">
+
+
+
+
+
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/automotive/src/main/res/layout/activity_main.xml b/automotive/src/main/res/layout/activity_main.xml
index fe11b751..6605500e 100644
--- a/automotive/src/main/res/layout/activity_main.xml
+++ b/automotive/src/main/res/layout/activity_main.xml
@@ -14,8 +14,8 @@
@@ -101,7 +101,7 @@
android:layout_weight="6">
@@ -117,7 +117,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:textColor="?android:attr/textColorPrimary"
- android:textSize="35sp"
+ android:textSize="@dimen/std_font_size"
android:drawableStart="@drawable/ic_distance"
/>
@@ -128,7 +128,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:textColor="?android:attr/textColorPrimary"
- android:textSize="35sp"
+ android:textSize="@dimen/std_font_size"
android:drawableStart="@drawable/ic_power"
/>
@@ -138,7 +138,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:textColor="?android:attr/textColorPrimary"
- android:textSize="35sp"
+ android:textSize="@dimen/std_font_size"
android:drawableStart="@drawable/ic_avg_consumption"
/>
@@ -148,7 +148,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:textColor="?android:attr/textColorPrimary"
- android:textSize="35sp"
+ android:textSize="@dimen/std_font_size"
android:text=" 00:00:00"
android:drawableStart="@drawable/ic_time"
/>
@@ -160,7 +160,7 @@
android:layout_marginTop="5dp"
android:layout_marginBottom="10dp"
android:textColor="?android:attr/textColorPrimary"
- android:textSize="35sp"
+ android:textSize="@dimen/std_font_size"
android:text=" -/- km"
android:drawableStart="@drawable/ic_remaining_range"
/>
@@ -172,7 +172,7 @@
@@ -180,7 +180,7 @@
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:layout_weight="1"
- android:layout_margin="0dp"
+ android:layout_marginHorizontal="20dp"
android:id="@+id/main_consumption_plot"/>
@@ -229,7 +229,7 @@
android:id="@+id/main_checkbox_speed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="@string/main_checkbox_speed"
+ android:text="@string/main_speed"
android:textSize="35sp"
android:layout_marginTop="0dp"
android:layout_marginBottom="5dp"
@@ -251,7 +251,7 @@
android:layout_marginEnd="7.5dp"
android:text="@string/main_button_trip_summary"/>