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 @@ +